+
+ redef fun z=(v)
+ do
+ if isset _z and v != z then needs_update
+ super
+ end
+end
+
+redef class OffsetPoint3d
+ redef fun x=(v)
+ do
+ if isset _x and v != x then needs_update
+ super
+ end
+
+ redef fun y=(v)
+ do
+ if isset _y and v != y then needs_update
+ super
+ end
+
+ redef fun z=(v)
+ do
+ if isset _z and v != z then needs_update
+ super
+ end
+end
+
+# Set of sprites sorting them into different `SpriteContext`
+private class SpriteSet
+ super HashSet[Sprite]
+
+ # Map texture then static vs dynamic to a `SpriteContext`
+ var contexts_map = new HashMap2[RootTexture, Bool, SpriteContext]
+
+ # Contexts in `contexts_map`
+ var contexts_items = new Array[SpriteContext]
+
+ # Sprites needing resorting in `contexts_map`
+ var sprites_to_remap = new Array[Sprite]
+
+ # Add a sprite to the appropriate context
+ fun map_sprite(sprite: Sprite)
+ do
+ assert sprite.context == null else print_error "Sprite {sprite} belongs to another SpriteSet"
+
+ var texture = sprite.texture.root
+ var context = contexts_map[texture, sprite.static]
+
+ if context == null then
+ var usage = if sprite.static then gl_STATIC_DRAW else gl_DYNAMIC_DRAW
+ context = new SpriteContext(texture, usage)
+
+ contexts_map[texture, sprite.static] = context
+ contexts_items.add context
+ end
+
+ context.sprites.add sprite
+ context.sprites_to_update.add sprite
+
+ sprite.context = context
+ sprite.sprite_set = self
+ end
+
+ # Remove a sprite from its context
+ fun unmap_sprite(sprite: Sprite)
+ do
+ var context = sprite.context
+ assert context != null
+ context.sprites.remove sprite
+
+ sprite.context = null
+ sprite.sprite_set = null
+ end
+
+ # Draw all sprites by all contexts
+ fun draw
+ do
+ for sprite in sprites_to_remap do
+ unmap_sprite sprite
+ map_sprite sprite
+ end
+ sprites_to_remap.clear
+
+ for context in contexts_items do context.draw
+ end
+
+ redef fun add(e)
+ do
+ if contexts_items.has(e.context) then return
+ map_sprite e
+ super
+ end
+
+ redef fun remove(e)
+ do
+ super
+ if e isa Sprite then unmap_sprite e
+ end
+
+ redef fun remove_all(e)
+ do
+ if not has(e) then return
+ remove e
+ end
+
+ redef fun clear
+ do
+ for sprite in self do
+ sprite.context = null
+ sprite.sprite_set = null
+ end
+ super
+ for c in contexts_items do c.destroy
+ contexts_map.clear
+ contexts_items.clear
+ end
+end
+
+# Context for calls to `glDrawElements`
+#
+# Each context has only one `texture` and `usage`, but many sprites.
+private class SpriteContext
+
+ # ---
+ # Context config and state
+
+ # Only root texture drawn by this context
+ var texture: nullable RootTexture
+
+ # OpenGL ES usage of `buffer_array` and `buffer_element`
+ var usage: GLBufferUsage
+
+ # Sprites drawn by this context
+ var sprites = new GroupedArray[Sprite]
+
+ # Sprites to update since last `draw`
+ var sprites_to_update = new Set[Sprite]
+
+ # Sprites that have been update and for which `needs_update` can be set to false
+ var updated_sprites = new Array[Sprite]
+
+ # Buffer size to preallocate at `resize`, multiplied by `sprites.length`
+ #
+ # Require: `resize_ratio >= 1.0`
+ var resize_ratio = 1.2
+
+ # ---
+ # OpenGL ES data
+
+ # OpenGL ES buffer name for vertex data
+ var buffer_array: Int = -1
+
+ # OpenGL ES buffer name for indices
+ var buffer_element: Int = -1
+
+ # Current capacity, in sprites, of `buffer_array` and `buffer_element`
+ var buffer_capacity = 0
+
+ # C buffers used to pass the data of a single sprite
+ var local_data_buffer = new GLfloatArray(float_per_vertex*4) is lazy
+ var local_indices_buffer = new CUInt16Array(indices_per_sprite) is lazy
+
+ # ---
+ # Constants
+
+ # Number of GL_FLOAT per vertex of `Simple2dProgram`
+ var float_per_vertex: Int is lazy do
+ # vec4 translation, vec4 color, vec4 coord,
+ # float scale, vec2 tex_coord, vec4 rotation_row*
+ return 4 + 4 + 4 +
+ 1 + 2 + 4*4
+ end
+
+ # Number of bytes per vertex of `Simple2dProgram`
+ var bytes_per_vertex: Int is lazy do
+ var fs = 4 # sizeof(GL_FLOAT)
+ return fs * float_per_vertex
+ end
+
+ # Number of bytes per sprite
+ var bytes_per_sprite: Int is lazy do return bytes_per_vertex * 4
+
+ # Number of vertex indices per sprite draw call (2 triangles)
+ var indices_per_sprite = 6
+
+ # ---
+ # Main services
+
+ # Allocate `buffer_array` and `buffer_element`
+ fun prepare
+ do
+ var bufs = glGenBuffers(2)
+ buffer_array = bufs[0]
+ buffer_element = bufs[1]
+
+ var gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+ end
+
+ # Destroy `buffer_array` and `buffer_element`
+ fun destroy
+ do
+ glDeleteBuffers([buffer_array, buffer_element])
+ var gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ buffer_array = -1
+ buffer_element = -1
+ end
+
+ # Resize `buffer_array` and `buffer_element` to fit all `sprites` (and more)
+ fun resize
+ do
+ app.perf_clock_sprites.lapse
+
+ # Allocate a bit more space
+ var capacity = (sprites.capacity.to_f * resize_ratio).to_i
+
+ var array_bytes = capacity * bytes_per_sprite
+ glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
+ assert glIsBuffer(buffer_array)
+ glBufferData(gl_ARRAY_BUFFER, array_bytes, new Pointer.nul, usage)
+ var gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ # GL_TRIANGLES 6 vertices * sprite
+ var n_indices = capacity * indices_per_sprite
+ var ius = 2 # sizeof(GL_UNSIGNED_SHORT)
+ var element_bytes = n_indices * ius
+ glBindBuffer(gl_ELEMENT_ARRAY_BUFFER, buffer_element)
+ assert glIsBuffer(buffer_element)
+ glBufferData(gl_ELEMENT_ARRAY_BUFFER, element_bytes, new Pointer.nul, usage)
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ buffer_capacity = capacity
+
+ sys.perfs["gamnit flat gpu resize"].add app.perf_clock_sprites.lapse
+ end
+
+ # Update GPU data of `sprite`
+ fun update_sprite(sprite: Sprite)
+ do
+ var sprite_index = sprites.index_of(sprite)
+ if sprite_index == -1 then return
+
+ # Vertices data
+
+ var data = local_data_buffer
+ var o = 0
+ for v in [0..4[ do
+ # vec4 translation
+ data[o+ 0] = sprite.center.x
+ data[o+ 1] = sprite.center.y
+ data[o+ 2] = sprite.center.z
+ data[o+ 3] = 0.0
+
+ # vec4 color
+ data[o+ 4] = sprite.tint[0]
+ data[o+ 5] = sprite.tint[1]
+ data[o+ 6] = sprite.tint[2]
+ data[o+ 7] = sprite.tint[3]
+
+ # float scale
+ data[o+ 8] = sprite.scale
+
+ # vec4 coord
+ data[o+ 9] = sprite.texture.vertices[v*3+0]
+ data[o+10] = sprite.texture.vertices[v*3+1]
+ data[o+11] = sprite.texture.vertices[v*3+2]
+ data[o+12] = 0.0
+
+ # vec2 tex_coord
+ var texture = texture
+ if texture != null then
+ var tc = if sprite.invert_x then
+ sprite.texture.texture_coords_invert_x
+ else sprite.texture.texture_coords
+ data[o+13] = tc[v*2+0]
+ data[o+14] = tc[v*2+1]
+ end
+
+ # mat4 rotation
+ var rot
+ if sprite.rotation == 0.0 then
+ # Cache the matrix at no rotation
+ rot = once new Matrix.identity(4)
+ else
+ rot = new Matrix.rotation(sprite.rotation, 0.0, 0.0, 1.0)
+ end
+ data.fill_from_matrix(rot, o+15)
+
+ o += float_per_vertex
+ end
+
+ glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
+ glBufferSubData(gl_ARRAY_BUFFER, sprite_index*bytes_per_sprite, bytes_per_sprite, data.native_array)
+
+ var gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+
+ # Element / indices
+ #
+ # 0--1
+ # | /|
+ # |/ |
+ # 2--3
+
+ var indices = local_indices_buffer
+ var io = sprite_index*4
+ indices[0] = io+0
+ indices[1] = io+2
+ indices[2] = io+1
+ indices[3] = io+1
+ indices[4] = io+2
+ indices[5] = io+3
+
+ glBindBuffer(gl_ELEMENT_ARRAY_BUFFER, buffer_element)
+ glBufferSubData(gl_ELEMENT_ARRAY_BUFFER, sprite_index*6*2, 6*2, indices.native_array)
+
+ gl_error = glGetError
+ assert gl_error == gl_NO_ERROR else print_error gl_error
+ end
+
+ # Draw all `sprites`
+ #
+ # Call `resize` and `update_sprite` as needed before actual draw operation.
+ #
+ # Require: `app.simple_2d_program` and `mvp` must be bound on the GPU
+ fun draw
+ do
+ 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
+ end
+
+ redef fun to_s
+ do
+ var ss = new Array[String]
+ for s in starts, e in ends do
+ ss.add "["
+ for i in [s..e[ do
+ var item: nullable Object = items[i]
+ if item == null then item = "null"
+ ss.add item.to_s
+ if i != e-1 then ss.add ","
+ end
+ ss.add "]"
+ end
+ return ss.join
+ end
+end
+
+redef class GLfloatArray
+ private fun fill_from_matrix(matrix: Matrix, dst_offset: nullable Int)
+ do
+ dst_offset = dst_offset or else 0
+ var mat_len = matrix.width*matrix.height
+ assert length >= mat_len + dst_offset
+ native_array.fill_from_matrix_native(matrix.items, dst_offset, mat_len)
+ end
+end
+
+redef class NativeGLfloatArray
+ private fun fill_from_matrix_native(matrix: matrix::NativeDoubleArray, dst_offset, len: Int) `{
+ int i;
+ for (i = 0; i < len; i ++)
+ self[i+dst_offset] = (GLfloat)matrix[i];
+ `}