c783e97d90e55395178a85e242a2b5154f4cc31b
[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 # Real screen framebuffer
58 private var screen_framebuffer_cache: Int = -1
59
60 # Real screen framebuffer name
61 fun screen_framebuffer: Int
62 do
63 var cache = screen_framebuffer_cache
64 if cache != -1 then return cache
65
66 cache = glGetIntegerv(gl_FRAMEBUFFER_BINDING, 0)
67 self.screen_framebuffer_cache = cache
68 return cache
69 end
70
71 redef fun create_gamnit
72 do
73 super
74
75 var program = dynres_program
76 program.compile_and_link
77 var error = program.error
78 assert error == null else print_error error
79
80 dynamic_context_cache = null
81 end
82
83 redef fun on_resize(display)
84 do
85 if dynamic_context_cache != null then dynamic_context.resize(display, max_dynamic_resolution_ratio)
86 super
87 end
88
89 # Prepare to draw to the dynamic screen if `dynamic_resolution_ratio != 1.0`
90 protected fun frame_core_dynamic_resolution_before(display: GamnitDisplay)
91 do
92 # TODO autodetect when to lower/increase resolution
93
94 if dynamic_resolution_ratio == 1.0 then
95 # Draw directly to the screen framebuffer
96 glBindFramebuffer(gl_FRAMEBUFFER, screen_framebuffer)
97 glViewport(0, 0, display.width, display.height)
98 glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
99
100 var gl_error = glGetError
101 assert gl_error == gl_NO_ERROR else print_error gl_error
102 return
103 end
104
105 # Draw to our dynamic framebuffer
106 glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.dynamic_framebuffer)
107
108 var ratio = dynamic_resolution_ratio
109 ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
110 glViewport(0, 0, (display.width.to_f*ratio).to_i,
111 (display.height.to_f*ratio).to_i)
112
113 glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
114
115 var gl_error = glGetError
116 assert gl_error == gl_NO_ERROR else print_error gl_error
117 end
118
119 # Draw the dynamic screen to the real screen if `dynamic_resolution_ratio != 1.0`
120 protected fun frame_core_dynamic_resolution_after(display: GamnitDisplay)
121 do
122 if dynamic_resolution_ratio == 1.0 then return
123 perf_clock_dynamic_resolution.lapse
124
125 var ratio = dynamic_resolution_ratio
126 ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
127
128 glBindFramebuffer(gl_FRAMEBUFFER, screen_framebuffer)
129 glBindBuffer(gl_ARRAY_BUFFER, dynamic_context.buffer_array)
130 glViewport(0, 0, display.width, display.height)
131 glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
132 dynres_program.use
133
134 # Uniforms
135 glActiveTexture gl_TEXTURE0
136 glBindTexture(gl_TEXTURE_2D, dynamic_context.texture)
137 dynres_program.texture.uniform 0
138 dynres_program.ratio.uniform ratio
139
140 # Attributes
141 var sizeof_gl_float = 4
142 var n_floats = 3
143 glEnableVertexAttribArray dynres_program.coord.location
144 glVertexAttribPointeri(dynres_program.coord.location, n_floats, gl_FLOAT, false, 0, 0)
145 var offset = 4 * n_floats * sizeof_gl_float
146
147 n_floats = 2
148 glEnableVertexAttribArray dynres_program.tex_coord.location
149 glVertexAttribPointeri(dynres_program.tex_coord.location, n_floats, gl_FLOAT, false, 0, offset)
150 var gl_error = glGetError
151 assert gl_error == gl_NO_ERROR else print_error gl_error
152
153 # Draw
154 glDrawArrays(gl_TRIANGLE_STRIP, 0, 4)
155 gl_error = glGetError
156 assert gl_error == gl_NO_ERROR else print_error gl_error
157
158 # Take down
159 glBindBuffer(gl_ARRAY_BUFFER, 0)
160 gl_error = glGetError
161 assert gl_error == gl_NO_ERROR else print_error gl_error
162
163 sys.perfs["gamnit flat dyn res"].add app.perf_clock_dynamic_resolution.lapse
164 end
165
166 # Framebuffer and texture for dynamic resolution intermediate drawing
167 private fun dynamic_context: DynamicContext
168 do
169 var cache = dynamic_context_cache
170 if cache != null then return cache
171
172 cache = create_dynamic_context
173 dynamic_context_cache = cache
174 return cache
175 end
176
177 private var dynamic_context_cache: nullable DynamicContext = null
178
179 private fun create_dynamic_context: DynamicContext
180 do
181 # TODO option or flag to regen on real resolution change.
182
183 var display = display
184 assert display != null
185
186 var context = new DynamicContext
187 context.prepare_once(display, max_dynamic_resolution_ratio)
188 return context
189 end
190 end
191
192 # Handles to reused GL buffers and texture
193 private class DynamicContext
194
195 # Dynamic screen framebuffer
196 var dynamic_framebuffer: Int = -1
197
198 # Depth renderbuffer attached to `dynamic_framebuffer`
199 var depth_renderbuffer: Int = -1
200
201 # Texture attached to `dynamic_framebuffer` as color attachment
202 var texture: Int = -1
203
204 # Buffer name for vertex data
205 var buffer_array: Int = -1
206
207 # Prepare all attributes once per resolution change
208 fun prepare_once(display: GamnitDisplay, max_dynamic_resolution_ratio: Float)
209 do
210 # TODO enable antialiasing.
211
212 # Framebuffer
213 var framebuffer = glGenFramebuffers(1).first
214 glBindFramebuffer(gl_FRAMEBUFFER, framebuffer)
215 assert glIsFramebuffer(framebuffer)
216 self.dynamic_framebuffer = framebuffer
217 var gl_error = glGetError
218 assert gl_error == gl_NO_ERROR else print_error gl_error
219
220 # Depth & texture/color
221 var depthbuffer = glGenRenderbuffers(1).first
222 self.depth_renderbuffer = depthbuffer
223 var texture = glGenTextures(1).first
224 self.texture = texture
225 gl_error = glGetError
226 assert gl_error == gl_NO_ERROR else print_error gl_error
227
228 resize(display, max_dynamic_resolution_ratio)
229 assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
230
231 # Array buffer
232 buffer_array = glGenBuffers(1).first
233 glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
234 assert glIsBuffer(buffer_array)
235 gl_error = glGetError
236 assert gl_error == gl_NO_ERROR else print_error gl_error
237
238 ## coord
239 var data = new Array[Float]
240 data.add_all([-1.0, -1.0, 0.0,
241 1.0, -1.0, 0.0,
242 -1.0, 1.0, 0.0,
243 1.0, 1.0, 0.0])
244 ## tex_coord
245 data.add_all([0.0, 0.0,
246 1.0, 0.0,
247 0.0, 1.0,
248 1.0, 1.0])
249 var c_data = new GLfloatArray.from(data)
250 glBufferData(gl_ARRAY_BUFFER, data.length*4, c_data.native_array, gl_STATIC_DRAW)
251
252 glBindBuffer(gl_ARRAY_BUFFER, 0)
253
254 gl_error = glGetError
255 assert gl_error == gl_NO_ERROR else print_error gl_error
256 end
257
258 # Init size or resize `depth_renderbuffer` and `texture`
259 fun resize(display: GamnitDisplay, max_dynamic_resolution_ratio: Float)
260 do
261 var width = (display.width.to_f * max_dynamic_resolution_ratio).to_i
262 var height = (display.height.to_f * max_dynamic_resolution_ratio).to_i
263
264 glBindFramebuffer(gl_FRAMEBUFFER, dynamic_framebuffer)
265
266 var depthbuffer = self.depth_renderbuffer
267 var texture = self.texture
268
269 # Depth
270 glBindRenderbuffer(gl_RENDERBUFFER, depthbuffer)
271 assert glIsRenderbuffer(depthbuffer)
272 glRenderbufferStorage(gl_RENDERBUFFER, gl_DEPTH_COMPONENT16, width, height)
273 glFramebufferRenderbuffer(gl_FRAMEBUFFER, gl_DEPTH_ATTACHMENT, gl_RENDERBUFFER, depthbuffer)
274 var gl_error = glGetError
275 assert gl_error == gl_NO_ERROR else print_error gl_error
276
277 # Texture
278 glBindTexture(gl_TEXTURE_2D, texture)
279 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR)
280 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
281 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
282 glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
283 glTexImage2D(gl_TEXTURE_2D, 0, gl_RGB, width, height,
284 0, gl_RGB, gl_UNSIGNED_BYTE, new Pointer.nul)
285 glFramebufferTexture2D(gl_FRAMEBUFFER, gl_COLOR_ATTACHMENT0, gl_TEXTURE_2D, texture, 0)
286
287 gl_error = glGetError
288 assert gl_error == gl_NO_ERROR else print_error gl_error
289
290 # Take down
291 glBindRenderbuffer(gl_RENDERBUFFER, 0)
292 glBindFramebuffer(gl_FRAMEBUFFER, 0)
293
294 gl_error = glGetError
295 assert gl_error == gl_NO_ERROR else print_error gl_error
296 end
297
298 var destroyed = false
299 fun destroy
300 do
301 if destroyed then return
302 destroyed = true
303
304 # Free the buffer
305 glDeleteBuffers([buffer_array])
306 var gl_error = glGetError
307 assert gl_error == gl_NO_ERROR else print_error gl_error
308 buffer_array = -1
309
310 # Free the dynamic framebuffer and its attachments
311 glDeleteBuffers([buffer_array])
312 glDeleteFramebuffers([dynamic_framebuffer])
313 glDeleteRenderbuffers([depth_renderbuffer])
314 glDeleteTextures([texture])
315 end
316 end
317
318 private class DynamicResolutionProgram
319 super GamnitProgramFromSource
320
321 redef var vertex_shader_source = """
322 // Vertex coordinates
323 attribute vec3 coord;
324
325 // Vertex coordinates on textures
326 attribute vec2 tex_coord;
327
328 // Output to the fragment shader
329 varying vec2 v_coord;
330
331 void main()
332 {
333 gl_Position = vec4(coord, 1.0);
334 v_coord = tex_coord;
335 }
336 """ @ glsl_vertex_shader
337
338 redef var fragment_shader_source = """
339 precision mediump float;
340
341 // Virtual screen texture / color attachment
342 uniform sampler2D texture0;
343
344 // Input from the vertex shader
345 varying vec2 v_coord;
346
347 // Ratio of the virtual screen to draw
348 uniform float ratio;
349
350 void main()
351 {
352 gl_FragColor = texture2D(texture0, v_coord*ratio);
353 }
354 """ @ glsl_fragment_shader
355
356 # Vertices coordinates
357 var coord = attributes["coord"].as(AttributeVec3) is lazy
358
359 # Coordinates on the textures, per vertex
360 var tex_coord = attributes["tex_coord"].as(AttributeVec2) is lazy
361
362 # Virtual screen texture / color attachment
363 var texture = uniforms["texture0"].as(UniformSampler2D) is lazy
364
365 # Ratio of the virtual screen to draw
366 var ratio = uniforms["ratio"].as(UniformFloat) is lazy
367 end