+ if buffer_array == -1 then prepare
+
+ assert buffer_array > 0 and buffer_element > 0 else
+ print_error "Internal error: {self} was destroyed"
+ end
+
+ # Setup
+ glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
+ glBindBuffer(gl_ELEMENT_ARRAY_BUFFER, buffer_element)
+
+ # Resize GPU buffers?
+ if sprites.capacity > buffer_capacity then
+ # Try to defragment first
+ var moved = sprites.defragment
+
+ if sprites.capacity > buffer_capacity then
+ # Defragmentation wasn't enough, grow
+ resize
+
+ # We must update everything
+ for s in sprites.items do if s != null then sprites_to_update.add s
+ else
+ # Just update the moved sprites
+ for s in moved do sprites_to_update.add s
+ end
+ else if sprites.available.not_empty then
+ # Defragment a bit anyway
+ # TODO defrag only when there's time left on a frame
+ var moved = sprites.defragment(1)
+ for s in moved do sprites_to_update.add s
+ end
+
+ # Update GPU sprites data
+ if sprites_to_update.not_empty then
+ app.perf_clock_sprites.lapse
+
+ for sprite in sprites_to_update do update_sprite(sprite)
+ sprites_to_update.clear
+
+ sys.perfs["gamnit flat gpu update"].add app.perf_clock_sprites.lapse
+ end
+
+ # Update uniforms specific to this context
+ var texture = texture
+ app.simple_2d_program.use_texture.uniform texture != null
+ if texture != null then
+ glActiveTexture gl_TEXTURE0
+ glBindTexture(gl_TEXTURE_2D, texture.gl_texture)
+ app.simple_2d_program.texture.uniform 0
+ end
+ var gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ # Configure attributes, in order:
+ # vec4 translation, vec4 color, float scale, vec4 coord, vec2 tex_coord, vec4 rotation_row*
+ var offset = 0
+ var p = app.simple_2d_program
+ var sizeof_gl_float = 4 # sizeof(GL_FLOAT)
+
+ var size = 4 # Number of floats
+ glEnableVertexAttribArray p.translation.location
+ glVertexAttribPointeri(p.translation.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+ offset += size * sizeof_gl_float
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ size = 4
+ glEnableVertexAttribArray p.color.location
+ glVertexAttribPointeri(p.color.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+ offset += size * sizeof_gl_float
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ size = 1
+ glEnableVertexAttribArray p.scale.location
+ glVertexAttribPointeri(p.scale.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+ offset += size * sizeof_gl_float
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ size = 4
+ glEnableVertexAttribArray p.coord.location
+ glVertexAttribPointeri(p.coord.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+ offset += size * sizeof_gl_float
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ size = 2
+ glEnableVertexAttribArray p.tex_coord.location
+ glVertexAttribPointeri(p.tex_coord.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+ offset += size * sizeof_gl_float
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ size = 4
+ for r in [p.rotation_row0, p.rotation_row1, p.rotation_row2, p.rotation_row3] do
+ if r.is_active then
+ glEnableVertexAttribArray r.location
+ glVertexAttribPointeri(r.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+ end
+ offset += size * sizeof_gl_float
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+ end
+
+ # Actual draw
+ for s in sprites.starts, e in sprites.ends do
+ var l = e-s
+ glDrawElementsi(gl_TRIANGLES, l*indices_per_sprite, gl_UNSIGNED_SHORT, 2*s*indices_per_sprite)
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+ end
+
+ # Take down
+ for attr in [p.translation, p.color, p.scale, p.coord, p.tex_coord,
+ p.rotation_row0, p.rotation_row1, p.rotation_row2, p.rotation_row3: Attribute] do
+ if not attr.is_active then continue
+ glDisableVertexAttribArray(attr.location)
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+ end
+
+ glBindBuffer(gl_ARRAY_BUFFER, 0)
+ glBindBuffer(gl_ELEMENT_ARRAY_BUFFER, 0)
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+ end
+end
+
+# Representation of sprite data on the GPU
+#
+# The main purpose of this class is to optimize the use of contiguous
+# space in GPU memory. Each contiguous memory block can be drawn in a
+# single call. The starts index of each block is kept by `starts,
+# and the end + 1 by `ends`.
+#
+# The data can be compressed by a call to `defragment`.
+#
+# ~~~
+# intrude import gamnit::flat
+#
+# var array = new GroupedArray[String]
+# assert array.to_s == ""
+#
+# array.add "a"
+# array.add "b"
+# array.add "c"
+# array.add "d"
+# array.add "e"
+# array.add "f"
+# assert array.to_s == "[a,b,c,d,e,f]"
+# assert array.capacity == 6
+#
+# array.remove "a"
+# assert array.to_s == "[b,c,d,e,f]"
+#
+# array.remove "b"
+# assert array.to_s == "[c,d,e,f]"
+#
+# array.remove "f"
+# assert array.to_s == "[c,d,e]"
+#
+# array.remove "d"
+# assert array.to_s == "[c][e]"
+#
+# array.add "A"
+# assert array.to_s == "[A][c][e]"
+#
+# array.add "B"
+# assert array.to_s == "[A,B,c][e]"
+#
+# array.remove "e"
+# assert array.to_s == "[A,B,c]"
+#
+# array.add "D"
+# assert array.to_s == "[A,B,c,D]"
+#
+# array.add "E"
+# assert array.to_s == "[A,B,c,D,E]"
+# assert array.capacity == 6
+# assert array.length == 5
+#
+# array.remove "A"
+# array.remove "B"
+# array.remove "c"
+# array.remove "D"
+# array.remove "E"
+# assert array.to_s == ""
+#
+# array.add "a"
+# assert array.to_s == "[a]"
+# ~~~
+private class GroupedArray[E]
+
+ # Memory with actual objects, and null in empty slots
+ var items = new Array[nullable E]
+
+ # Number of items in the array
+ var length = 0
+
+ # Number of item slots in the array
+ fun capacity: Int do return items.length
+
+ # Index of `item`
+ fun index_of(item: E): Int do return items.index_of(item)
+
+ # List of available slots
+ var available = new MinHeap[Int].default
+
+ # Start index of filled chunks
+ var starts = new List[Int]
+
+ # Index of the spots after filled chunks
+ var ends = new List[Int]
+
+ # Add `item` to the first available slot
+ fun add(item: E)
+ do
+ length += 1
+
+ if available.not_empty then
+ # starts & ends can't be empty
+
+ var i = available.take
+ items[i] = item
+
+ if i == starts.first - 1 then
+ # slot 0 free, 1 taken
+ starts.first -= 1
+ else if i == 0 then
+ # slot 0 and more free
+ starts.unshift 0
+ ends.unshift 1
+ else if starts.length >= 2 and ends.first + 1 == starts[1] then
+ # merge 2 chunks
+ ends.remove_at 0
+ starts.remove_at 1
+ else
+ # at end of first chunk
+ ends.first += 1
+ end
+ return
+ end
+
+ items.add item
+ if ends.is_empty then
+ starts.add 0
+ ends.add 1
+ else ends.last += 1
+ end
+
+ # Remove the first instance of `item`
+ fun remove(item: E)
+ do
+ var i = items.index_of(item)
+ assert i != -1
+ length -= 1
+ items[i] = null
+
+ var ii = 0
+ for s in starts, e in ends do
+ if s <= i and i < e then
+ if s == e-1 then
+ # single item chunk
+ starts.remove_at ii
+ ends.remove_at ii
+
+ if starts.is_empty then
+ items.clear
+ available.clear
+ return
+ end
+ else if e-1 == i then
+ # last item of chunk
+ ends[ii] -= 1
+
+ else if s == i then
+ # first item of chunk
+ starts[ii] += 1
+ else
+ # break up chunk
+ ends.insert(ends[ii], ii+1)
+ ends[ii] = i
+ starts.insert(i+1, ii+1)
+ end
+
+ available.add i
+ return
+ end
+ ii += 1
+ end
+
+ abort
+ end
+
+ # Defragment and compress everything into a single chunks beginning at 0
+ #
+ # Returns the elements that moved as a list.
+ #
+ # ~~~
+ # intrude import gamnit::flat
+ #
+ # var array = new GroupedArray[String]
+ # array.add "a"
+ # array.add "b"
+ # array.add "c"
+ # array.add "d"
+ # array.remove "c"
+ # array.remove "a"
+ # assert array.to_s == "[b][d]"
+ #
+ # var moved = array.defragment
+ # assert moved.to_s == "[d]"
+ # assert array.to_s == "[d,b]"
+ # assert array.length == 2
+ # assert array.capacity == 2
+ #
+ # array.add "e"
+ # array.add "f"
+ # assert array.to_s == "[d,b,e,f]"
+ # ~~~
+ fun defragment(max: nullable Int): Array[E]
+ do
+ app.perf_clock_sprites.lapse
+ max = max or else length
+
+ var moved = new Array[E]
+ while max > 0 and (starts.length > 1 or starts.first != 0) do
+ var i = ends.last - 1
+ var e = items[i]
+ remove e
+ add e
+ moved.add e
+ max -= 1
+ end
+
+ if starts.length == 1 and starts.first == 0 then
+ for i in [length..capacity[ do items.pop
+ available.clear
+ end
+
+ sys.perfs["gamnit flat gpu defrag"].add app.perf_clock_sprites.lapse
+ return moved