gamnit: cache the last sprite added to `sprites_to_update`
[nit.git] / lib / gamnit / flat.nit
index aef21d8..85c702c 100644 (file)
@@ -41,16 +41,59 @@ import more_collections
 import performance_analysis
 
 import gamnit
-import gamnit::cameras_cache
+intrude import gamnit::cameras
+intrude import gamnit::cameras_cache
 import gamnit::dynamic_resolution
 import gamnit::limit_fps
 import gamnit::camera_control
 
-# Draw a `texture` at `center`
+# Visible 2D entity in the game world or UI
 #
-# An instance of `Sprite` can only belong to a single `SpriteSet` at
-# a time. The on screen position depends on the camera associated
+# Similar to `gamnit::Actor` which is in 3D.
+#
+# Each sprite associates a `texture` to the position `center`.
+# The appearance is modified by `rotation`, `invert_x`,
+# `scale`, `red`, `green`, `blue` and `alpha`.
+# These values can be changed at any time and will trigger an update
+# of the data on the GPU side, having a small performance cost.
+#
+# For a sprite to be visible, it must be added to either the world `sprites`
+# or the `ui_sprites`.
+# However, an instance of `Sprite` can only belong to a single `SpriteSet`
+# at a time. The final on-screen position depends on the camera associated
 # to the `SpriteSet`.
+#
+# ~~~
+# # Load texture and create sprite
+# var texture = new Texture("path/in/assets.png")
+# var sprite = new Sprite(texture, new Point3d[Float](0.0, 0.0, 0.0))
+#
+# # Add sprite to the visible game world
+# app.sprites.add sprite
+#
+# # Extra configuration of the sprite
+# sprite.rotation = pi/2.0
+# sprite.scale = 2.0
+#
+# # Show only the blue colors
+# sprite.red = 0.0
+# sprite.green = 0.0
+# ~~~
+#
+# To add a sprite to the UI it can be anchored to screen borders
+# with `ui_camera.top_left` and the likes.
+#
+# ~~~nitish
+# # Place it a bit off the top left of the screen
+# var pos = app.ui_camera.top_left.offset(128.0, -128.0, 0)
+#
+# # Load texture and create sprite
+# var texture = new Texture("path/in/assets.png")
+# var sprite = new Sprite(texture, pos)
+#
+# # Add it to the UI (above world sprites)
+# app.ui_sprites.add sprite
+# ~~~
 class Sprite
 
        # Texture drawn to screen
@@ -80,10 +123,10 @@ class Sprite
                center_direct = value
        end
 
-       # Rotation on the Z axis, positive values go counterclockwise
+       # Rotation on the Z axis, positive values turn counterclockwise
        var rotation = 0.0 is writable(rotation_direct=)
 
-       # Rotation on the Z axis, positive values go counterclockwise
+       # Rotation on the Z axis, positive values turn counterclockwise
        fun rotation=(value: Float)
        do
                if isset _rotation and value != rotation then needs_update
@@ -102,10 +145,12 @@ class Sprite
 
        # Scale applied to this sprite
        #
-       # The default size of `self` depends on the size in pixels of `texture`.
+       # The basic size of `self` depends on the size in pixels of `texture`.
        var scale = 1.0 is writable(scale_direct=)
 
-       # Scale applied to this sprite, see `scale`
+       # Scale applied to this sprite
+       #
+       # The basic size of `self` depends on the size in pixels of `texture`.
        fun scale=(value: Float)
        do
                if isset _scale and value != scale then needs_update
@@ -190,7 +235,10 @@ class Sprite
        fun needs_update
        do
                var c = context
-               if c != null then c.sprites_to_update.add self
+               if c == null then return
+               if c.last_sprite_to_update == self then return
+               c.sprites_to_update.add self
+               c.last_sprite_to_update = self
        end
 
        # Request a resorting of this sprite in its sprite list
@@ -208,6 +256,9 @@ class Sprite
        # Current context to which `self` was sorted
        private var context: nullable SpriteContext = null
 
+       # Index in `context`
+       private var context_index: Int = -1
+
        # Current context to which `self` belongs
        private var sprite_set: nullable SpriteSet = null
 end
@@ -233,10 +284,10 @@ redef class App
        # Camera for `ui_sprites` using an orthogonal view
        var ui_camera = new UICamera(app.display.as(not null)) is lazy
 
-       # World sprites to draw as seen by `world_camera`
+       # World sprites drawn as seen by `world_camera`
        var sprites: Set[Sprite] = new SpriteSet
 
-       # UI sprites to draw as seen by `ui_camera`, drawn over world `sprites`
+       # UI sprites drawn as seen by `ui_camera`, over world `sprites`
        var ui_sprites: Set[Sprite] = new SpriteSet
 
        # Main method to refine in clients to update game logic and `sprites`
@@ -249,9 +300,7 @@ redef class App
        do
                texture.load
 
-               ui_camera.reset_height 1080.0
-
-               var splash = new Sprite(texture, ui_camera.center)
+               var splash = new Sprite(texture, ui_camera.center.offset(0.0, 0.0, 0.0))
                ui_sprites.add splash
 
                var display = display
@@ -329,11 +378,19 @@ redef class App
                if display != null then display.close
        end
 
-       redef fun frame_core(display)
+       redef fun on_resize(display)
        do
-               # Prepare to draw, clear buffers
-               glClear(gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT)
+               super
+
+               world_camera.mvp_matrix_cache = null
+               ui_camera.mvp_matrix_cache = null
 
+               # Update all sprites in the UI
+               for sprite in ui_sprites do sprite.needs_update
+       end
+
+       redef fun frame_core(display)
+       do
                # Check errors
                var gl_error = glGetError
                assert gl_error == gl_NO_ERROR else print_error gl_error
@@ -541,12 +598,6 @@ private class Simple2dProgram
 end
 
 redef class Point3d[N]
-       # Get a new `Point3d[Float]` with an offset of each axis of `x, y, z`
-       fun offset(x, y, z: Numeric): Point3d[Float]
-       do
-               return new Point3d[Float](self.x.to_f+x.to_f, self.y.to_f+y.to_f, self.z.to_f+z.to_f)
-       end
-
        # ---
        # Associate each point to its sprites
 
@@ -597,12 +648,32 @@ redef class Point3d[N]
        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[GamnitRootTexture, Bool, SpriteContext]
+       var contexts_map = new HashMap2[RootTexture, Bool, SpriteContext]
 
        # Contexts in `contexts_map`
        var contexts_items = new Array[SpriteContext]
@@ -628,6 +699,7 @@ private class SpriteSet
 
                context.sprites.add sprite
                context.sprites_to_update.add sprite
+               context.last_sprite_to_update = sprite
 
                sprite.context = context
                sprite.sprite_set = self
@@ -697,17 +769,20 @@ private class SpriteContext
        # Context config and state
 
        # Only root texture drawn by this context
-       var texture: nullable GamnitRootTexture
+       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]
+       var sprites = new GroupedSprites
 
        # Sprites to update since last `draw`
        var sprites_to_update = new Set[Sprite]
 
+       # Cache of the last `Sprite` added to `sprites_to_update` since the last call to `draw`
+       var last_sprite_to_update: nullable Sprite = null
+
        # Sprites that have been update and for which `needs_update` can be set to false
        var updated_sprites = new Array[Sprite]
 
@@ -813,8 +888,11 @@ private class SpriteContext
        # Update GPU data of `sprite`
        fun update_sprite(sprite: Sprite)
        do
-               var sprite_index = sprites.index_of(sprite)
-               if sprite_index == -1 then return
+               var context = sprite.context
+               if context != self then return
+
+               var sprite_index = sprite.context_index
+               assert sprite_index != -1
 
                # Vertices data
 
@@ -939,6 +1017,7 @@ private class SpriteContext
 
                        for sprite in sprites_to_update do update_sprite(sprite)
                        sprites_to_update.clear
+                       last_sprite_to_update = null
 
                        sys.perfs["gamnit flat gpu update"].add app.perf_clock_sprites.lapse
                end
@@ -1040,6 +1119,8 @@ end
 # The data can be compressed by a call to `defragment`.
 #
 # ~~~
+# intrude import gamnit::flat
+#
 # var array = new GroupedArray[String]
 # assert array.to_s == ""
 #
@@ -1102,9 +1183,6 @@ private class GroupedArray[E]
        # 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
 
@@ -1114,8 +1192,8 @@ private class GroupedArray[E]
        # Index of the spots after filled chunks
        var ends = new List[Int]
 
-       # Add `item` to the first available slot
-       fun add(item: E)
+       # Add `item` to the first available slot and return its index
+       fun add(item: E): Int
        do
                length += 1
 
@@ -1140,7 +1218,7 @@ private class GroupedArray[E]
                                # at end of first chunk
                                ends.first += 1
                        end
-                       return
+                       return i
                end
 
                items.add item
@@ -1148,13 +1226,20 @@ private class GroupedArray[E]
                        starts.add 0
                        ends.add 1
                else ends.last += 1
+               return ends.last - 1
        end
 
        # Remove the first instance of `item`
        fun remove(item: E)
        do
-               var i = items.index_of(item)
-               assert i != -1
+               var index = items.index_of(item)
+               remove_at(item, index)
+       end
+
+       # Remove `item` at `index`
+       fun remove_at(item: E, index: Int)
+       do
+               var i = index
                length -= 1
                items[i] = null
 
@@ -1199,6 +1284,8 @@ private class GroupedArray[E]
        # Returns the elements that moved as a list.
        #
        # ~~~
+       # intrude import gamnit::flat
+       #
        # var array = new GroupedArray[String]
        # array.add "a"
        # array.add "b"
@@ -1227,6 +1314,7 @@ private class GroupedArray[E]
                while max > 0 and (starts.length > 1 or starts.first != 0) do
                        var i = ends.last - 1
                        var e = items[i]
+                       assert e != null
                        remove e
                        add e
                        moved.add e
@@ -1259,6 +1347,20 @@ private class GroupedArray[E]
        end
 end
 
+# Optimized `GroupedArray` to use `Sprite::context_index` and avoid using `index_of`
+private class GroupedSprites
+       super GroupedArray[Sprite]
+
+       redef fun add(item)
+       do
+               var index = super
+               item.context_index = index
+               return index
+       end
+
+       redef fun remove(item) do remove_at(item, item.context_index)
+end
+
 redef class GLfloatArray
        private fun fill_from_matrix(matrix: Matrix, dst_offset: nullable Int)
        do