gamnit: intro dynamic resolution support
authorAlexis Laferrière <alexis.laf@xymus.net>
Fri, 31 Mar 2017 02:15:23 +0000 (22:15 -0400)
committerAlexis Laferrière <alexis.laf@xymus.net>
Mon, 10 Apr 2017 16:05:05 +0000 (12:05 -0400)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

lib/gamnit/depth/depth.nit
lib/gamnit/dynamic_resolution.nit [new file with mode: 0644]
lib/gamnit/flat.nit

index a9a446a..f18f2eb 100644 (file)
@@ -45,6 +45,8 @@ redef class App
        # Draw all elements of `actors` and then call `frame_core_flat`
        protected fun frame_core_depth(display: GamnitDisplay)
        do
+               frame_core_dynamic_resolution_before display
+
                # Update cameras on both our programs
                versatile_program.use
                versatile_program.mvp.uniform world_camera.mvp_matrix
@@ -71,6 +73,8 @@ redef class App
 
                frame_core_ui_sprites display
                perfs["gamnit depth ui_sprites"].add frame_core_depth_clock.lapse
+
+               frame_core_dynamic_resolution_after display
        end
 
        private var frame_core_depth_clock = new Clock
diff --git a/lib/gamnit/dynamic_resolution.nit b/lib/gamnit/dynamic_resolution.nit
new file mode 100644 (file)
index 0000000..40ea221
--- /dev/null
@@ -0,0 +1,316 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Virtual screen with a resolution independent from the real screen
+#
+# The attribute `dynamic_resolution_ratio` sets the size of the virtual screen
+# in relation to the real screen. See its documentation for more information.
+#
+# Reference implementation:
+# https://software.intel.com/en-us/articles/dynamic-resolution-rendering-on-opengl-es-2
+module dynamic_resolution
+
+import performance_analysis
+
+import gamnit
+
+redef class App
+       # Resolution of the dynamic screen as ratio of the real screen resolution.
+       #
+       # - At 1.0, the default, the virtual screen is not used and the visuals are
+       #   drawn directly to the real screen and pixels.
+       # - When below 1.0, there is less pixels in the dynamic screen than in the
+       #   real screen. This reduces the strain on the GPU, especially of high
+       #   resolution displays.
+       # - Values above 1.0 are not supported at this point, but they would allow
+       #   super-sampling.
+       #
+       # This value must be set either by the user using a video quality slider or
+       # by an heuristic according to the device capabilities.
+       # A lower value should use less battery power on mobile devices.
+       #
+       # This value is applied to both X and Y, so it has an exponential effect on
+       # the number of pixels.
+       var dynamic_resolution_ratio = 1.0
+
+       # Minimum dynamic screen resolution
+       var min_dynamic_resolution_ratio = 0.0125 is writable
+
+       # Maximum dynamic screen resolution, must be set before first draw
+       var max_dynamic_resolution_ratio = 1.0 is writable
+
+       private var dynres_program = new DynamicResolutionProgram
+
+       private var perf_clock_dynamic_resolution = new Clock is lazy
+
+       redef fun on_create
+       do
+               super
+
+               var program = dynres_program
+               program.compile_and_link
+               var error = program.error
+               assert error == null else print_error error
+       end
+
+       # Prepare to draw to the dynamic screen if `dynamic_resolution_ratio != 1.0`
+       protected fun frame_core_dynamic_resolution_before(display: GamnitDisplay)
+       do
+               # TODO autodetect when to lower/increase resolution
+
+               if dynamic_resolution_ratio == 1.0 then
+                       # Draw directly to the screen framebuffer
+                       glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.screen_framebuffer)
+                       glViewport(0, 0, display.width, display.height)
+
+                       var gl_error = glGetError
+                       assert gl_error == gl_NO_ERROR else print_error gl_error
+                       return
+               end
+
+               # Draw to our dynamic framebuffer
+               glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.dynamic_framebuffer)
+               glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
+
+               var ratio = dynamic_resolution_ratio
+               ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
+               glViewport(0, 0, (display.width.to_f*ratio).to_i,
+                                                (display.height.to_f*ratio).to_i)
+
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+       end
+
+       # Draw the dynamic screen to the real screen if `dynamic_resolution_ratio != 1.0`
+       protected fun frame_core_dynamic_resolution_after(display: GamnitDisplay)
+       do
+               if dynamic_resolution_ratio == 1.0 then return
+               perf_clock_dynamic_resolution.lapse
+
+               var ratio = dynamic_resolution_ratio
+               ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
+
+               glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.screen_framebuffer)
+               glBindBuffer(gl_ARRAY_BUFFER, dynamic_context.buffer_array)
+               glViewport(0, 0, display.width, display.height)
+               glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
+               dynres_program.use
+
+               # Uniforms
+               glActiveTexture gl_TEXTURE0
+               glBindTexture(gl_TEXTURE_2D, dynamic_context.texture)
+               dynres_program.texture.uniform 0
+               dynres_program.ratio.uniform ratio
+
+               # Attributes
+               var sizeof_gl_float = 4
+               var n_floats = 3
+               glEnableVertexAttribArray dynres_program.coord.location
+               glVertexAttribPointeri(dynres_program.coord.location, n_floats, gl_FLOAT, false, 0, 0)
+               var offset = 4 * n_floats * sizeof_gl_float
+
+               n_floats = 2
+               glEnableVertexAttribArray dynres_program.tex_coord.location
+               glVertexAttribPointeri(dynres_program.tex_coord.location, n_floats, gl_FLOAT, false, 0, offset)
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # Draw
+               glDrawArrays(gl_TRIANGLE_STRIP, 0, 4)
+
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               sys.perfs["gamnit flat dyn res"].add app.perf_clock_dynamic_resolution.lapse
+       end
+
+       # Framebuffer and texture for dynamic resolution intermediate drawing
+       private var dynamic_context: DynamicContext = create_dynamic_context is lazy
+
+       private fun create_dynamic_context: DynamicContext
+       do
+               # TODO option or flag to regen on real resolution change.
+
+               var display = display
+               assert display != null
+
+               var context = new DynamicContext
+               context.prepare_once(display, max_dynamic_resolution_ratio)
+               return context
+       end
+end
+
+# Program drawing the dynamic screen to the real screen
+private class DynamicContext
+
+       # Real screen framebuffer
+       var screen_framebuffer: Int = -1
+
+       # Dynamic screen framebuffer
+       var dynamic_framebuffer: Int = -1
+
+       # Depth renderbuffer attached to `dynamic_framebuffer`
+       var depth_renderbuffer: Int = -1
+
+       # Texture attached to `dynamic_framebuffer` as color attachment
+       var texture: Int = -1
+
+       # Buffer name for vertex data
+       var buffer_array: Int = -1
+
+       var float_per_vertex: Int is lazy do return 4 + 4 + 3
+
+       # Prepare all attributes once per resolution change
+       fun prepare_once(display: GamnitDisplay, max_dynamic_resolution_ratio: Float)
+       do
+               # TODO enable antialiasing.
+
+               var width = (display.width.to_f * max_dynamic_resolution_ratio).to_i
+               var height = (display.height.to_f * max_dynamic_resolution_ratio).to_i
+
+               # Set aside the real screen framebuffer name
+               var screen_framebuffer = glGetIntegerv(gl_FRAMEBUFFER_BINDING, 0)
+               self.screen_framebuffer = screen_framebuffer
+
+               # Framebuffer
+               var framebuffer = glGenFramebuffers(1).first
+               glBindFramebuffer(gl_FRAMEBUFFER, framebuffer)
+               assert glIsFramebuffer(framebuffer)
+               self.dynamic_framebuffer = framebuffer
+
+               # Depth
+               var depthbuffer = glGenRenderbuffers(1).first
+               glBindRenderbuffer(gl_RENDERBUFFER, depthbuffer)
+               assert glIsRenderbuffer(depthbuffer)
+               glRenderbufferStorage(gl_RENDERBUFFER, gl_DEPTH_COMPNENT16, width, height)
+               glFramebufferRenderbuffer(gl_FRAMEBUFFER, gl_DEPTH_ATTACHMENT, gl_RENDERBUFFER, depthbuffer)
+               self.depth_renderbuffer = depthbuffer
+
+               # Texture
+               var texture = glGenTextures(1).first
+               glBindTexture(gl_TEXTURE_2D, texture)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
+               glTexImage2D(gl_TEXTURE_2D, 0, gl_RGB, width, height,
+                            0, gl_RGB, gl_UNSIGNED_BYTE, new Pointer.nul)
+               glFramebufferTexture2D(gl_FRAMEBUFFER, gl_COLOR_ATTACHMENT0, gl_TEXTURE_2D, texture, 0)
+               self.texture = texture
+
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+               assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
+
+               # Take down
+               glBindRenderbuffer(gl_RENDERBUFFER, 0)
+               glBindFramebuffer(gl_FRAMEBUFFER, 0)
+
+               # Array buffer
+               buffer_array = glGenBuffers(1).first
+               glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
+               assert glIsBuffer(buffer_array)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               ## coord
+               var data = new Array[Float]
+               data.add_all([-1.0, -1.0, 0.0,
+                          1.0, -1.0, 0.0,
+                         -1.0,  1.0, 0.0,
+                          1.0,  1.0, 0.0])
+               ## tex_coord
+               data.add_all([0.0, 0.0,
+                             1.0, 0.0,
+                             0.0, 1.0,
+                             1.0, 1.0])
+               var c_data = new GLfloatArray.from(data)
+               glBufferData(gl_ARRAY_BUFFER, data.length*4, c_data.native_array, gl_STATIC_DRAW)
+
+               glBindBuffer(gl_ARRAY_BUFFER, 0)
+
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+       end
+
+       var destroyed = false
+       fun destroy
+       do
+               if destroyed then return
+               destroyed = true
+
+               # Free the buffer
+               glDeleteBuffers([buffer_array])
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+               buffer_array = -1
+
+               # Free the dynamic framebuffer and its attachments
+               glDeleteBuffers([buffer_array])
+               glDeleteFramebuffers([dynamic_framebuffer])
+               glDeleteRenderbuffers([depth_renderbuffer])
+               glDeleteTextures([texture])
+       end
+end
+
+private class DynamicResolutionProgram
+       super GamnitProgramFromSource
+
+       redef var vertex_shader_source = """
+               // Vertex coordinates
+               attribute vec3 coord;
+
+               // Vertex coordinates on textures
+               attribute vec2 tex_coord;
+
+               // Output to the fragment shader
+               varying vec2 v_coord;
+
+               void main()
+               {
+                       gl_Position = vec4(coord, 1.0);
+                       v_coord = tex_coord;
+               }
+               """ @ glsl_vertex_shader
+
+       redef var fragment_shader_source = """
+               precision mediump float;
+
+               // Virtual screen texture / color attachment
+               uniform sampler2D texture0;
+
+               // Input from the vertex shader
+               varying vec2 v_coord;
+
+               // Ratio of the virtual screen to draw
+               uniform float ratio;
+
+               void main()
+               {
+                       gl_FragColor = texture2D(texture0, v_coord*ratio);
+               }
+               """ @ glsl_fragment_shader
+
+       # Vertices coordinates
+       var coord = attributes["coord"].as(AttributeVec3) is lazy
+
+       # Coordinates on the textures, per vertex
+       var tex_coord = attributes["tex_coord"].as(AttributeVec2) is lazy
+
+       # Virtual screen texture / color attachment
+       var texture = uniforms["texture0"].as(UniformSampler2D) is lazy
+
+       # Ratio of the virtual screen to draw
+       var ratio = uniforms["ratio"].as(UniformFloat) is lazy
+end
index d5cb28e..f6e0fd2 100644 (file)
@@ -41,6 +41,7 @@ import performance_analysis
 
 import gamnit
 import gamnit::cameras
+import gamnit::dynamic_resolution
 import gamnit::limit_fps
 
 import android_two_fingers_motion is conditional(android)
@@ -355,13 +356,16 @@ redef class App
        # Draw the whole screen, all `glDraw...` calls should be executed here
        protected fun frame_core_draw(display: GamnitDisplay)
        do
-               perf_clock_main.lapse
+               frame_core_dynamic_resolution_before display
 
+               perf_clock_main.lapse
                frame_core_world_sprites display
                perfs["gamnit flat world_sprites"].add perf_clock_main.lapse
 
                frame_core_ui_sprites display
                perfs["gamnit flat ui_sprites"].add perf_clock_main.lapse
+
+               frame_core_dynamic_resolution_after display
        end
 
        private fun frame_core_sprites(display: GamnitDisplay, sprite_set: SpriteSet, camera: Camera)