Merge: gamnit: new services and a lot of bug fixes and performance improvements
[nit.git] / lib / gamnit / dynamic_resolution.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Virtual screen with a resolution independent from the real screen
16 #
17 # The attribute `dynamic_resolution_ratio` sets the size of the virtual screen
18 # in relation to the real screen. See its documentation for more information.
19 #
20 # Reference implementation:
21 # https://software.intel.com/en-us/articles/dynamic-resolution-rendering-on-opengl-es-2
22 module dynamic_resolution
23
24 import performance_analysis
25
26 import gamnit
27
28 redef class App
29 # Resolution of the dynamic screen as ratio of the real screen resolution.
30 #
31 # - At 1.0, the default, the virtual screen is not used and the visuals are
32 # drawn directly to the real screen and pixels.
33 # - When below 1.0, there is less pixels in the dynamic screen than in the
34 # real screen. This reduces the strain on the GPU, especially of high
35 # resolution displays.
36 # - Values above 1.0 are not supported at this point, but they would allow
37 # super-sampling.
38 #
39 # This value must be set either by the user using a video quality slider or
40 # by an heuristic according to the device capabilities.
41 # A lower value should use less battery power on mobile devices.
42 #
43 # This value is applied to both X and Y, so it has an exponential effect on
44 # the number of pixels.
45 var dynamic_resolution_ratio = 1.0 is writable
46
47 # Minimum dynamic screen resolution
48 var min_dynamic_resolution_ratio = 0.0125 is writable
49
50 # Maximum dynamic screen resolution, must be set before first draw
51 var max_dynamic_resolution_ratio = 1.0 is writable
52
53 private var dynres_program = new DynamicResolutionProgram
54
55 private var perf_clock_dynamic_resolution = new Clock is lazy
56
57 redef fun create_scene
58 do
59 super
60
61 var program = dynres_program
62 program.compile_and_link
63 var error = program.error
64 assert error == null else print_error error
65 end
66
67 redef fun on_resize(display)
68 do
69 dynamic_context.resize(display, max_dynamic_resolution_ratio)
70 super
71 end
72
73 # Prepare to draw to the dynamic screen if `dynamic_resolution_ratio != 1.0`
74 protected fun frame_core_dynamic_resolution_before(display: GamnitDisplay)
75 do
76 # TODO autodetect when to lower/increase resolution
77
78 if dynamic_resolution_ratio == 1.0 then
79 # Draw directly to the screen framebuffer
80 glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.screen_framebuffer)
81 glViewport(0, 0, display.width, display.height)
82 glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
83
84 var gl_error = glGetError
85 assert gl_error == gl_NO_ERROR else print_error gl_error
86 return
87 end
88
89 # Draw to our dynamic framebuffer
90 glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.dynamic_framebuffer)
91
92 var ratio = dynamic_resolution_ratio
93 ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
94 glViewport(0, 0, (display.width.to_f*ratio).to_i,
95 (display.height.to_f*ratio).to_i)
96
97 glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
98
99 var gl_error = glGetError
100 assert gl_error == gl_NO_ERROR else print_error gl_error
101 end
102
103 # Draw the dynamic screen to the real screen if `dynamic_resolution_ratio != 1.0`
104 protected fun frame_core_dynamic_resolution_after(display: GamnitDisplay)
105 do
106 if dynamic_resolution_ratio == 1.0 then return
107 perf_clock_dynamic_resolution.lapse
108
109 var ratio = dynamic_resolution_ratio
110 ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
111
112 glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.screen_framebuffer)
113 glBindBuffer(gl_ARRAY_BUFFER, dynamic_context.buffer_array)
114 glViewport(0, 0, display.width, display.height)
115 glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
116 dynres_program.use
117
118 # Uniforms
119 glActiveTexture gl_TEXTURE0
120 glBindTexture(gl_TEXTURE_2D, dynamic_context.texture)
121 dynres_program.texture.uniform 0
122 dynres_program.ratio.uniform ratio
123
124 # Attributes
125 var sizeof_gl_float = 4
126 var n_floats = 3
127 glEnableVertexAttribArray dynres_program.coord.location
128 glVertexAttribPointeri(dynres_program.coord.location, n_floats, gl_FLOAT, false, 0, 0)
129 var offset = 4 * n_floats * sizeof_gl_float
130
131 n_floats = 2
132 glEnableVertexAttribArray dynres_program.tex_coord.location
133 glVertexAttribPointeri(dynres_program.tex_coord.location, n_floats, gl_FLOAT, false, 0, offset)
134 var gl_error = glGetError
135 assert gl_error == gl_NO_ERROR else print_error gl_error
136
137 # Draw
138 glDrawArrays(gl_TRIANGLE_STRIP, 0, 4)
139 gl_error = glGetError
140 assert gl_error == gl_NO_ERROR else print_error gl_error
141
142 # Take down
143 glBindBuffer(gl_ARRAY_BUFFER, 0)
144 gl_error = glGetError
145 assert gl_error == gl_NO_ERROR else print_error gl_error
146
147 sys.perfs["gamnit flat dyn res"].add app.perf_clock_dynamic_resolution.lapse
148 end
149
150 # Framebuffer and texture for dynamic resolution intermediate drawing
151 private var dynamic_context: DynamicContext = create_dynamic_context is lazy
152
153 private fun create_dynamic_context: DynamicContext
154 do
155 # TODO option or flag to regen on real resolution change.
156
157 var display = display
158 assert display != null
159
160 var context = new DynamicContext
161 context.prepare_once(display, max_dynamic_resolution_ratio)
162 return context
163 end
164 end
165
166 # Handles to reused GL buffers and texture
167 private class DynamicContext
168
169 # Real screen framebuffer
170 var screen_framebuffer: Int = -1
171
172 # Dynamic screen framebuffer
173 var dynamic_framebuffer: Int = -1
174
175 # Depth renderbuffer attached to `dynamic_framebuffer`
176 var depth_renderbuffer: Int = -1
177
178 # Texture attached to `dynamic_framebuffer` as color attachment
179 var texture: Int = -1
180
181 # Buffer name for vertex data
182 var buffer_array: Int = -1
183
184 # Prepare all attributes once per resolution change
185 fun prepare_once(display: GamnitDisplay, max_dynamic_resolution_ratio: Float)
186 do
187 # TODO enable antialiasing.
188
189 # Set aside the real screen framebuffer name
190 var screen_framebuffer = glGetIntegerv(gl_FRAMEBUFFER_BINDING, 0)
191 self.screen_framebuffer = screen_framebuffer
192
193 # Framebuffer
194 var framebuffer = glGenFramebuffers(1).first
195 glBindFramebuffer(gl_FRAMEBUFFER, framebuffer)
196 assert glIsFramebuffer(framebuffer)
197 self.dynamic_framebuffer = framebuffer
198 var gl_error = glGetError
199 assert gl_error == gl_NO_ERROR else print_error gl_error
200
201 # Depth & texture/color
202 var depthbuffer = glGenRenderbuffers(1).first
203 self.depth_renderbuffer = depthbuffer
204 var texture = glGenTextures(1).first
205 self.texture = texture
206 gl_error = glGetError
207 assert gl_error == gl_NO_ERROR else print_error gl_error
208
209 resize(display, max_dynamic_resolution_ratio)
210 assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
211
212 # Array buffer
213 buffer_array = glGenBuffers(1).first
214 glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
215 assert glIsBuffer(buffer_array)
216 gl_error = glGetError
217 assert gl_error == gl_NO_ERROR else print_error gl_error
218
219 ## coord
220 var data = new Array[Float]
221 data.add_all([-1.0, -1.0, 0.0,
222 1.0, -1.0, 0.0,
223 -1.0, 1.0, 0.0,
224 1.0, 1.0, 0.0])
225 ## tex_coord
226 data.add_all([0.0, 0.0,
227 1.0, 0.0,
228 0.0, 1.0,
229 1.0, 1.0])
230 var c_data = new GLfloatArray.from(data)
231 glBufferData(gl_ARRAY_BUFFER, data.length*4, c_data.native_array, gl_STATIC_DRAW)
232
233 glBindBuffer(gl_ARRAY_BUFFER, 0)
234
235 gl_error = glGetError
236 assert gl_error == gl_NO_ERROR else print_error gl_error
237 end
238
239 # Init size or resize `depth_renderbuffer` and `texture`
240 fun resize(display: GamnitDisplay, max_dynamic_resolution_ratio: Float)
241 do
242 var width = (display.width.to_f * max_dynamic_resolution_ratio).to_i
243 var height = (display.height.to_f * max_dynamic_resolution_ratio).to_i
244
245 glBindFramebuffer(gl_FRAMEBUFFER, dynamic_framebuffer)
246
247 var depthbuffer = self.depth_renderbuffer
248 var texture = self.texture
249
250 # Depth
251 glBindRenderbuffer(gl_RENDERBUFFER, depthbuffer)
252 assert glIsRenderbuffer(depthbuffer)
253 glRenderbufferStorage(gl_RENDERBUFFER, gl_DEPTH_COMPONENT16, width, height)
254 glFramebufferRenderbuffer(gl_FRAMEBUFFER, gl_DEPTH_ATTACHMENT, gl_RENDERBUFFER, depthbuffer)
255 var gl_error = glGetError
256 assert gl_error == gl_NO_ERROR else print_error gl_error
257
258 # Texture
259 glBindTexture(gl_TEXTURE_2D, texture)
260 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR)
261 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
262 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
263 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
264 glTexImage2D(gl_TEXTURE_2D, 0, gl_RGB, width, height,
265 0, gl_RGB, gl_UNSIGNED_BYTE, new Pointer.nul)
266 glFramebufferTexture2D(gl_FRAMEBUFFER, gl_COLOR_ATTACHMENT0, gl_TEXTURE_2D, texture, 0)
267
268 gl_error = glGetError
269 assert gl_error == gl_NO_ERROR else print_error gl_error
270
271 # Take down
272 glBindRenderbuffer(gl_RENDERBUFFER, 0)
273 glBindFramebuffer(gl_FRAMEBUFFER, 0)
274
275 gl_error = glGetError
276 assert gl_error == gl_NO_ERROR else print_error gl_error
277 end
278
279 var destroyed = false
280 fun destroy
281 do
282 if destroyed then return
283 destroyed = true
284
285 # Free the buffer
286 glDeleteBuffers([buffer_array])
287 var gl_error = glGetError
288 assert gl_error == gl_NO_ERROR else print_error gl_error
289 buffer_array = -1
290
291 # Free the dynamic framebuffer and its attachments
292 glDeleteBuffers([buffer_array])
293 glDeleteFramebuffers([dynamic_framebuffer])
294 glDeleteRenderbuffers([depth_renderbuffer])
295 glDeleteTextures([texture])
296 end
297 end
298
299 private class DynamicResolutionProgram
300 super GamnitProgramFromSource
301
302 redef var vertex_shader_source = """
303 // Vertex coordinates
304 attribute vec3 coord;
305
306 // Vertex coordinates on textures
307 attribute vec2 tex_coord;
308
309 // Output to the fragment shader
310 varying vec2 v_coord;
311
312 void main()
313 {
314 gl_Position = vec4(coord, 1.0);
315 v_coord = tex_coord;
316 }
317 """ @ glsl_vertex_shader
318
319 redef var fragment_shader_source = """
320 precision mediump float;
321
322 // Virtual screen texture / color attachment
323 uniform sampler2D texture0;
324
325 // Input from the vertex shader
326 varying vec2 v_coord;
327
328 // Ratio of the virtual screen to draw
329 uniform float ratio;
330
331 void main()
332 {
333 gl_FragColor = texture2D(texture0, v_coord*ratio);
334 }
335 """ @ glsl_fragment_shader
336
337 # Vertices coordinates
338 var coord = attributes["coord"].as(AttributeVec3) is lazy
339
340 # Coordinates on the textures, per vertex
341 var tex_coord = attributes["tex_coord"].as(AttributeVec2) is lazy
342
343 # Virtual screen texture / color attachment
344 var texture = uniforms["texture0"].as(UniformSampler2D) is lazy
345
346 # Ratio of the virtual screen to draw
347 var ratio = uniforms["ratio"].as(UniformFloat) is lazy
348 end