+ # ---
+ # Associate each point to its sprites
+
+ private var sprites: nullable Array[Sprite] = null
+
+ private fun sprites_add(sprite: Sprite)
+ do
+ var sprites = sprites
+ if sprites == null then
+ sprites = new Array[Sprite]
+ self.sprites = sprites
+ end
+ sprites.add sprite
+ end
+
+ private fun sprites_remove(sprite: Sprite)
+ do
+ var sprites = sprites
+ assert sprites != null
+ sprites.remove sprite
+ end
+
+ # ---
+ # Notify `sprites` on attribute modification
+
+ private fun needs_update
+ do
+ var sprites = sprites
+ if sprites != null then for s in sprites do s.needs_update
+ end
+
+ 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
+
+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