Browse Source

Buffer readbacks;

bjorn 1 year ago
parent
commit
0bce32c19a

+ 153 - 17
api/init.lua

@@ -12918,6 +12918,26 @@ return {
                 }
               }
             },
+            {
+              name = "getData",
+              tag = "buffer-transfer",
+              summary = "Get the data in the Buffer.",
+              description = "Downloads the Buffer's data from VRAM and returns it as a table.  This function is very very slow because it stalls the CPU until the data is finished downloading, so it should only be used for debugging or non-interactive scripts.  `Buffer:newReadback` is an alternative that returns a `Readback` object, which will not block the CPU.",
+              key = "Buffer:getData",
+              module = "lovr.graphics",
+              notes = "The length of the table will equal the number of items read.  Here are some examples of how the table is formatted:\n\n    buffer = lovr.graphics.newBuffer('int', { 7 })\n    buffer:getData() --> returns { 7 }\n\n    buffer = lovr.graphics.newBuffer('vec3', { 7, 8, 9 })\n    buffer:getData() --> returns {{ 7, 8, 9 }}\n\n    buffer = lovr.graphics.newBuffer('int', { 1, 2, 3 })\n    buffer:getData() --> returns { 1, 2, 3 }\n\n    buffer = lovr.graphics.newBuffer({ 'vec2', 'vec2' }, {\n      vec2(1,2), vec2(3,4),\n      vec2(5,6), vec2(7,8)\n    })\n    buffer:getData() --> returns { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } }\n\n    buffer = lovr.graphics.newBuffer({\n      { 'a', 'float' },\n      { 'b', 'float' }\n    }, { a = 1, b = 2 })\n    buffer:getData() --> returns { { 1, 2 } }\n\n    buffer = lovr.graphics.newBuffer({\n      { 'x', 'int', 3 }\n    }, { x = { 1, 2, 3 } })\n    buffer:getData() --> returns { { x = { 1, 2, 3 } } }\n\n    buffer = lovr.graphics.newBuffer({\n      { 'lights', {\n        { 'pos', 'vec3' },\n        { 'size', 'float' },\n      }, 10}\n    }, data)\n    buffer:getData() --> returns { { lights = { { pos = ..., size = ... }, ... } } }\n\nIn summary, each individual item is wrapped in a table, except if the format is a single number. If the format has nested types or arrays then the tables will be key-value, otherwise they will use numeric keys.",
+              related = {
+                "Buffer:newReadback",
+                "Buffer:mapData",
+                "Readback:getData"
+              },
+              variants = {
+                arguments = {},
+                returns = {
+                  "t"
+                }
+              }
+            },
             {
               name = "getFormat",
               tag = "buffer-metadata",
@@ -13002,13 +13022,15 @@ return {
             {
               name = "getPointer",
               tag = "buffer-transfer",
-              summary = "Get a raw pointer to the Buffer memory.",
-              description = "Returns a raw pointer to the Buffer's memory as a lightuserdata, intended for use with the LuaJIT FFI or for passing to C libraries.",
+              summary = "Get a writable pointer to the Buffer's memory.",
+              description = "Returns a pointer to GPU memory and schedules a copy from this pointer to the buffer's data. The data in the pointer will replace the data in the buffer.  This is intended for use with the LuaJIT FFI or for passing to C libraries.",
               key = "Buffer:getPointer",
               module = "lovr.graphics",
-              notes = "The pointer remains valid until the next call to `lovr.graphics.submit`, during which the data in the pointer will be uploaded to the buffer.\n\nThe initial contents of the pointer are undefined.\n\nCurrently the pointer addresses a range equal to the size of the Buffer, and so this overwrites the entire contents of the Buffer.\n\nSpecial care should be taken when writing data:\n\n- Reading data from the pointer will be very slow on some systems, and should be avoided.\n- It is better to write data to the pointer sequentially.  Random access may be slower.",
+              deprecated = true,
+              notes = "The pointer remains valid until the next call to `lovr.graphics.submit`, during which the data in the pointer will be uploaded to the buffer.\n\nThe initial contents of the pointer are undefined.\n\nSpecial care should be taken when writing data:\n\n- Reading data from the pointer will be very slow on some systems, and should be avoided.\n- It is better to write data to the pointer sequentially.  Random access may be slower.",
               related = {
-                "Blob:getPointer"
+                "Blob:getPointer",
+                "Buffer:mapData"
               },
               variants = {
                 {
@@ -13095,48 +13117,134 @@ return {
                 }
               }
             },
+            {
+              name = "mapData",
+              tag = "buffer-transfer",
+              summary = "Get a writable pointer to the Buffer's memory.",
+              description = "Returns a pointer to GPU memory and schedules a copy from this pointer to the buffer's data. The data in the pointer will replace the data in the buffer.  This is intended for use with the LuaJIT FFI or for passing to C libraries.",
+              key = "Buffer:mapData",
+              module = "lovr.graphics",
+              notes = "The pointer remains valid until the next call to `lovr.graphics.submit`, during which the data in the pointer will be uploaded to the buffer.\n\nThe initial contents of the pointer are undefined.\n\nSpecial care should be taken when writing data:\n\n- Reading data from the pointer will be very slow on some systems, and should be avoided.\n- It is better to write data to the pointer sequentially.  Random access may be slower.",
+              related = {
+                "Blob:getPointer",
+                "Buffer:getPointer"
+              },
+              variants = {
+                {
+                  arguments = {},
+                  returns = {
+                    {
+                      name = "pointer",
+                      type = "lightuserdata",
+                      description = "A pointer to the Buffer's memory."
+                    }
+                  }
+                }
+              }
+            },
+            {
+              name = "newReadback",
+              tag = "buffer-transfer",
+              summary = "Read back the contents of the Buffer asynchronously.",
+              description = "Creates and returns a new `Readback` that will download the data in the Buffer from VRAM. Once the readback is complete, `Readback:getData` returns the data as a table, or `Readback:getBlob` returns the data as a `Blob`.",
+              key = "Buffer:newReadback",
+              module = "lovr.graphics",
+              notes = "The offset and extent arguments must be a multiple of the Buffer's stride (unless it was created without a format).",
+              related = {
+                "Buffer:getData",
+                "Texture:newReadback"
+              },
+              variants = {
+                {
+                  arguments = {
+                    {
+                      name = "offset",
+                      type = "number",
+                      description = "A byte offset to read from.",
+                      default = "0"
+                    },
+                    {
+                      name = "extent",
+                      type = "number",
+                      description = "The number of bytes to read.  If nil, reads the rest of the buffer.",
+                      default = "nil"
+                    }
+                  },
+                  returns = {
+                    {
+                      name = "readback",
+                      type = "Readback",
+                      description = "A new Readback object."
+                    }
+                  }
+                }
+              }
+            },
             {
               name = "setData",
               tag = "buffer-transfer",
               summary = "Change the data in the Buffer.",
-              description = "Changes data in a temporary Buffer using a table or a Blob.  Permanent buffers can be changed using `Pass:copy`.",
+              description = "Copies data to the Buffer from either a table, `Blob`, or `Buffer`.",
               key = "Buffer:setData",
               module = "lovr.graphics",
               examples = {
                 {
-                  code = "function lovr.draw(pass)\n  buffer = lovr.graphics.getBuffer(3, 'floats')\n  buffer:setData({ { 1.0 }, { 2.0 }, { 3.0 } })\n  buffer:setData({ 1.0, 2.0, 3.0 })\n\n  buffer = lovr.graphics.getBuffer(5, { 'vec3', 'vec3', 'vec2' })\n  buffer:setData({ vec3(1, 2, 3), vec3(4, 5, 6), vec2(7, 8) })\n  buffer:setData({ { 1, 2, 3, 4, 5, 6, 7, 8 } })\n  buffer:setData({ 1, 2, 3, 4, 5, 6, 7, 8 })\n  buffer:setData({\n    { x1, y1, z1, nx1, ny1, nz1, u1, v1 },\n    { x2, y2, z2, vec3(nx, ny, nz) }\n  }, 1, 3, 2)\nend"
+                  description = "Various examples of copying different formats.",
+                  code = "function lovr.load()\n  buffer = lovr.graphics.newBuffer('int', 3)\n  buffer:setData({ 1, 2, 3 })\n\n  buffer = lovr.graphics.newBuffer('vec3', 2)\n  buffer:setData({ 1,2,3, 4,5,6 })\n  buffer:setData({ { 1, 2, 3 }, { 4, 5, 6 } })\n  buffer:setData({ vec3(1, 2, 3), vec3(4, 5, 6) })\n\n  -- When the Buffer's length is 1, wrapping in table is optional:\n  buffer = lovr.graphics.newBuffer('vec3')\n  buffer:setData(1, 2, 3)\n  buffer:setData(vec3(1, 2, 3))\n\n  -- Same for key-value structs\n  buffer = lovr.graphics.newBuffer({\n    { 'x', 'float' },\n    { 'y', 'float' }\n  })\n  buffer:setData({ x = 1, y = 2 })\n\n  -- Key/value formats\n  buffer = lovr.graphics.newBuffer({\n    { 'x', 'float' },\n    { 'y', 'float' }\n  }, 3)\n  buffer:setData({\n    { x = 1, y = 2 },\n    { x = 3, y = 4 },\n    { x = 5, y = 6 }\n  })\n  buffer:setData({ 1, 2, 3, 4, 5, 6 })\n  buffer:setData({ { 1, 2 }, { 3, 4 }, { 5, 6 } })\n\n  -- Nested formats\n  buffer = lovr.graphics.newBuffer({\n    { 'a', {\n      {'b', {\n        'c', 'float'\n      }}\n    }}\n  })\n  buffer:setData({ a = { b = { c = 42 } } })\n\n  -- Inner arrays\n  buffer = lovr.graphics.newBuffer({\n    { 'positions', 'vec3', 10 },\n    { 'sizes', 'float', 10 },\n    layout = 'std140'\n  })\n  local data = { positions = {}, sizes = {} }\n  for i = 1, buffer:getLength() do\n    data.positions[i] = vec3(i, i, i)\n    data.sizes[i] = i\n  end\n  buffer:setData(data)\nend"
                 }
               },
-              notes = "When using a table, the table can contain a nested table for each value in the Buffer, or it can be a flat list of field component values.  It is not possible to mix both nested tables and flat values.\n\nFor each item updated, components of each field in the item (according to the Buffer's format) are read from either the nested subtable or the table itself.  A single number can be used to update a field with a scalar type.  Multiple numbers or a `lovr.math` vector can be used to update a field with a vector or mat4 type.  Multiple numbers can be used to update mat2 and mat3 fields.  When updating normalized field types, components read from the table will be clamped to the normalized range ([0,1] or [-1,1]).  In the Buffer, each field is written at its byte offset according to the Buffer's format, and subsequent items are separated by the byte stride of the Buffer.  Any missing components for an updated field will be set to zero.",
+              notes = "One gotcha is that unspecified fields will be set to zero.  Here's an example:\n\n    buffer = lovr.graphics.newBuffer({{ 'x', 'int' }, { 'y', 'int' }})\n    buffer:setData({ x = 1, y = 1 }) -- set the data\n    buffer:setData({ x = 1 }) -- set the data, partially\n    -- buffer data is now {x=1, y=0}!\n\nThis doesn't apply to separate items in the buffer.  For example, if the Buffer's length is 2 and only the 1st item is set, the second item will remain undisturbed:\n\n    buffer = lovr.graphics.newBuffer({{ 'x', 'int' }, { 'y', 'int' }}, 2)\n    buffer:setData({{ x = 1, y = 1 }, { x = 2, y = 2 }}) -- set the data\n    buffer:setData({{ x = 1 }}) -- set one item, partially\n    -- buffer data is now {{x=1, y=0}, {x=2, y=2}}",
               variants = {
                 {
                   arguments = {
                     {
-                      name = "data",
+                      name = "table",
                       type = "table",
                       description = "A flat or nested table of items to copy to the Buffer (see notes for format)."
                     },
                     {
-                      name = "sourceIndex",
+                      name = "destinationIndex",
                       type = "number",
-                      description = "The index in the table to copy from.",
+                      description = "The index of the first value in the Buffer to update.",
                       default = "1"
                     },
                     {
-                      name = "destinationIndex",
+                      name = "sourceIndex",
                       type = "number",
-                      description = "The index of the first value in the Buffer to update.",
+                      description = "The index in the table to copy from.",
                       default = "1"
                     },
                     {
                       name = "count",
                       type = "number",
-                      description = "The number of values to update.  `nil` will copy as many items as possible, based on the lengths of the source and destination.",
+                      description = "The number of items to copy.  `nil` will copy as many items as possible, based on the lengths of the source and destination.",
                       default = "nil"
                     }
                   },
                   returns = {}
                 },
+                {
+                  description = "Copies a single field to a buffer with numbers (buffer length must be 1).",
+                  arguments = {
+                    {
+                      name = "...numbers",
+                      type = "number",
+                      description = "Numerical components to copy to the buffer."
+                    }
+                  },
+                  returns = {}
+                },
+                {
+                  description = "Copies a single vector to a buffer (buffer length must be 1).",
+                  arguments = {
+                    {
+                      name = "vector",
+                      type = "*",
+                      description = "Vector objects to copy to the buffer."
+                    }
+                  },
+                  returns = {}
+                },
                 {
                   arguments = {
                     {
@@ -13144,16 +13252,44 @@ return {
                       type = "Blob",
                       description = "The Blob to copy data from."
                     },
+                    {
+                      name = "destinationOffset",
+                      type = "number",
+                      description = "The byte offset to copy to.",
+                      default = "0"
+                    },
                     {
                       name = "sourceOffset",
                       type = "number",
-                      description = "A byte offset into the Blob to copy from.",
+                      description = "The byte offset to copy from.",
                       default = "0"
                     },
+                    {
+                      name = "size",
+                      type = "number",
+                      description = "The number of bytes to copy.  If nil, copies as many bytes as possible.",
+                      default = "nil"
+                    }
+                  },
+                  returns = {}
+                },
+                {
+                  arguments = {
+                    {
+                      name = "buffer",
+                      type = "Buffer",
+                      description = "The Buffer to copy data from."
+                    },
                     {
                       name = "destinationOffset",
                       type = "number",
-                      description = "A byte offset in the Buffer to copy to.",
+                      description = "The byte offset to copy to.",
+                      default = "0"
+                    },
+                    {
+                      name = "sourceOffset",
+                      type = "number",
+                      description = "The byte offset to copy from.",
                       default = "0"
                     },
                     {
@@ -21805,7 +21941,7 @@ return {
             {
               name = "getData",
               summary = "Get Readback's data as a table.",
-              description = "Returns the data from the Readback, as a table.",
+              description = "Returns the data from the Readback, as a table.  See `Buffer:getData` for the way the table is structured.",
               key = "Readback:getData",
               module = "lovr.graphics",
               notes = "This returns `nil` for readbacks of `Texture` objects.",
@@ -21820,7 +21956,7 @@ return {
                     {
                       name = "data",
                       type = "table",
-                      description = "A flat table of numbers containing the values that were read back."
+                      description = "A table containing the data that was read back."
                     }
                   }
                 }

+ 79 - 0
api/lovr/graphics/Buffer/getData.lua

@@ -0,0 +1,79 @@
+return {
+  tag = 'buffer-transfer',
+  summary = 'Get the data in the Buffer.',
+  description = [[
+    Downloads the Buffer's data from VRAM and returns it as a table.  This function is very very
+    slow because it stalls the CPU until the data is finished downloading, so it should only be used
+    for debugging or non-interactive scripts.  `Buffer:newReadback` is an alternative that returns a
+    `Readback` object, which will not block the CPU.
+  ]],
+  arguments = {
+    index = {
+      type = 'number',
+      default = '1',
+      description = 'The index of the first item to read.'
+    },
+    count = {
+      type = 'number',
+      default = 'nil',
+      description = 'The number of items to read.  If nil, reads the remainder of the buffer.'
+    }
+  },
+  returns = {
+    t = {
+      type = 'table',
+      description = 'The table with the Buffer\'s data.'
+    }
+  },
+  variants = {
+    arguments = {},
+    returns = { 't' }
+  },
+  notes = [[
+    The length of the table will equal the number of items read.  Here are some examples of how the
+    table is formatted:
+
+        buffer = lovr.graphics.newBuffer('int', { 7 })
+        buffer:getData() --> returns { 7 }
+
+        buffer = lovr.graphics.newBuffer('vec3', { 7, 8, 9 })
+        buffer:getData() --> returns {{ 7, 8, 9 }}
+
+        buffer = lovr.graphics.newBuffer('int', { 1, 2, 3 })
+        buffer:getData() --> returns { 1, 2, 3 }
+
+        buffer = lovr.graphics.newBuffer({ 'vec2', 'vec2' }, {
+          vec2(1,2), vec2(3,4),
+          vec2(5,6), vec2(7,8)
+        })
+        buffer:getData() --> returns { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } }
+
+        buffer = lovr.graphics.newBuffer({
+          { 'a', 'float' },
+          { 'b', 'float' }
+        }, { a = 1, b = 2 })
+        buffer:getData() --> returns { { 1, 2 } }
+
+        buffer = lovr.graphics.newBuffer({
+          { 'x', 'int', 3 }
+        }, { x = { 1, 2, 3 } })
+        buffer:getData() --> returns { { x = { 1, 2, 3 } } }
+
+        buffer = lovr.graphics.newBuffer({
+          { 'lights', {
+            { 'pos', 'vec3' },
+            { 'size', 'float' },
+          }, 10}
+        }, data)
+        buffer:getData() --> returns { { lights = { { pos = ..., size = ... }, ... } } }
+
+    In summary, each individual item is wrapped in a table, except if the format is a single number.
+    If the format has nested types or arrays then the tables will be key-value, otherwise they will
+    use numeric keys.
+  ]],
+  related = {
+    'Buffer:newReadback',
+    'Buffer:mapData',
+    'Readback:getData'
+  }
+}

+ 18 - 8
api/lovr/graphics/Buffer/getPointer.lua

@@ -1,12 +1,24 @@
 return {
+  deprecated = true,
   tag = 'buffer-transfer',
-  summary = 'Get a raw pointer to the Buffer memory.',
+  summary = 'Get a writable pointer to the Buffer\'s memory.',
   description = [[
-    Returns a raw pointer to the Buffer's memory as a lightuserdata, intended for use with the
+    Returns a pointer to GPU memory and schedules a copy from this pointer to the buffer's data. The
+    data in the pointer will replace the data in the buffer.  This is intended for use with the
     LuaJIT FFI or for passing to C libraries.
-
   ]],
-  arguments = {},
+  arguments = {
+    offset = {
+      type = 'number',
+      default = '0',
+      description = 'A byte offset in the buffer to write to.'
+    },
+    extent = {
+      type = 'number',
+      default = 'nil',
+      description = 'The number of bytes to replace.  If nil, writes to the rest of the buffer.'
+    }
+  },
   returns = {
     pointer = {
       type = 'lightuserdata',
@@ -25,15 +37,13 @@ return {
 
     The initial contents of the pointer are undefined.
 
-    Currently the pointer addresses a range equal to the size of the Buffer, and so this overwrites
-    the entire contents of the Buffer.
-
     Special care should be taken when writing data:
 
     - Reading data from the pointer will be very slow on some systems, and should be avoided.
     - It is better to write data to the pointer sequentially.  Random access may be slower.
   ]],
   related = {
-    'Blob:getPointer'
+    'Blob:getPointer',
+    'Buffer:mapData'
   }
 }

+ 48 - 0
api/lovr/graphics/Buffer/mapData.lua

@@ -0,0 +1,48 @@
+return {
+  tag = 'buffer-transfer',
+  summary = 'Get a writable pointer to the Buffer\'s memory.',
+  description = [[
+    Returns a pointer to GPU memory and schedules a copy from this pointer to the buffer's data. The
+    data in the pointer will replace the data in the buffer.  This is intended for use with the
+    LuaJIT FFI or for passing to C libraries.
+  ]],
+  arguments = {
+    offset = {
+      type = 'number',
+      default = '0',
+      description = 'A byte offset in the buffer to write to.'
+    },
+    extent = {
+      type = 'number',
+      default = 'nil',
+      description = 'The number of bytes to replace.  If nil, writes to the rest of the buffer.'
+    }
+  },
+  returns = {
+    pointer = {
+      type = 'lightuserdata',
+      description = 'A pointer to the Buffer\'s memory.'
+    }
+  },
+  variants = {
+    {
+      arguments = {},
+      returns = { 'pointer' }
+    }
+  },
+  notes = [[
+    The pointer remains valid until the next call to `lovr.graphics.submit`, during which the data
+    in the pointer will be uploaded to the buffer.
+
+    The initial contents of the pointer are undefined.
+
+    Special care should be taken when writing data:
+
+    - Reading data from the pointer will be very slow on some systems, and should be avoided.
+    - It is better to write data to the pointer sequentially.  Random access may be slower.
+  ]],
+  related = {
+    'Blob:getPointer',
+    'Buffer:getPointer'
+  }
+}

+ 41 - 0
api/lovr/graphics/Buffer/newReadback.lua

@@ -0,0 +1,41 @@
+return {
+  tag = 'buffer-transfer',
+  summary = 'Read back the contents of the Buffer asynchronously.',
+  description = [[
+    Creates and returns a new `Readback` that will download the data in the Buffer from VRAM. Once
+    the readback is complete, `Readback:getData` returns the data as a table, or `Readback:getBlob`
+    returns the data as a `Blob`.
+  ]],
+  arguments = {
+    offset = {
+      type = 'number',
+      default = '0',
+      description = 'A byte offset to read from.'
+    },
+    extent = {
+      type = 'number',
+      default = 'nil',
+      description = 'The number of bytes to read.  If nil, reads the rest of the buffer.'
+    }
+  },
+  returns = {
+    readback = {
+      type = 'Readback',
+      description = 'A new Readback object.'
+    }
+  },
+  variants = {
+    {
+      arguments = { 'offset', 'extent' },
+      returns = { 'readback' }
+    }
+  },
+  notes = [[
+    The offset and extent arguments must be a multiple of the Buffer's stride (unless it was created
+    without a format).
+  ]],
+  related = {
+    'Buffer:getData',
+    'Texture:newReadback'
+  }
+}

+ 111 - 40
api/lovr/graphics/Buffer/setData.lua

@@ -1,46 +1,55 @@
 return {
   tag = 'buffer-transfer',
   summary = 'Change the data in the Buffer.',
-  description = [[
-    Changes data in a temporary Buffer using a table or a Blob.  Permanent buffers can be changed
-    using `Pass:copy`.
-  ]],
+  description = 'Copies data to the Buffer from either a table, `Blob`, or `Buffer`.',
   arguments = {
-    data = {
+    table = {
       type = 'table',
       description = 'A flat or nested table of items to copy to the Buffer (see notes for format).'
     },
-    sourceIndex = {
+    destinationIndex = {
       type = 'number',
       default = '1',
-      description = 'The index in the table to copy from.'
+      description = 'The index of the first value in the Buffer to update.'
     },
-    destinationIndex = {
+    sourceIndex = {
       type = 'number',
       default = '1',
-      description = 'The index of the first value in the Buffer to update.'
+      description = 'The index in the table to copy from.'
     },
     count = {
       type = 'number',
       default = 'nil',
       description = [[
-        The number of values to update.  `nil` will copy as many items as possible, based on the
+        The number of items to copy.  `nil` will copy as many items as possible, based on the
         lengths of the source and destination.
       ]]
     },
+    ['...numbers'] = {
+      type = 'number',
+      description = 'Numerical components to copy to the buffer.'
+    },
+    vector = {
+      type = '*',
+      description = 'Vector objects to copy to the buffer.'
+    },
     blob = {
       type = 'Blob',
       description = 'The Blob to copy data from.'
     },
+    buffer = {
+      type = 'Buffer',
+      description = 'The Buffer to copy data from.'
+    },
     sourceOffset = {
       type = 'number',
       default = '0',
-      description = 'A byte offset into the Blob to copy from.'
+      description = 'The byte offset to copy from.'
     },
     destinationOffset = {
       type = 'number',
       default = '0',
-      description = 'A byte offset in the Buffer to copy to.'
+      description = 'The byte offset to copy to.'
     },
     size = {
       type = 'number',
@@ -51,42 +60,104 @@ return {
   returns = {},
   variants = {
     {
-      arguments = { 'data', 'sourceIndex', 'destinationIndex', 'count' },
+      arguments = { 'table', 'destinationIndex', 'sourceIndex', 'count' },
       returns = {}
     },
     {
-      arguments = { 'blob', 'sourceOffset', 'destinationOffset', 'size' },
+      description = 'Copies a single field to a buffer with numbers (buffer length must be 1).',
+      arguments = { '...numbers' },
+      returns = {}
+    },
+    {
+      description = 'Copies a single vector to a buffer (buffer length must be 1).',
+      arguments = { 'vector' },
+      returns = {}
+    },
+    {
+      arguments = { 'blob', 'destinationOffset', 'sourceOffset', 'size' },
+      returns = {}
+    },
+    {
+      arguments = { 'buffer', 'destinationOffset', 'sourceOffset', 'size' },
       returns = {}
     }
   },
   notes = [[
-    When using a table, the table can contain a nested table for each value in the Buffer, or it can
-    be a flat list of field component values.  It is not possible to mix both nested tables and flat
-    values.
+    One gotcha is that unspecified fields will be set to zero.  Here's an example:
 
-    For each item updated, components of each field in the item (according to the Buffer's format)
-    are read from either the nested subtable or the table itself.  A single number can be used to
-    update a field with a scalar type.  Multiple numbers or a `lovr.math` vector can be used to
-    update a field with a vector or mat4 type.  Multiple numbers can be used to update mat2 and mat3
-    fields.  When updating normalized field types, components read from the table will be clamped to
-    the normalized range ([0,1] or [-1,1]).  In the Buffer, each field is written at its byte offset
-    according to the Buffer's format, and subsequent items are separated by the byte stride of the
-    Buffer.  Any missing components for an updated field will be set to zero.
+        buffer = lovr.graphics.newBuffer({{ 'x', 'int' }, { 'y', 'int' }})
+        buffer:setData({ x = 1, y = 1 }) -- set the data
+        buffer:setData({ x = 1 }) -- set the data, partially
+        -- buffer data is now {x=1, y=0}!
+
+    This doesn't apply to separate items in the buffer.  For example, if the Buffer's length is 2
+    and only the 1st item is set, the second item will remain undisturbed:
+
+        buffer = lovr.graphics.newBuffer({{ 'x', 'int' }, { 'y', 'int' }}, 2)
+        buffer:setData({{ x = 1, y = 1 }, { x = 2, y = 2 }}) -- set the data
+        buffer:setData({{ x = 1 }}) -- set one item, partially
+        -- buffer data is now {{x=1, y=0}, {x=2, y=2}}
   ]],
-  example = [[
-    function lovr.draw(pass)
-      buffer = lovr.graphics.getBuffer(3, 'floats')
-      buffer:setData({ { 1.0 }, { 2.0 }, { 3.0 } })
-      buffer:setData({ 1.0, 2.0, 3.0 })
+  example = {
+    description = 'Various examples of copying different formats.',
+    code = [[
+      function lovr.load()
+        buffer = lovr.graphics.newBuffer('int', 3)
+        buffer:setData({ 1, 2, 3 })
+
+        buffer = lovr.graphics.newBuffer('vec3', 2)
+        buffer:setData({ 1,2,3, 4,5,6 })
+        buffer:setData({ { 1, 2, 3 }, { 4, 5, 6 } })
+        buffer:setData({ vec3(1, 2, 3), vec3(4, 5, 6) })
+
+        -- When the Buffer's length is 1, wrapping in table is optional:
+        buffer = lovr.graphics.newBuffer('vec3')
+        buffer:setData(1, 2, 3)
+        buffer:setData(vec3(1, 2, 3))
+
+        -- Same for key-value structs
+        buffer = lovr.graphics.newBuffer({
+          { 'x', 'float' },
+          { 'y', 'float' }
+        })
+        buffer:setData({ x = 1, y = 2 })
+
+        -- Key/value formats
+        buffer = lovr.graphics.newBuffer({
+          { 'x', 'float' },
+          { 'y', 'float' }
+        }, 3)
+        buffer:setData({
+          { x = 1, y = 2 },
+          { x = 3, y = 4 },
+          { x = 5, y = 6 }
+        })
+        buffer:setData({ 1, 2, 3, 4, 5, 6 })
+        buffer:setData({ { 1, 2 }, { 3, 4 }, { 5, 6 } })
+
+        -- Nested formats
+        buffer = lovr.graphics.newBuffer({
+          { 'a', {
+            {'b', {
+              'c', 'float'
+            }}
+          }}
+        })
+        buffer:setData({ a = { b = { c = 42 } } })
 
-      buffer = lovr.graphics.getBuffer(5, { 'vec3', 'vec3', 'vec2' })
-      buffer:setData({ vec3(1, 2, 3), vec3(4, 5, 6), vec2(7, 8) })
-      buffer:setData({ { 1, 2, 3, 4, 5, 6, 7, 8 } })
-      buffer:setData({ 1, 2, 3, 4, 5, 6, 7, 8 })
-      buffer:setData({
-        { x1, y1, z1, nx1, ny1, nz1, u1, v1 },
-        { x2, y2, z2, vec3(nx, ny, nz) }
-      }, 1, 3, 2)
-    end
-  ]]
+        -- Inner arrays
+        buffer = lovr.graphics.newBuffer({
+          { 'positions', 'vec3', 10 },
+          { 'sizes', 'float', 10 },
+          layout = 'std140'
+        })
+        local data = { positions = {}, sizes = {} }
+        for i = 1, buffer:getLength() do
+          data.positions[i] = vec3(i, i, i)
+          data.sizes[i] = i
+        end
+        buffer:setData(data)
+      end
+    ]]
+  }
 }

+ 5 - 2
api/lovr/graphics/Readback/getData.lua

@@ -1,11 +1,14 @@
 return {
   summary = 'Get Readback\'s data as a table.',
-  description = 'Returns the data from the Readback, as a table.',
+  description = [[
+    Returns the data from the Readback, as a table.  See `Buffer:getData` for the way the table is
+    structured.
+  ]],
   arguments = {},
   returns = {
     data = {
       type = 'table',
-      description = 'A flat table of numbers containing the values that were read back.'
+      description = 'A table containing the data that was read back.'
     }
   },
   variants = {