<p dir="ltr">Hey Carl,<br>
Yeah, this looks nifty. I've got some comments on the implementation though.</p>
<p dir="ltr">On Dec 11, 2014 11:55 AM, "Carl Worth" <<a href="mailto:cworth@cworth.org">cworth@cworth.org</a>> wrote:<br>
><br>
> This new interface allows for writing a series of objects to a chunk<br>
> of memory (a "blob").. The allocated memory is maintained within the<br>
> blob itself, (and re-allocated by doubling when necessary).<br>
><br>
> There are also functions for reading objects from a blob as well. If<br>
> code attempts to read beyond the available memory, the read functions<br>
> return 0 values (or its moral equivalent) without reading past the<br>
> allocated memory. Once the caller is done with the reads, it can check<br>
> blob->overrun to ensure whether any invalid values were previously<br>
> returned due to attempts to read too far.<br>
> ---<br>
> src/glsl/Makefile.sources | 3 +-<br>
> src/glsl/blob.c | 287 ++++++++++++++++++++++++++++++++++++++++++++++<br>
> src/glsl/blob.h | 263 ++++++++++++++++++++++++++++++++++++++++++<br>
> 3 files changed, 552 insertions(+), 1 deletion(-)<br>
> create mode 100644 src/glsl/blob.c<br>
> create mode 100644 src/glsl/blob.h<br>
><br>
> diff --git a/src/glsl/Makefile.sources b/src/glsl/Makefile.sources<br>
> index 676fa0d..875e4bf 100644<br>
> --- a/src/glsl/Makefile.sources<br>
> +++ b/src/glsl/Makefile.sources<br>
> @@ -105,7 +105,8 @@ LIBGLSL_FILES = \<br>
> $(GLSL_SRCDIR)/opt_swizzle_swizzle.cpp \<br>
> $(GLSL_SRCDIR)/opt_tree_grafting.cpp \<br>
> $(GLSL_SRCDIR)/opt_vectorize.cpp \<br>
> - $(GLSL_SRCDIR)/s_expression.cpp<br>
> + $(GLSL_SRCDIR)/s_expression.cpp \<br>
> + $(GLSL_SRCDIR)/blob.c<br>
><br>
> # glsl_compiler<br>
><br>
> diff --git a/src/glsl/blob.c b/src/glsl/blob.c<br>
> new file mode 100644<br>
> index 0000000..0af71fc<br>
> --- /dev/null<br>
> +++ b/src/glsl/blob.c<br>
> @@ -0,0 +1,287 @@<br>
> +/*<br>
> + * Copyright © 2014 Intel Corporation<br>
> + *<br>
> + * Permission is hereby granted, free of charge, to any person obtaining a<br>
> + * copy of this software and associated documentation files (the "Software"),<br>
> + * to deal in the Software without restriction, including without limitation<br>
> + * the rights to use, copy, modify, merge, publish, distribute, sublicense,<br>
> + * and/or sell copies of the Software, and to permit persons to whom the<br>
> + * Software is furnished to do so, subject to the following conditions:<br>
> + *<br>
> + * The above copyright notice and this permission notice (including the next<br>
> + * paragraph) shall be included in all copies or substantial portions of the<br>
> + * Software.<br>
> + *<br>
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR<br>
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,<br>
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL<br>
> + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER<br>
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING<br>
> + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS<br>
> + * IN THE SOFTWARE.<br>
> + */<br>
> +<br>
> +#include <string.h><br>
> +<br>
> +#include "main/macros.h"<br>
> +#include "util/ralloc.h"<br>
> +#include "blob.h"<br>
> +<br>
> +#define BLOB_INITIAL_SIZE 4096<br>
> +<br>
> +/* Ensure that \blob will be able to fit an additional object of size<br>
> + * \additional. The growing (if any) will occur by doubling the existing<br>
> + * allocation.<br>
> + */<br>
> +static bool<br>
> +grow_to_fit (struct blob *blob, size_t additional)<br>
> +{<br>
> + size_t to_allocate;<br>
> + uint8_t *new_data;<br>
> +<br>
> + if (blob->size + additional <= blob->allocated)<br>
> + return true;<br>
> +<br>
> + if (blob->allocated == 0)<br>
> + to_allocate = BLOB_INITIAL_SIZE;<br>
> + else<br>
> + to_allocate = blob->allocated * 2;</p>
<p dir="ltr">What happens if additional is bigger than blob->size? Maybe iterate (if you want to keep power-of-two) or take a max here.</p>
<p dir="ltr">> +<br>
> + new_data = reralloc_size(blob, blob->data, to_allocate);<br>
> + if (new_data == NULL)<br>
> + return false;<br>
> +<br>
> + blob->data = new_data;<br>
> + blob->allocated = to_allocate;<br>
> +<br>
> + return true;<br>
> +}<br>
> +<br>
> +/* Align the blob->size so that reading or writing a value at (blob->data +<br>
> + * blob->size) will result in an access aligned to a granularity of \alignment<br>
> + * bytes.<br>
> + *<br>
> + * \return True unless allocation fails<br>
> + */<br>
> +static bool<br>
> +align_blob (struct blob *blob, size_t alignment)<br>
> +{<br>
> + size_t new_size;<br>
> +<br>
> + new_size = ALIGN(blob->size, alignment);</p>
<p dir="ltr">Do we what to align the pointer or the size? Aligning the size only makes sense if the pointer is already aligned. At the very least, we should assert that the requested alignment is less than or equal to the known pointer alignment for the start of the blob.</p>
<p dir="ltr">> +<br>
> + if (! grow_to_fit (blob, new_size - blob->size))<br>
> + return false;<br>
> +<br>
> + blob->size = new_size;<br>
> +<br>
> + return true;<br>
> +}<br>
> +<br>
> +static void<br>
> +align_blob_reader (struct blob_reader *blob, size_t alignment)<br>
> +{<br>
> + blob->current = blob->data + ALIGN(blob->current - blob->data, alignment);</p>
<p dir="ltr">Again, pointer or size? Also, why is this way up here when the other reader stuff comes later.</p>
<p dir="ltr">> +}<br>
> +<br>
> +struct blob *<br>
> +blob_create (void *mem_ctx)<br>
> +{<br>
> + struct blob *blob;<br>
> +<br>
> + blob = ralloc(mem_ctx, struct blob);<br>
> + if (blob == NULL)<br>
> + return NULL;<br>
> +<br>
> + blob->data = NULL;<br>
> + blob->allocated = 0;<br>
> + blob->size = 0;<br>
> +<br>
> + return blob;<br>
> +}<br>
> +<br>
> +bool<br>
> +blob_write_bytes (struct blob *blob, const void *bytes, size_t to_write)<br>
> +{<br>
> + if (! grow_to_fit (blob, to_write))<br>
> + return false;<br>
> +<br>
> + memcpy (blob->data + blob->size, bytes, to_write);<br>
> + blob->size += to_write;<br>
> +<br>
> + return true;<br>
> +}<br>
> +<br>
> +uint8_t *<br>
> +blob_reserve_bytes (struct blob *blob, size_t to_write)<br>
> +{<br>
> + uint8_t *ret;<br>
> +<br>
> + if (! grow_to_fit (blob, to_write))<br>
> + return NULL;<br>
> +<br>
> + ret = blob->data + blob->size;<br>
> + blob->size += to_write;<br>
> +<br>
> + return ret;<br>
> +}<br>
> +<br>
> +bool<br>
> +blob_write_uint32 (struct blob *blob, uint32_t value)<br>
> +{<br>
> + align_blob(blob, sizeof(value));<br>
> +<br>
> + return blob_write_bytes(blob, &value, sizeof(value));<br>
> +}<br>
> +<br>
> +bool<br>
> +blob_write_uint64 (struct blob *blob, uint64_t value)<br>
> +{<br>
> + align_blob(blob, sizeof(value));<br>
> +<br>
> + return blob_write_bytes(blob, &value, sizeof(value));<br>
> +}<br>
> +<br>
> +bool<br>
> +blob_write_intptr (struct blob *blob, intptr_t value)<br>
> +{<br>
> + align_blob(blob, sizeof(value));<br>
> +<br>
> + return blob_write_bytes(blob, &value, sizeof(value));<br>
> +}<br>
> +<br>
> +bool<br>
> +blob_write_string (struct blob *blob, const char *str)<br>
> +{<br>
> + return blob_write_bytes(blob, str, strlen(str) + 1);<br>
> +}<br>
> +<br>
> +void<br>
> +blob_reader_init (struct blob_reader *blob, uint8_t *data, size_t size)</p>
<p dir="ltr">Calling it blob is a bit disconcerting, but probably not a big deal.</p>
<p dir="ltr">> +{<br>
> + blob->data = data;<br>
> + blob->end = data + size;<br>
> + blob->current = data;<br>
> + blob->overrun = false;<br>
> +}<br>
> +<br>
> +/* Check that an object of size \size can be read from this blob.<br>
> + *<br>
> + * If not, set blob->overrun to indicate that we attempted to read too far.<br>
> + */<br>
> +static bool<br>
> +ensure_can_read (struct blob_reader *blob, size_t size)<br>
> +{<br>
> + if (blob->current < blob->end && blob->end - blob->current >= size)<br>
> + return true;<br>
> +<br>
> + blob->overrun = true;<br>
> +<br>
> + return false;<br>
> +}<br>
> +<br>
> +void *<br>
> +blob_read_bytes (struct blob_reader *blob, size_t size)<br>
> +{<br>
> + void *ret;<br>
> +<br>
> + if (blob->current > blob->end || blob->end - blob->current < size)<br>
> + return NULL;</p>
<p dir="ltr">Why isn't this a call to ensure_can_read? Without this, the overrun flag never gets set. We also don't get overrun set for out-of-bounds copies and string reads. If that's intentional, it should be documented.</p>
<p dir="ltr">Also, you reversed the logic wrong. It should be current >= end.</p>
<p dir="ltr">> +<br>
> + ret = blob->current;<br>
> +<br>
> + blob->current += size;<br>
> +<br>
> + return ret;<br>
> +}<br>
> +<br>
> +void<br>
> +blob_copy_bytes (struct blob_reader *blob, uint8_t *dest, size_t size)<br>
> +{<br>
> + uint8_t *bytes;<br>
> +<br>
> + bytes = blob_read_bytes (blob, size);<br>
> + if (bytes == NULL)<br>
> + return;<br>
> +<br>
> + memcpy (dest, bytes, size);<br>
> +}<br>
> +<br>
> +uint32_t<br>
> +blob_read_uint32 (struct blob_reader *blob)<br>
> +{<br>
> + uint32_t ret;<br>
> + int size = sizeof(ret);<br>
> +<br>
> + align_blob_reader(blob, size);<br>
> +<br>
> + if (! ensure_can_read(blob, size))<br>
> + return 0;<br>
> +<br>
> + ret = *((uint32_t*) blob->current);<br>
> +<br>
> + blob->current += size;<br>
> +<br>
> + return ret;<br>
> +}<br>
> +<br>
> +uint64_t<br>
> +blob_read_uint64 (struct blob_reader *blob)<br>
> +{<br>
> + uint64_t ret;<br>
> + int size = sizeof(ret);<br>
> +<br>
> + align_blob_reader(blob, size);<br>
> +<br>
> + if (! ensure_can_read(blob, size))<br>
> + return 0;<br>
> +<br>
> + ret = *((uint64_t*) blob->current);<br>
> +<br>
> + blob->current += size;<br>
> +<br>
> + return ret;<br>
> +}<br>
> +<br>
> +intptr_t<br>
> +blob_read_intptr (struct blob_reader *blob)<br>
> +{<br>
> + intptr_t ret;<br>
> + int size = sizeof(ret);<br>
> +<br>
> + align_blob_reader(blob, size);<br>
> +<br>
> + if (! ensure_can_read(blob, size))<br>
> + return 0;<br>
> +<br>
> + ret = *((intptr_t *) blob->current);<br>
> +<br>
> + blob->current += size;<br>
> +<br>
> + return ret;<br>
> +}<br>
> +<br>
> +char *<br>
> +blob_read_string (struct blob_reader *blob)<br>
> +{<br>
> + int size;<br>
> + char *ret;<br>
> + uint8_t *nul;<br>
> +<br>
> + /* If there is no NULL terminator, the reader gets nothing. */<br>
> + nul = memchr (blob->current, 0, blob->end - blob->current);</p>
<p dir="ltr">Should we check for current >= end first?...</p>
<p dir="ltr">> +<br>
> + if (nul == NULL)<br>
> + return NULL;<br>
> +<br>
> + size = nul - blob->current + 1;<br>
> +<br>
> + if (! ensure_can_read(blob, size))<br>
> + return 0;</p>
<p dir="ltr">...If we do, this does nothing. We may want to set the overrun flag if there is no null.</p>
<p dir="ltr">> +<br>
> + ret = (char *) blob->current;<br>
> +<br>
> + blob->current += size;<br>
> +<br>
> + return ret;<br>
> +}<br>
> diff --git a/src/glsl/blob.h b/src/glsl/blob.h<br>
> new file mode 100644<br>
> index 0000000..374b21e<br>
> --- /dev/null<br>
> +++ b/src/glsl/blob.h<br>
> @@ -0,0 +1,263 @@<br>
> +/*<br>
> + * Copyright © 2014 Intel Corporation<br>
> + *<br>
> + * Permission is hereby granted, free of charge, to any person obtaining a<br>
> + * copy of this software and associated documentation files (the "Software"),<br>
> + * to deal in the Software without restriction, including without limitation<br>
> + * the rights to use, copy, modify, merge, publish, distribute, sublicense,<br>
> + * and/or sell copies of the Software, and to permit persons to whom the<br>
> + * Software is furnished to do so, subject to the following conditions:<br>
> + *<br>
> + * The above copyright notice and this permission notice (including the next<br>
> + * paragraph) shall be included in all copies or substantial portions of the<br>
> + * Software.<br>
> + *<br>
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR<br>
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,<br>
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL<br>
> + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER<br>
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING<br>
> + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS<br>
> + * IN THE SOFTWARE.<br>
> + */<br>
> +<br>
> +#pragma once<br>
> +#ifndef BLOB_H<br>
> +#define BLOB_H<br>
> +<br>
> +#ifdef __cplusplus<br>
> +extern "C" {<br>
> +#endif<br>
> +<br>
> +#include <stdint.h><br>
> +<br>
> +/* The blob functions implement a simple, low-level API for serializing and<br>
> + * deserializing.<br>
> + *<br>
> + * All objects written to a blob will be serialized directly, (without any<br>
> + * additional meta-data to describe the data written). Therefore, it is the<br>
> + * caller's responsibility to ensure that any data can be read later, (either<br>
> + * by knowing exactly what data is expected, or by writing to the blob<br>
> + * sufficient meta-data to describe what has been written).<br>
> + *<br>
> + * A blob is efficient in that it dynamically grows by doubling in size, so<br>
> + * allocation costs are logarithmic.<br>
> + */<br>
> +<br>
> +/* The allocated size is the number of bytes that have actually been allocated<br>
> + * for the "data" array, while "size" is the number of bytes actually used to<br>
> + * store data.<br>
> + */<br>
> +struct blob {<br>
> + uint8_t *data;<br>
> + size_t allocated;<br>
> + size_t size;<br>
> +};<br>
> +<br>
> +/* When done reading, the caller can ensure that everything was consumed by<br>
> + * checking the following:<br>
> + *<br>
> + * 1. blob->data should be equal to blob->end, (if not, too little was<br>
> + * read).<br>
> + *<br>
> + * 2. blob->overrun should be false, (otherwise, too much was read).<br>
> + */<br>
> +struct blob_reader {<br>
> + uint8_t *data;<br>
> + uint8_t *end;<br>
> + uint8_t *current;<br>
> + bool overrun;<br>
> +};<br>
> +<br>
> +/**<br>
> + * Create a new, empty blob, belonging to \mem_ctx.<br>
> + *<br>
> + * \return The new blob, (or NULL in case of allocation failure).<br>
> + */<br>
> +struct blob *<br>
> +blob_create (void *mem_ctx);<br>
> +<br>
> +/**<br>
> + * Add some unstructured, fixed-size data to a blob.<br>
> + *<br>
> + * \return True unless allocation failed.<br>
> + */<br>
> +bool<br>
> +blob_write_bytes (struct blob *blob, const void *bytes, size_t to_write);<br>
> +<br>
> +/**<br>
> + * Reserve space in \blob for a number of bytes.<br>
> + *<br>
> + * Space will be allocated within the blob for these byes, but the bytes will<br>
> + * be left uninitialized. The caller is expected to use the return value to<br>
> + * write directly (and immediately) to these bytes.<br>
> + *<br>
> + * NOTE: The return value is valid immediately upon return, but can be<br>
> + * invalidated by any other call to a blob function. So the caller should call<br>
> + * blob_reserve_byes immediately before writing through the returned pointer.<br>
> + *<br>
> + * This function is intended to be used when interfacing with an existing API<br>
> + * that is not aware of the blob API, (so that blob_write_bytes cannot be<br>
> + * called).<br>
> + *<br>
> + * \return A pointer to space allocated within \blob to which \to_write bytes<br>
> + * can be written, (or NULL in case of any allocation error).<br>
> + */<br>
> +uint8_t *<br>
> +blob_reserve_bytes (struct blob *blob, size_t to_write);<br>
> +<br>
> +/**<br>
> + * Overwrite some data previously written to the blob.<br>
> + *<br>
> + * Writes data to an existing portion of the blob at an offset of \offset.<br>
> + * This data range must have previously been written to the blob by one of the<br>
> + * blob_write_* calls.<br>
> + *<br>
> + * For example usage, see blob_overwrite_uint32<br>
> + *<br>
> + * \return True unless the requested offset or offset+to_write lie outside<br>
> + * the current blob's size.<br>
> + */<br>
> +bool<br>
> +blob_overwrite_bytes (struct blob *blob,<br>
> + size_t offset,<br>
> + const void *bytes,<br>
> + size_t to_write);</p>
<p dir="ltr">This function isn't implemented.</p>
<p dir="ltr">> +<br>
> +/**<br>
> + * Add a uint32_t to a blob.<br>
> + *<br>
> + * Note: This function will only write to a uint32_t-aligned offset from the<br>
> + * beginning of the blob's data, so some padding bytes may be added to the<br>
> + * blob if this write follows some unaligned write (such as<br>
> + * blob_write_string).<br>
> + *<br>
> + * \return True unless allocation failed.<br>
> + */<br>
> +bool<br>
> +blob_write_uint32 (struct blob *blob, uint32_t value);<br>
> +<br>
> +/**<br>
> + * Add a uint64_t to a blob.<br>
> + *<br>
> + * Note: This function will only write to a uint64_t-aligned offset from the<br>
> + * beginning of the blob's data, so some padding bytes may be added to the<br>
> + * blob if this write follows some unaligned write (such as<br>
> + * blob_write_string).<br>
> + *<br>
> + * \return True unless allocation failed.<br>
> + */<br>
> +bool<br>
> +blob_write_uint64 (struct blob *blob, uint64_t value);<br>
> +<br>
> +/**<br>
> + * Add an intptr_t to a blob.<br>
> + *<br>
> + * Note: This function will only write to an intptr_t-aligned offset from the<br>
> + * beginning of the blob's data, so some padding bytes may be added to the<br>
> + * blob if this write follows some unaligned write (such as<br>
> + * blob_write_string).<br>
> + *<br>
> + * \return True unless allocation failed.<br>
> + */<br>
> +bool<br>
> +blob_write_intptr (struct blob *blob, intptr_t value);<br>
> +<br>
> +/**<br>
> + * Add a NULL-terminated string to a blob, (including the NULL terminator).<br>
> + *<br>
> + * \return True unless allocation failed.<br>
> + */<br>
> +bool<br>
> +blob_write_string (struct blob *blob, const char *str);<br>
> +<br>
> +/**<br>
> + * Start reading a blob, (inintializing the contents of \blob for reading).<br>
> + *<br>
> + * After this call, the caller can use the various blob_read_* functions to<br>
> + * read elements from the data array.<br>
> + *<br>
> + * For all of the blob_read_* functions, if there is insufficient data<br>
> + * remaining, the functions will do nothing, (perhaps returning default values<br>
> + * such as 0). The caller can detect this by noting that the blob_reader's<br>
> + * current value is unchanged before and after the call.<br>
> + */<br>
> +void<br>
> +blob_reader_init (struct blob_reader *blob, uint8_t *data, size_t size);<br>
> +<br>
> +/**<br>
> + * Read some unstructured, fixed-size data from the current location, (and<br>
> + * update the current location to just past this data).<br>
> + *<br>
> + * The memory returned belongs to the data underlying the blob reader. The<br>
> + * caller must copy the data in order to use it after the lifetime of the data<br>
> + * underlying the blob reader.<br>
> + *<br>
> + * \return The bytes read (see note above about memory lifetime).<br>
> + */<br>
> +void *<br>
> +blob_read_bytes (struct blob_reader *blob, size_t size);<br>
> +<br>
> +/**<br>
> + * Read some unstructured, fixed-size data from the current location, copying<br>
> + * it to \dest (and update the current location to just past this data)<br>
> + */<br>
> +void<br>
> +blob_copy_bytes (struct blob_reader *blob, uint8_t *dest, size_t size);<br>
> +<br>
> +/**<br>
> + * Read a uint32_t from the current location, (and update the current location<br>
> + * to just past this uint32_t).<br>
> + *<br>
> + * Note: This function will only read from a uint32_t-aligned offset from the<br>
> + * beginning of the blob's data, so some padding bytes may be skipped.<br>
> + *<br>
> + * \return The uint32_t read<br>
> + */<br>
> +uint32_t<br>
> +blob_read_uint32 (struct blob_reader *blob);<br>
> +<br>
> +/**<br>
> + * Read a uint64_t from the current location, (and update the current location<br>
> + * to just past this uint64_t).<br>
> + *<br>
> + * Note: This function will only read from a uint64_t-aligned offset from the<br>
> + * beginning of the blob's data, so some padding bytes may be skipped.<br>
> + *<br>
> + * \return The uint64_t read<br>
> + */<br>
> +uint64_t<br>
> +blob_read_uint64 (struct blob_reader *blob);<br>
> +<br>
> +/**<br>
> + * Read an intptr_t value from the current location, (and update the<br>
> + * current location to just past this intptr_t).<br>
> + *<br>
> + * Note: This function will only read from an intptr_t-aligned offset from the<br>
> + * beginning of the blob's data, so some padding bytes may be skipped.<br>
> + *<br>
> + * \return The intptr_t read<br>
> + */<br>
> +intptr_t<br>
> +blob_read_intptr (struct blob_reader *blob);<br>
> +<br>
> +/**<br>
> + * Read a NULL-terminated string from the current location, (and update the<br>
> + * current location to just past this string).<br>
> + *<br>
> + * The memory returned belongs to the data underlying the blob reader. The<br>
> + * caller must copy the string in order to use the string after the lifetime<br>
> + * of the data underlying the blob reader.<br>
> + *<br>
> + * \return The string read (see note above about memory lifetime). However, if<br>
> + * there is no NULL byte remaining within the blob, this function returns<br>
> + * NULL.<br>
> + */<br>
> +char *<br>
> +blob_read_string (struct blob_reader *blob);<br>
> +<br>
> +#ifdef __cplusplus<br>
> +}<br>
> +#endif<br>
> +<br>
> +#endif /* BLOB_H */<br>
> --<br>
> 2.1.1<br>
><br>
> _______________________________________________<br>
> mesa-dev mailing list<br>
> <a href="mailto:mesa-dev@lists.freedesktop.org">mesa-dev@lists.freedesktop.org</a><br>
> <a href="http://lists.freedesktop.org/mailman/listinfo/mesa-dev">http://lists.freedesktop.org/mailman/listinfo/mesa-dev</a></p>