wolfhece 2.0.14__py3-none-any.whl → 2.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1544 @@
1
+ """ This module helps to upload shaders and textures in the GPU.
2
+ It also provides a lot of controls to ensure compatibility
3
+ of textures, samplers, uniforms types (because OpenGL is pretty
4
+ silent about them and incompatibilities lead to mindbending debugging)
5
+ It is very basic and tuned to our needs.
6
+ """
7
+ import re
8
+ import os
9
+ import ctypes
10
+ import logging
11
+ from typing import Union
12
+ from pathlib import Path
13
+ # from traceback import print_stack
14
+
15
+ import numpy as np
16
+ from OpenGL.GL import (
17
+ GL_MAX_TEXTURE_IMAGE_UNITS,
18
+ glActiveTexture,
19
+ GL_COLOR_ATTACHMENT0,
20
+ GL_COLOR_ATTACHMENT1,
21
+ GL_COLOR_ATTACHMENT2,
22
+ GL_COLOR_ATTACHMENT3,
23
+ GL_COLOR_ATTACHMENT4,
24
+ GL_COLOR_ATTACHMENT5,
25
+ GL_COLOR_ATTACHMENT6,
26
+ GL_COLOR_ATTACHMENT7,
27
+ GL_COLOR_ATTACHMENT8,
28
+ GL_TEXTURE0,
29
+ GL_TEXTURE1,
30
+ GL_TEXTURE2,
31
+ GL_TEXTURE3,
32
+ GL_TEXTURE4,
33
+ GL_TEXTURE5,
34
+ GL_TEXTURE6,
35
+ GL_TEXTURE7,
36
+ GL_TEXTURE8,
37
+ GL_TEXTURE9,
38
+ GL_TEXTURE10,
39
+ GL_TEXTURE11,
40
+ GL_VERTEX_SHADER,
41
+ GL_FRAGMENT_SHADER,
42
+ GL_GEOMETRY_SHADER,
43
+ GL_COMPUTE_SHADER,
44
+ GL_MAX_VIEWPORT_DIMS,
45
+ glCreateShader,
46
+ glCompileShader,
47
+ glDeleteShader,
48
+ GL_COMPILE_STATUS,
49
+ glShaderSource,
50
+ glGetShaderiv,
51
+ glGetShaderInfoLog,
52
+ GL_FALSE,
53
+ GL_TRUE,
54
+ glDeleteProgram,
55
+ GL_TEXTURE_RECTANGLE,
56
+ GL_TEXTURE_2D,
57
+ glGenTextures,
58
+ glTexParameteri,
59
+ GL_TEXTURE_MAG_FILTER,
60
+ GL_NEAREST,
61
+ GL_TEXTURE_MIN_FILTER,
62
+ GL_NEAREST,
63
+ GL_TEXTURE_WRAP_S,
64
+ GL_CLAMP_TO_EDGE,
65
+ GL_TEXTURE_WRAP_T,
66
+ GL_CLAMP_TO_EDGE,
67
+ glBindTexture,
68
+ GL_R32F,
69
+ GL_RED,
70
+ GL_FLOAT,
71
+ glTexImage2D,
72
+ GL_RGB32F,
73
+ GL_RGB32UI,
74
+ GL_RGB,
75
+ GL_RGBA,
76
+ GL_RGBA_INTEGER,
77
+ GL_R8UI,
78
+ GL_R32UI,
79
+ GL_R32I,
80
+ GL_RG16UI,
81
+ GL_RGB16UI,
82
+ GL_RG_INTEGER,
83
+ GL_RGB_INTEGER,
84
+ GL_BGRA,
85
+ GL_RGBA8,
86
+ GL_RG32F,
87
+ GL_RG,
88
+ GL_RED_INTEGER,
89
+ GL_UNSIGNED_BYTE,
90
+ GL_UNSIGNED_INT,
91
+ GL_UNSIGNED_SHORT,
92
+ GL_SHORT,
93
+ GL_INT,
94
+ glBindFramebuffer,
95
+ GL_FRAMEBUFFER,
96
+ glGetError,
97
+ GL_NO_ERROR,
98
+ glReadBuffer,
99
+ glReadPixels,
100
+ glGetTexImage,
101
+ glGenFramebuffers,
102
+ glFramebufferTexture2D,
103
+ glDrawBuffers,
104
+ glCheckFramebufferStatus,
105
+ GL_FRAMEBUFFER_COMPLETE,
106
+ GL_NONE,
107
+ glCreateProgram,
108
+ glAttachShader,
109
+ glLinkProgram,
110
+ glGetProgramiv,
111
+ GL_LINK_STATUS,
112
+ glGetUniformLocation,
113
+ glUniform1f,
114
+ glUniform1ui,
115
+ glUniform1i,
116
+ glUniform2f,
117
+ glUniformMatrix4fv,
118
+ glGetIntegerv,
119
+ GL_MAJOR_VERSION,
120
+ GL_MINOR_VERSION,
121
+ glGetString,
122
+ GL_VERSION,
123
+ GL_VENDOR,
124
+ GL_SHADING_LANGUAGE_VERSION,
125
+ glGetInteger,
126
+ glClearTexImage,
127
+ glGenVertexArrays,
128
+ glBindVertexArray,
129
+ glBindBuffer,
130
+ GL_ARRAY_BUFFER,
131
+ glBufferData,
132
+ GL_STATIC_DRAW,
133
+ glGenBuffers,
134
+ glEnableVertexAttribArray,
135
+ glVertexAttribPointer,
136
+ glBindVertexArray,
137
+ glPixelStorei,
138
+ GL_UNPACK_ALIGNMENT,
139
+ GL_PACK_ALIGNMENT,
140
+ GL_CURRENT_PROGRAM
141
+ )
142
+
143
+ from OpenGL.GL import (
144
+ GL_DEPTH_ATTACHMENT,
145
+ glGenRenderbuffers,
146
+ glBindRenderbuffer,
147
+ GL_RENDERBUFFER,
148
+ glRenderbufferStorage,
149
+ GL_DEPTH_COMPONENT16,
150
+ glFramebufferRenderbuffer,
151
+ GL_DEPTH_COMPONENT32F,
152
+ GL_READ_FRAMEBUFFER,
153
+ glDeleteTextures,
154
+ GL_FRAMEBUFFER_BINDING,
155
+ )
156
+
157
+ from OpenGL.GL import GL_READ_ONLY, GL_RGBA32F, GL_RGBA32UI, glBindImageTexture
158
+
159
+ os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
160
+ import pygame
161
+
162
+ # I use these arrays to avoid dubious computations such as
163
+ # "GL_COLOR_ATTACHMENT0 + n".
164
+ # FIXME Populate them in function of the GPU's maximum values.
165
+
166
+ GL_COLOR_ATTACHMENTS = [
167
+ GL_COLOR_ATTACHMENT0,
168
+ GL_COLOR_ATTACHMENT1,
169
+ GL_COLOR_ATTACHMENT2,
170
+ GL_COLOR_ATTACHMENT3,
171
+ GL_COLOR_ATTACHMENT4,
172
+ GL_COLOR_ATTACHMENT5,
173
+ GL_COLOR_ATTACHMENT6,
174
+ GL_COLOR_ATTACHMENT7,
175
+ GL_COLOR_ATTACHMENT8,
176
+ ]
177
+ TEXTURE_UNITS = [
178
+ GL_TEXTURE0,
179
+ GL_TEXTURE1,
180
+ GL_TEXTURE2,
181
+ GL_TEXTURE3,
182
+ GL_TEXTURE4,
183
+ GL_TEXTURE5,
184
+ GL_TEXTURE6,
185
+ GL_TEXTURE7,
186
+ GL_TEXTURE8,
187
+ GL_TEXTURE9,
188
+ GL_TEXTURE10,
189
+ GL_TEXTURE11,
190
+ ]
191
+ TEX_SAMPLERS_RE = re.compile(
192
+ r".*uniform\s+(sampler2DRect|usampler2DRect|isampler2DRect|image2D|image2DRect|uimage2DRect)\s+(\w+)\s*;"
193
+ )
194
+ IMAGE_UNIT_RE = re.compile(r"layout([,]+, binding = [0-9]+)")
195
+
196
+
197
+ def check_gl_error():
198
+ err = glGetError()
199
+ assert err == GL_NO_ERROR, f"GlError = {err}"
200
+
201
+ def rgb_to_rgba(t):
202
+ assert len(t.shape) == 3
203
+ assert t.shape[2] == 3
204
+ return np.pad(t, ((0, 0), (0, 0), (0, 1)))
205
+
206
+ def memory_aligned_byte_array(size, value):
207
+ # OpenGL wants 4-byts aligned values
208
+ # One can use:
209
+ # from OpenGL.GL import glPixelStorei, GL_UNPACK_ALIGNMENT
210
+ # glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
211
+ # but it's a global setting
212
+
213
+ # This code doesn't work...
214
+ logging.debug(f"memory_aligned_byte_array size={size} value={value}")
215
+ assert ctypes.alignment(ctypes.c_uint32) == 4
216
+ assert ctypes.alignment(ctypes.c_uint32) == glGetInteger(
217
+ GL_UNPACK_ALIGNMENT
218
+ ), "We must align on OpenGL alignment"
219
+ v = value << 24 + value << 16 + value << 8 + value
220
+ padded_len = (size + 3) // 4
221
+ img_data = (ctypes.c_uint32 * padded_len)(*([v] * padded_len))
222
+ assert ctypes.addressof(img_data) % 4 == 0
223
+ # I bet the issue is that img_data being mapped only
224
+ # it may be gc'ed before we have a chance to send it
225
+ # to OpenGL...
226
+ img_data2 = (ctypes.c_uint8 * size).from_buffer(img_data)
227
+ logging.debug("memory_aligned_byte_array - done")
228
+ return img_data2
229
+
230
+ def _get_format_type_from_internal_format(internal_format):
231
+ if internal_format == GL_R32F:
232
+ format, type_ = GL_RED, GL_FLOAT
233
+ elif internal_format == GL_RGB32F:
234
+ format, type_ = GL_RGB, GL_FLOAT
235
+ elif internal_format == GL_RGBA32F:
236
+ format, type_ = GL_RGBA, GL_FLOAT
237
+ elif internal_format == GL_RGBA32UI:
238
+ format, type_ = GL_RGBA_INTEGER, GL_UNSIGNED_INT
239
+ elif internal_format == GL_R32UI:
240
+ format, type_ = GL_RED_INTEGER, GL_UNSIGNED_INT
241
+ # See https://stackoverflow.com/questions/59542891/opengl-integer-texture-raising-gl-invalid-value
242
+ # GL_RED_INTEGER is for unnormalized values (such as uint's)
243
+ elif internal_format == GL_R8UI:
244
+ format, type_ = GL_RED_INTEGER, GL_UNSIGNED_BYTE
245
+ elif internal_format == GL_R32I:
246
+ format, type_ = GL_RED_INTEGER, GL_INT
247
+ elif internal_format == GL_RG16UI:
248
+ # FIXME This doesn't work, but it should...
249
+ # pyopengl sayz : ValueError: Unrecognised image format: GL_RG_INTEGER
250
+ format, type_ = GL_RG_INTEGER, GL_UNSIGNED_SHORT
251
+ elif internal_format == GL_RGB16UI:
252
+ format, type_ = GL_RGB_INTEGER, GL_UNSIGNED_SHORT
253
+ elif internal_format == GL_RGB32UI:
254
+ format, type_ = GL_RGB_INTEGER, GL_UNSIGNED_INT
255
+ else:
256
+ raise Exception(f"Unsupported format {internal_format}")
257
+
258
+ return format, type_
259
+
260
+ def read_texture2(tex_id, desired_format, width:int = None, height:int = None) -> np.ndarray:
261
+ """ Read a texture `tex_id` out of the GPU and returns it as an array.
262
+
263
+ The desired_fromat is the one you want to get the texture in. Be aware that
264
+ some formats are not supported by python OpenGL (right now, we know of
265
+ RG16UI).
266
+ """
267
+ format, type_ = _get_format_type_from_internal_format(desired_format)
268
+ glBindTexture(GL_TEXTURE_RECTANGLE, tex_id)
269
+
270
+ # No need to know the height/width.
271
+ # npa will be "bytes".
272
+ npa = glGetTexImage (
273
+ GL_TEXTURE_RECTANGLE,
274
+ 0, # mipmap level
275
+ format, #format, // GL will convert to this format
276
+ type_ #type, // Using this data type per-pixel
277
+ )
278
+
279
+ if format == GL_RED and type_ == GL_FLOAT:
280
+ assert width is None and height is None, "For this specific format, I will figure height/width myself."
281
+
282
+ # For some reasons, glGetTexImage returns a transposed array
283
+ # with an additional, useless dimension... I fix that but
284
+ # it seems strange to me.
285
+ s = list(npa.shape)
286
+ s[0], s[1] = s[1], s[0]
287
+
288
+ if len(s) == 3 and s[2] == 1:
289
+ a = np.squeeze(npa,axis=2)
290
+ return a.reshape(tuple(s[0:2]))
291
+ else:
292
+ return npa.reshape(tuple(s))
293
+
294
+ elif format == GL_RED_INTEGER and type_ == GL_UNSIGNED_BYTE:
295
+ assert width is not None and height is not None, "For this specific format, I can't figure height/width myself."
296
+ assert len(npa) == width*height, f"Dimensions {width}*{height} don't match size: {npa.size} elements"
297
+ return np.ndarray( (height, width) , np.uint8, npa)
298
+
299
+ elif format == GL_RGB_INTEGER and type_ in (GL_UNSIGNED_SHORT, GL_UNSIGNED_INT):
300
+ s = list(npa.shape)
301
+ s[0], s[1] = s[1], s[0]
302
+
303
+ if len(s) == 4 and s[2] == 1:
304
+ a = np.squeeze(npa,axis=2)
305
+ return a.reshape((s[0], s[1], 3))
306
+ else:
307
+ return npa.reshape(tuple(s))
308
+ return np.ndarray( (height, width, 3) , np.uint16, npa)
309
+
310
+ elif format == GL_RGBA and type_ == GL_FLOAT:
311
+ assert width is None and height is None, "For this specific format, I will figure height/width myself."
312
+ s = list(npa.shape)
313
+ assert len(s) == 4 and s[2] == 1
314
+ a = np.squeeze(npa,axis=2)
315
+ return a.reshape( (s[1], s[0], s[3]) )
316
+
317
+ elif format == GL_RGBA_INTEGER and type_ == GL_UNSIGNED_INT:
318
+ #assert width is None and height is None, "For this specific format, I will figure height/width myself."
319
+ s = list(npa.shape)
320
+ assert len(s) == 4 and s[2] == 1
321
+ a = np.squeeze(npa,axis=2)
322
+ return a.reshape( (s[1], s[0], s[3]) )
323
+
324
+ else:
325
+ raise Exception(f"Unsupported format/type combination: {format} - {type_}")
326
+
327
+
328
+ def read_texture(frame_buffer_id, color_attachment_ndx, width, height, internal_format):
329
+ """
330
+ DEPRECATED Use the version 2 (this one needs framebuffers which is painful to manage).
331
+
332
+ Read a rectangle (0,0,width, height) in a texture of size at least
333
+ (width, height).
334
+
335
+ color_attachment_ndx: either an int or a GL_COLOR_ATTACHMENTx
336
+
337
+ FIXME Check things up with : https://registry.khronos.org/OpenGL-Refpages/gl4/html/glGetFramebufferAttachmentParameter.xhtml
338
+
339
+ FIXME This code is limited as we read from a frame buffer (and not directly
340
+ from a texture id). So one has to provide framebuffer and attachment number
341
+ (which is not quite convenient).
342
+
343
+ The internal format of a texture can be found in the dictioneary `textures_formats`
344
+ FIXME again, this is sub optimal. It'd be nicer to resolve a texture ID
345
+ to its corresponding frame buffer/attachment (but it dosen't make too much
346
+ sense since a texture may be attached to several FB and since I sometimes
347
+ hack the fb/textures directly (so I don't maintaint an FB/attach <-> tex. id correspondance)
348
+ """
349
+ assert frame_buffer_id > 0
350
+
351
+ format, type_ = _get_format_type_from_internal_format(internal_format)
352
+
353
+ # This function exists to codify some of the knowledge I gathered
354
+ # while learning how to download a texture.
355
+
356
+ # The fact is that reading a texture from the GPU is much
357
+ # more difficult than reading a frame buffer color attachment.
358
+
359
+ # Right now we don't actually read from texture. We read from
360
+ # a color attachment. It means that we can only read it
361
+ # when it has the data we need.
362
+
363
+ # Make sure the right buffer is selected
364
+ # GL_FRAMEBUFFER binds framebuffer to both the read and draw framebuffer targets
365
+
366
+ # old_framebuffer_id = glGetIntegerv(GL_FRAMEBUFFER_BINDING)
367
+ # glBindFramebuffer(GL_READ_FRAMEBUFFER, old_framebuffer_id)
368
+
369
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_buffer_id)
370
+ logging.debug(f"Bound FrameBuffer {frame_buffer_id} for read_texture")
371
+ err = glGetError()
372
+ assert (
373
+ err == GL_NO_ERROR
374
+ ), f"GL Error : {err} (0x{err:x}). glBindFramebuffer(GL_READ_FRAMEBUFFER, {frame_buffer_id}) failed."
375
+
376
+ # select a color buffer source for pixels
377
+ if color_attachment_ndx in GL_COLOR_ATTACHMENTS:
378
+ ca = color_attachment_ndx
379
+ else:
380
+ ca = GL_COLOR_ATTACHMENTS[color_attachment_ndx]
381
+ glReadBuffer(ca)
382
+ assert glGetError() == GL_NO_ERROR
383
+
384
+ # GL_FLOAT = np.float32
385
+ t = glReadPixels(0, 0, width, height, format, type_)
386
+ assert glGetError() == GL_NO_ERROR
387
+
388
+ if format == GL_RGB and type_ == GL_FLOAT:
389
+ return t.reshape(height, width, 3)
390
+ if format == GL_RGBA and type_ == GL_FLOAT:
391
+ return t.reshape(height, width, 4)
392
+ elif format == GL_RED and type_ == GL_FLOAT:
393
+ return t.reshape(height, width)
394
+ elif format == GL_RED_INTEGER and type_ == GL_INT:
395
+ return t.reshape(height, width)
396
+ elif format == GL_RED_INTEGER and type_ == GL_UNSIGNED_INT:
397
+ return t.reshape(height, width)
398
+ elif format == GL_RED_INTEGER and type_ == GL_UNSIGNED_BYTE:
399
+ # For some reason, PyOpenGL doesn't return a numpy array here but
400
+ # `bytes`. So I have to do some extra step to cast the texture into a
401
+ # numpy array myself.
402
+ return np.frombuffer(t, np.uint8).reshape(height, width)
403
+ else:
404
+ raise Exception(
405
+ f"Unsupported format/type combination: {internal_format} - {type_}"
406
+ )
407
+
408
+ def upload_geometry_to_vao(triangles: np.ndarray, attr_loc: int = 0, normalized: bool = True):
409
+ """
410
+ Geometry is a n rows * [x,y,z] columns matrix representing the vertices,
411
+ each having x,y,z coordinates (NDC coordinates, that is, each in [-1,+1].
412
+
413
+ The vertices will be wired to the vertex attribute `attr_loc`
414
+
415
+ VAO only stores info about buffers (not transformation, not parameters,
416
+ etc.)
417
+
418
+ Returns a Vertex Array Object.
419
+ """
420
+
421
+ assert triangles.dtype == np.float32
422
+ vao_id = glGenVertexArrays(1)
423
+ glBindVertexArray(vao_id)
424
+
425
+ vertex_buffer_object_id = glGenBuffers(1)
426
+ glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object_id)
427
+ # OpenGL doc: creates and initializes a buffer object's data store
428
+ glBufferData(GL_ARRAY_BUFFER, triangles.nbytes, triangles.ravel(), GL_STATIC_DRAW)
429
+
430
+ # print(f"Size: {triangles.nbytes} bytes")
431
+
432
+ # NOTE You don't need to glUseProgram() before; this is just an
433
+ # attribute number.
434
+ # Array access is enabled by binding the VAO in question and calling
435
+ glEnableVertexAttribArray(attr_loc)
436
+
437
+ # Tell OpenGL we want the attribute 0 to be enabled and wired to our vertices coordinates.
438
+ # In the vertex shader, we'll have to do something like:
439
+ # layout(location = 0) in vec4 my_vertex;
440
+ # to connect that "attribute 0" to the shader.
441
+
442
+ if normalized:
443
+ gl_norm = GL_TRUE
444
+ else:
445
+ gl_norm = GL_FALSE
446
+
447
+ glVertexAttribPointer(attr_loc, 3, GL_FLOAT, gl_norm, 0, None)
448
+
449
+ # order important else you clear the buffer's bind located in
450
+ # the vertex array object :-)
451
+ glBindVertexArray(GL_NONE)
452
+ glBindBuffer(GL_ARRAY_BUFFER, GL_NONE)
453
+ return vao_id
454
+
455
+
456
+ def make_quad(xmin, xmax, ymin, ymax):
457
+ # print(f"{xmin:.2f}-{xmax:.2f}: {ymin:.2f}-{ymax:.2f}")
458
+ quad_vertices = [
459
+ (xmax, ymin, 0.5),
460
+ (xmax, ymax, 0.5),
461
+ (xmin, ymax, 0.5),
462
+ (xmin, ymin, 0.5),
463
+ ]
464
+
465
+ quad_vertex_triangles = [(0, 1, 2), (0, 2, 3)]
466
+
467
+ quad_normals = [(0.000000, 0.000000, 1.000000), (0.000000, 0.000000, 1.000000)]
468
+
469
+ quad_texcoords = [(1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]
470
+
471
+ quad_texture_triangles = [(0, 1, 2), (0, 2, 3)]
472
+
473
+ quad_normal_triangles = [(1, 1, 1), (1, 1, 1)]
474
+
475
+ np_quad_vertices = np.array(
476
+ [quad_vertices[index] for indices in quad_vertex_triangles for index in indices]
477
+ )
478
+
479
+ np_quad_normals = np.array(
480
+ [quad_normals[index] for indices in quad_normal_triangles for index in indices]
481
+ )
482
+
483
+ np_quad_texcoords = np.array(
484
+ [
485
+ quad_texcoords[index]
486
+ for indices in quad_texture_triangles
487
+ for index in indices
488
+ ]
489
+ )
490
+
491
+ # Texture coordintaes remain in floats
492
+ np_quad_texcoords_rectangle = np_quad_texcoords
493
+ return np_quad_vertices, np_quad_texcoords_rectangle
494
+
495
+
496
+ def make_unit_quad(texture_width, texture_height):
497
+ return make_quad(-1.0, 1.0, -1.0, 1.0)
498
+
499
+
500
+ def query_gl_caps():
501
+ """ Query the GPU for its capabilities. """
502
+ MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB = (
503
+ 35881 # useful for geometry shader limitations; see https://community.khronos.org/t/textures-in-the-geometry-shader/75766/3
504
+ )
505
+
506
+ from OpenGL.GL import (
507
+ GL_MAX_COMPUTE_WORK_GROUP_SIZE,
508
+ glGetIntegeri_v,
509
+ glGetInteger,
510
+ GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS,
511
+ GL_MAX_COMPUTE_WORK_GROUP_COUNT,
512
+ GL_MAX_COLOR_ATTACHMENTS,
513
+ GL_MAX_DRAW_BUFFERS,
514
+ GL_MAX_TEXTURE_IMAGE_UNITS,
515
+ GL_MAX_VERTEX_UNIFORM_VECTORS,
516
+ GL_MAX_FRAGMENT_UNIFORM_VECTORS,
517
+ GL_MAX_TEXTURE_SIZE,
518
+ GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS,
519
+ GL_MAX_SHADER_STORAGE_BLOCK_SIZE,
520
+ GL_MAX_TEXTURE_BUFFER_SIZE,
521
+ )
522
+ from math import sqrt
523
+
524
+ logging.info(
525
+ f"OpenGl version: {glGetIntegerv(GL_MAJOR_VERSION)}.{glGetIntegerv(GL_MINOR_VERSION)}; {glGetString(GL_VERSION)}; {glGetString(GL_VENDOR)} -- GL/SL:{glGetString(GL_SHADING_LANGUAGE_VERSION) }; MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB={glGetInteger(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB)} max viewport={glGetIntegerv(GL_MAX_VIEWPORT_DIMS)}"
526
+ )
527
+ # Maximum dimension of a texture => maximum size of the computation domain.
528
+ # In cas you wonder, it's pretty difficult (and totally out of the OpenGL spec) to
529
+ # know how much memory there is on the GPU. That's because memory is swapped there
530
+ # too, just like in any OS. And therefore it's also tricky (impossible ?) to know which texture
531
+ # is in GPU RAM or swapped out...
532
+ logging.info("Texture info:")
533
+ logging.info(
534
+ f"GL_MAX_TEXTURE_SIZE = {glGetIntegerv(GL_MAX_TEXTURE_SIZE)}x{glGetIntegerv(GL_MAX_TEXTURE_SIZE)} texels"
535
+ )
536
+ max_buffer_size = glGetInteger(
537
+ GL_MAX_TEXTURE_BUFFER_SIZE
538
+ ) # In texels ! See: https://www.khronos.org/opengl/wiki/Buffer_Texture
539
+ logging.info(
540
+ f"GL_MAX_TEXTURE_BUFFER_SIZE = {max_buffer_size / (1024**2):.1f} mega-texels"
541
+ )
542
+ d = int(sqrt(max_buffer_size))
543
+ logging.info(f"Texture buffer max square size given memory limit.: {d}x{d} texels")
544
+
545
+ logging.info("SSBO info:")
546
+ max_ssbo_size = glGetInteger(GL_MAX_SHADER_STORAGE_BLOCK_SIZE)
547
+ logging.info(
548
+ f"GL_MAX_SHADER_STORAGE_BLOCK_SIZE = {max_ssbo_size / (1024**2):.1f} Mbytes"
549
+ )
550
+ d = int(sqrt(max_ssbo_size / 16))
551
+ logging.info(f"SSBO max square size: {d}x{d} RGBAf32 elements")
552
+ # logging.info(f"If one float per buffer SSBO max square size: {d}x{d} RGBAf32 elements")
553
+
554
+ logging.info(
555
+ f"GL_MAX_COMPUTE_WORK_GROUP_COUNT = x:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT,0)[0]} y:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT,0)[0]}"
556
+ )
557
+ logging.info(
558
+ f"GL_MAX_COMPUTE_WORK_GROUP_SIZE = x:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE,0)[0]} y:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE,1)[0]} (== max tile size)"
559
+ )
560
+ logging.info(
561
+ f"GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS = { glGetInteger(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS)}"
562
+ )
563
+
564
+ logging.info(
565
+ f"GL_MAX_COLOR_ATTACHMENTS = { glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS)}"
566
+ )
567
+ logging.info(f"GL_MAX_DRAW_BUFFERS = { glGetIntegerv(GL_MAX_DRAW_BUFFERS)}")
568
+ logging.info(
569
+ f"GL_MAX_TEXTURE_IMAGE_UNITS = { glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS)}"
570
+ )
571
+ logging.info(
572
+ f"GL_MAX_VERTEX_UNIFORM_VECTORS = { glGetIntegerv(GL_MAX_VERTEX_UNIFORM_VECTORS)}"
573
+ )
574
+ logging.info(
575
+ f"GL_MAX_FRAGMENT_UNIFORM_VECTORS = { glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_VECTORS)}"
576
+ )
577
+
578
+ logging.info(
579
+ f"GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS = { glGetIntegerv(GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS)}"
580
+ )
581
+
582
+ # I don't think we use these
583
+ # logging.debug(f"GL_MAX_ARRAY_TEXTURE_LAYERS = { glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS)}")
584
+
585
+
586
+
587
+ class GL_cache_tools():
588
+ """ This class helps to upload shaders and textures in the GPU. """
589
+
590
+ samplers_in_shader: dict[tuple[int, str], str]
591
+ shaders_programs: dict[int, int] = dict() # program id -> shaders
592
+ shaders_names: dict[int, str] = dict() # shader_id -> string
593
+
594
+ def __init__(self) -> None:
595
+
596
+ # Recall the type of a sampler associated to a shader.
597
+ # So maps (shader id, sampler's name) to sampler's type name.
598
+ self.samplers_in_shader = dict()
599
+
600
+ self.all_textures_sizes = dict()
601
+
602
+ # Maps texture id to (context, texture internal format)
603
+ # Texture interanl format are loike: GL_RGB32F...
604
+ self.textures_formats = dict()
605
+
606
+ self._set_uniform_cache = dict()
607
+
608
+ # def gl_clear_all_caches(self):
609
+ # self.samplers_in_shader.clear()
610
+ # self.shaders_programs.clear()
611
+ # self.shaders_names.clear()
612
+ # self.all_textures_sizes.clear()
613
+ # self.textures_formats.clear()
614
+ # self._set_uniform_cache.clear()
615
+
616
+ # def clear_uniform_cache(self):
617
+ # self._set_uniform_cache.clear()
618
+
619
+ def describe_program(self, pid:int):
620
+ return ",".join([self.shaders_names[sid] for sid in self.shaders_programs[pid]])
621
+
622
+
623
+ def load_shader_from_source(self, shader_type, source: str) -> int:
624
+ assert shader_type in (
625
+ GL_VERTEX_SHADER,
626
+ GL_FRAGMENT_SHADER,
627
+ GL_GEOMETRY_SHADER,
628
+ GL_COMPUTE_SHADER,
629
+ )
630
+
631
+ # OpenGL will silently do nothing if you don't activate the extension
632
+ # which is pretty tough to debug.
633
+ # https://www.khronos.org/opengl/wiki/GL_EXT_texture_integer
634
+
635
+ gl_ext_texture_integer = (
636
+ "GL_EXT_gpu_shader4" in source
637
+ or "#extension GL_ARB_texture_rectangle" in source
638
+ )
639
+ for s in ["usampler2DRect", "isampler2DRect", "sampler2DRect"]:
640
+ if s in source:
641
+ assert (
642
+ gl_ext_texture_integer
643
+ ), f"To use {s} you need the extension GL_EXT_gpu_shader4"
644
+
645
+ shader_id: int = glCreateShader(shader_type) # type: ignore
646
+
647
+ if shader_id == 0:
648
+ raise Exception("Shader loading failed")
649
+ return 0
650
+
651
+ glShaderSource(shader_id, source)
652
+ glCompileShader(shader_id)
653
+
654
+ if glGetShaderiv(shader_id, GL_COMPILE_STATUS, None) == GL_FALSE:
655
+ info_log = glGetShaderInfoLog(shader_id)
656
+ logging.error(info_log.decode("ASCII"))
657
+ try:
658
+ glDeleteProgram(shader_id)
659
+ except:
660
+ pass
661
+ finally:
662
+ raise Exception(f"Unable to load shader. {info_log.decode('ASCII')}")
663
+
664
+ # Keep track of types.
665
+ for line in source.split("\n"):
666
+ m = TEX_SAMPLERS_RE.match(line)
667
+ if m:
668
+ type_name = m.groups()[0]
669
+ sampler_name = m.groups()[1]
670
+ logging.debug(
671
+ f"Load shader: (shader:{shader_id}, sampler name:{sampler_name}) -> type={type_name}"
672
+ )
673
+ self.samplers_in_shader[(shader_id, sampler_name)] = type_name
674
+
675
+ self.shaders_names[shader_id] = "from source"
676
+ return shader_id
677
+
678
+
679
+
680
+ def track_texture_size(self, tex_id:int, img_data, w:int, h:int, format) -> int:
681
+ if isinstance(img_data, np.ndarray):
682
+ s = img_data.nbytes
683
+ else:
684
+ s = ctypes.sizeof(img_data)
685
+
686
+ logging.debug(
687
+ f"Uploaded {w}x{h} texels, {s} bytes ({s/(1024**2):.1f} MB) to GPU, format={format}"
688
+ )
689
+ self.all_textures_sizes[tex_id] = s
690
+ return s
691
+
692
+ def total_textures_size(self) -> int:
693
+ """ Return the total size of all textures in bytes. """
694
+ s = []
695
+ for tex_id, size in self.all_textures_sizes.items():
696
+ s.append( size )
697
+ return sum(s)
698
+
699
+
700
+ def drop_textures(self, texture_ids: Union[list[int], int]):
701
+ """ Drop one or more textures. Expect texture id's.
702
+ """
703
+ assert texture_ids is not None
704
+ if not isinstance(texture_ids, list):
705
+ texture_ids = [texture_ids]
706
+
707
+ # In some rare occurences, we reuse twice the same
708
+ # texture.
709
+ texture_ids = list(set(texture_ids))
710
+ for tid in texture_ids:
711
+ assert tid in self.textures_formats, f"Never seen that texture `{tid}` before"
712
+ del self.textures_formats[tid]
713
+
714
+ assert glGetError() == GL_NONE
715
+ #print(f"deleteing {texture_ids}")
716
+ glDeleteTextures(texture_ids)
717
+
718
+
719
+ def upload_np_array_to_gpu(self,
720
+ context,
721
+ format,
722
+ img_data: np.ndarray,
723
+ texture_id: Union[int, None] = None
724
+ ) -> int:
725
+ """The goal of this function is to standardize textures
726
+ configuration and upload to GPU. We trade generality for
727
+ ease of use.
728
+
729
+ If you pass in a texture_id, then the texture will be updated
730
+ instead of created.
731
+
732
+ Returns the texture OpenGL id.
733
+ """
734
+
735
+ assert context in (GL_TEXTURE_RECTANGLE, GL_TEXTURE_2D)
736
+ assert isinstance(img_data, np.ndarray)
737
+
738
+ # From : https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
739
+
740
+ # "The first element corresponds to the lower left corner of the texture
741
+ # image. Subsequent elements progress left-to-right through the remaining
742
+ # texels in the lowest row of the texture image, and then in successively
743
+ # higher rows of the texture image. The final element corresponds to the
744
+ # upper right corner of the texture image. "
745
+
746
+ assert img_data.flags["C"], "I believe pyOpenGL prefer C-contiguous data array"
747
+
748
+ if texture_id is None:
749
+ new_texture = True
750
+ texture_id = glGenTextures(1) # Name one new texture
751
+ # if texture_id == 10:
752
+ # print("**********************************************")
753
+ # print_stack()
754
+ #assert texture_id not in textures_formats, f"I just generated a texture ID that already exists in my database ({textures_formats[texture_id]}) ??? Maybe you need to clear the DB first ?"
755
+ self.textures_formats[texture_id] = (context, format)
756
+ else:
757
+ assert (
758
+ texture_id in self.textures_formats
759
+ ), "Can't update a texture I have never seen before"
760
+ assert self.textures_formats[texture_id] == (
761
+ context,
762
+ format,
763
+ ), "You're changing the nature of the texture"
764
+ new_texture = False
765
+
766
+ glBindTexture(
767
+ context, texture_id
768
+ ) # Bind the texture to context target (kind of assocaiting it to a type)
769
+
770
+ # Prevent texture interpolation with samplers
771
+ glTexParameteri(context, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
772
+ glTexParameteri(context, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
773
+ # GL_CLAMP_TO_EDGE: Clamps the coordinates between 0 and 1. The result is
774
+ # that higher coordinates become clamped to the edge, resulting in a
775
+ # stretched edge pattern.
776
+ glTexParameteri(context, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
777
+ glTexParameteri(context, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
778
+
779
+ if format == GL_R32F:
780
+ assert len(img_data.shape) == 2, "We only accept 2D textures for GL_R32F format"
781
+ assert img_data.dtype in (
782
+ float,
783
+ np.float32,
784
+ ), f"We only accept floats, you gave {img_data.dtype}"
785
+ h, w = img_data.shape
786
+ # internal format; format; type.
787
+ glTexImage2D(context, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, img_data)
788
+ elif format == GL_RGB32F:
789
+ assert (
790
+ len(img_data.shape) == 3 and img_data.shape[2] == 3
791
+ ), "We only accept 2D RGB textures, shape=(h,w,3) for GL_RGB32F format"
792
+ assert img_data.dtype in (float, np.float32)
793
+ h, w, _ = img_data.shape
794
+ glTexImage2D(context, 0, GL_RGB32F, w, h, 0, GL_RGB, GL_FLOAT, img_data)
795
+ elif format == GL_RG16UI:
796
+ assert (
797
+ len(img_data.shape) == 3 and img_data.shape[2] == 2
798
+ ), "We only accept 2D RG textures, shape=(h,w,2) for GL_RG16UI format"
799
+ assert img_data.dtype == np.uint16
800
+ h, w, _ = img_data.shape
801
+ # FIXME Why do I need to suffix GL_RG with INTEGER (I don't do it elsewhere
802
+ # but here it is mandatory)
803
+ glTexImage2D(context, 0, GL_RG16UI, w, h, 0, GL_RG_INTEGER, GL_UNSIGNED_SHORT, img_data)
804
+ elif format == GL_RGB16UI:
805
+ assert (
806
+ len(img_data.shape) == 3 and img_data.shape[2] == 3
807
+ ), f"We only accept 2D RGB textures, shape=(h,w,3) for GL_RGB16UI format. You gave: {img_data.shape}"
808
+ assert img_data.dtype == np.uint16, "Expecting unsigned short"
809
+ h, w, _ = img_data.shape
810
+ glTexImage2D(context, 0, GL_RGB16UI, w, h, 0, GL_RGB_INTEGER, GL_UNSIGNED_SHORT, img_data)
811
+ elif format == GL_RGB32UI:
812
+ assert (
813
+ len(img_data.shape) == 3 and img_data.shape[2] == 3
814
+ ), f"We only accept 2D RGB textures, shape=(h,w,3) for GL_RGB16UI format. You gave: {img_data.shape}"
815
+ assert img_data.dtype == np.uint32, f"Expecting unsigned int, got {img_data.dtype}"
816
+ h, w, _ = img_data.shape
817
+ glTexImage2D(context, 0, GL_RGB32UI, w, h, 0, GL_RGB_INTEGER, GL_UNSIGNED_INT, img_data)
818
+ elif format == GL_RGBA32F:
819
+ assert (
820
+ len(img_data.shape) == 3 and img_data.shape[2] == 4
821
+ ), "We only accept 2D RGBA textures, shape=(h,w,3) for GL_RGBA32F format"
822
+ assert img_data.dtype in (float, np.float32)
823
+ h, w, _ = img_data.shape
824
+ glTexImage2D(context, 0, GL_RGBA32F, w, h, 0, GL_RGBA, GL_FLOAT, img_data)
825
+ elif format == GL_RGBA32UI:
826
+ assert (
827
+ len(img_data.shape) == 3 and img_data.shape[2] == 4
828
+ ), f"We only accept 2D RGBA textures, shape=(h,w,3) for GL_RGBA32UI format, you gave {img_data.shape}"
829
+ assert img_data.dtype in (np.uint32, )
830
+ h, w, _ = img_data.shape
831
+ glTexImage2D(context, 0, GL_RGBA32UI, w, h, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, img_data)
832
+ elif format == GL_R32UI:
833
+ assert (
834
+ len(img_data.shape) == 2
835
+ ), "We only accept 2D textures for GL_R32UI format"
836
+ assert (
837
+ img_data.dtype == np.uint32
838
+ ), f"We only accept uint32, you gave {img_data.dtype}"
839
+ h, w = img_data.shape
840
+ # See https://stackoverflow.com/questions/59542891/opengl-integer-texture-raising-gl-invalid-value
841
+ # GL_RED_INTEGER is for unnormalized values (such as uint's)
842
+ glTexImage2D(
843
+ context, 0, GL_R32UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, img_data
844
+ )
845
+ elif format == GL_R8UI:
846
+ assert (
847
+ len(img_data.shape) == 2
848
+ ), "We only accept 2D textures for GL_R32UI format"
849
+ assert (
850
+ img_data.dtype == np.uint8
851
+ ), f"We only accept uint8, you gave {img_data.dtype}"
852
+ h, w = img_data.shape
853
+ glTexImage2D(
854
+ context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, img_data
855
+ )
856
+ elif format == GL_R32I:
857
+ assert len(img_data.shape) == 2, "We only accept 2D textures for GL_R32I format"
858
+ assert (
859
+ img_data.dtype == np.int32
860
+ ), f"We only accept int32, you gave {img_data.dtype}"
861
+ h, w = img_data.shape
862
+ glTexImage2D(context, 0, GL_R32I, w, h, 0, GL_RED_INTEGER, GL_INT, img_data)
863
+ else:
864
+ raise Exception(f"Unsupported format {format}")
865
+
866
+ check_gl_error()
867
+ self.track_texture_size(texture_id, img_data, w, h, format)
868
+ return texture_id
869
+
870
+
871
+ def upload_blank_texture_to_gpu(self, w: int, h: int, context, format=GL_R32F, value=0.0):
872
+ """
873
+ Make and upload a blank (or one color) texture to the GPU.
874
+
875
+ `format`: some texture format. The way it is done here means that
876
+ we will derive the texture format in the GPU as well as the texture
877
+ format of the "value" data.
878
+ `context`: either GL_TEXTURE_RECTANGLE, GL_TEXTURE_2D
879
+ `value`: will be set on each components of the texels.
880
+ """
881
+ # FIXME Wire this to upload_np_array_to_gpu(...)
882
+
883
+ assert context in (GL_TEXTURE_RECTANGLE, GL_TEXTURE_2D)
884
+
885
+ texture_id = glGenTextures(1) # Name one new texture
886
+ # if texture_id == 10:
887
+ # print("**********************************************")
888
+ # print_stack()
889
+ #assert texture_id not in textures_formats, "A brand new texture can't already exist!"
890
+ self.textures_formats[texture_id] = (context, format)
891
+ glBindTexture(
892
+ context, texture_id
893
+ ) # Bind the texture to context target (kind of assocaiting it to a type)
894
+ glTexParameteri(context, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
895
+ glTexParameteri(context, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
896
+ glTexParameteri(context, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
897
+ glTexParameteri(context, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
898
+
899
+ #logging.debug(f"Uploading {w}x{h} constant pixels to GPU. Format= {format}")
900
+
901
+ if format in (GL_R32UI, GL_R8UI, GL_RGBA8) and value == 0.0:
902
+ # Quality of life when leaving value to its default.
903
+ value = 0
904
+
905
+ if format == GL_R32F:
906
+ img_data = (ctypes.c_float * (w * h * 1))(*([value] * (w * h * 1)))
907
+ glTexImage2D(context, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, img_data)
908
+ elif format == GL_RG32F:
909
+ img_data = (ctypes.c_float * (w * h * 2))(*([value] * (w * h * 2)))
910
+ glTexImage2D(context, 0, GL_RG32F, w, h, 0, GL_RG, GL_FLOAT, img_data)
911
+ elif format == GL_RGB32F:
912
+ if type(value) == list:
913
+ value: list
914
+ assert len(value) == 3, "I expect three components for R,G,B"
915
+ img_data = (ctypes.c_float * (w * h * 3))(*(value * (w * h)))
916
+ else:
917
+ # img_data = (ctypes.c_float * (w * h * 3))(*([value] * (w*h*3)))
918
+ img_data = np.full((w * h * 3,), value, dtype=np.float32)
919
+ glTexImage2D(context, 0, GL_RGB32F, w, h, 0, GL_RGB, GL_FLOAT, img_data)
920
+ elif format == GL_RGBA32F:
921
+ if type(value) == list:
922
+ value: list
923
+ assert len(value) == 4, "I expect three components for R,G,B"
924
+ img_data = (ctypes.c_float * (w * h * 4))(*(value * (w * h)))
925
+ else:
926
+ # img_data = (ctypes.c_float * (w * h * 4))(*([value] * (w*h*4)))
927
+ img_data = np.full((w * h * 4,), value, dtype=np.float32)
928
+
929
+ glTexImage2D(context, 0, GL_RGBA32F, w, h, 0, GL_RGBA, GL_FLOAT, img_data)
930
+ elif format == GL_RGBA8:
931
+ if type(value) == list:
932
+ value: list
933
+ assert len(value) == 4, "I expect three components for R,G,B"
934
+ img_data = (ctypes.c_uint8 * (w * h * 4))(*(value * (w * h)))
935
+ else:
936
+ assert type(value) in (
937
+ np.uint8,
938
+ int,
939
+ ), f"I expect unsigned 8 bits integer, you gave {type(value)}"
940
+ assert 0 <= value <= 255
941
+ img_data = (ctypes.c_uint8 * (w * h * 4))(*([value] * (w * h * 4)))
942
+ # glTexImage2D( ctx, "base internal formats" (see Table2 and Table1 of https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml )==format of the texture in the GPU memory,
943
+ # w,h,0, format=format of the source pixels data, typz=data type of the source pixels data)
944
+ glTexImage2D(context, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data)
945
+ elif format == GL_R32UI:
946
+ assert type(value) == int and value >= 0
947
+ img_data = (ctypes.c_uint32 * (w * h * 1))(*([value] * (w * h * 1)))
948
+ # internal format; format (of pixel data); (data) type (of pixel data)
949
+ glTexImage2D(
950
+ context, 0, GL_R32UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, img_data
951
+ )
952
+ elif format == GL_R8UI:
953
+ logging.debug("Creataing data")
954
+ assert type(value) in (
955
+ np.uint8,
956
+ int,
957
+ ), f"I expect unsigned 8 bits integer, you gave {type(value)}"
958
+ assert 0 <= value <= 255
959
+
960
+ # I tried to allocate memory in an 4-bytes aligned way
961
+ # but it doesn't work => I use glPixelStore
962
+ img_data = np.full((h * w,), value, dtype=np.uint8)
963
+ logging.debug("Uploading data")
964
+ pixels_align_old = glGetInteger(GL_UNPACK_ALIGNMENT)
965
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
966
+ glTexImage2D(
967
+ context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, img_data
968
+ )
969
+ glPixelStorei(GL_UNPACK_ALIGNMENT, pixels_align_old) # Restore old value
970
+ logging.debug("Uploaded data")
971
+
972
+ # size = w*h
973
+ # logging.debug(f"memory_aligned_byte_array size={size} value={value}")
974
+ # assert ctypes.alignment(ctypes.c_uint32) == 4
975
+ # assert ctypes.alignment(ctypes.c_uint32) == glGetInteger(GL_UNPACK_ALIGNMENT), "We must align on OpenGL alignment"
976
+ # v = value << 24 + value << 16 + value << 8 + value
977
+ # padded_len = (size + 3) // 4
978
+ # img_data = (ctypes.c_uint32 * padded_len)(*([v] * padded_len))
979
+ # assert ctypes.addressof(img_data) % 4 == 0
980
+ # img_data2 = (ctypes.c_uint8 * size).from_buffer(img_data)
981
+ # logging.debug("memory_aligned_byte_array - done")
982
+ # img_data2 = np.frombuffer(img_data2, dtype=np.uint8)
983
+ # glTexImage2D(context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, img_data2)
984
+
985
+ elif format == GL_R32I:
986
+ assert type(value) == int
987
+ # img_data = (ctypes.c_int32 * (w * h * 1))(*([value] * (w*h*1)))
988
+ img_data = np.full((h * w,), value, dtype=np.int32)
989
+ # internal format; format (of pixel data); (data) type (of pixel data)
990
+ # glTexImage2D(context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, img_data)
991
+ glTexImage2D(context, 0, GL_R32I, w, h, 0, GL_RED_INTEGER, GL_INT, img_data)
992
+ else:
993
+ raise Exception(f"Unsupported texture format : {format}")
994
+
995
+ check_gl_error()
996
+ self.track_texture_size(texture_id, img_data, w, h, format)
997
+
998
+ return texture_id
999
+
1000
+
1001
+ def clear_texture(self, texture_id:int):
1002
+ assert (
1003
+ texture_id in self.textures_formats
1004
+ ), "I can't find your texture_id (textures_formats) in the textures I have created..."
1005
+ context_, internal_format = self.textures_formats[texture_id]
1006
+
1007
+ if internal_format == GL_R32F:
1008
+ format, type_ = GL_RED, GL_FLOAT
1009
+ elif internal_format == GL_RGB32F:
1010
+ format, type_ = GL_RGB, GL_FLOAT
1011
+ elif internal_format == GL_RGBA32F:
1012
+ format, type_ = GL_RGBA, GL_FLOAT
1013
+ elif internal_format == GL_R32UI:
1014
+ format, type_ = GL_RED_INTEGER, GL_UNSIGNED_INT
1015
+ # See https://stackoverflow.com/questions/59542891/opengl-integer-texture-raising-gl-invalid-value
1016
+ # GL_RED_INTEGER is for unnormalized values (such as uint's)
1017
+ elif internal_format == GL_R8UI:
1018
+ format, type_ = GL_RED_INTEGER, GL_UNSIGNED_BYTE
1019
+ elif internal_format == GL_R32I:
1020
+ format, type_ = GL_RED_INTEGER, GL_INT
1021
+ else:
1022
+ raise Exception(f"Unsupported format {internal_format}")
1023
+
1024
+ if type_ == GL_FLOAT:
1025
+ ct = ctypes.c_float
1026
+ elif type_ == GL_INT:
1027
+ ct = ctypes.c_int32
1028
+ elif type_ == GL_UNSIGNED_BYTE:
1029
+ ct = ctypes.c_uint8
1030
+ else:
1031
+ raise Exception("Unsupported type : {}", type_)
1032
+
1033
+ if format in (GL_RED, GL_RED_INTEGER):
1034
+ size = 1
1035
+ elif format == GL_RGB:
1036
+ size = 3
1037
+ elif format == GL_RGBA:
1038
+ size = 4
1039
+ else:
1040
+ raise Exception("Unsupported format : {}", format)
1041
+
1042
+ img_data = (ct * size)(*([0] * size))
1043
+ glClearTexImage(texture_id, 0, format, type_, img_data)
1044
+
1045
+
1046
+
1047
+ def load_shader_from_file(self, fpath: Path, log_path: Path = None, shader_type=None):
1048
+ """ Load a shader from a file - can contain %%includes.
1049
+ The extension of the file determines the type of shader :
1050
+ - .vs : vertex shader
1051
+ - .frs : fragment shader
1052
+ - .gs : geometry shader
1053
+ - .comp : compute shader (.cs is deprecated)
1054
+ """
1055
+ shader_dir = fpath.parent
1056
+ with open(fpath, "r") as source:
1057
+
1058
+ if shader_type is None:
1059
+ suffix = fpath.suffix
1060
+ if suffix == ".vs":
1061
+ shader_type = GL_VERTEX_SHADER
1062
+ elif suffix == ".frs":
1063
+ shader_type = GL_FRAGMENT_SHADER
1064
+ elif suffix == ".gs":
1065
+ shader_type = GL_GEOMETRY_SHADER
1066
+ elif suffix in (".comp", ".cs"):
1067
+ if suffix == ".cs":
1068
+ logging.warning(".cs extension is deprecated")
1069
+
1070
+ shader_type = GL_COMPUTE_SHADER
1071
+ else:
1072
+ raise Exception(f"Unrecognized shader extension {suffix}")
1073
+ else:
1074
+ assert shader_type in [GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER, GL_COMPUTE_SHADER], f"Unknown shader type {shader_type}"
1075
+
1076
+ try:
1077
+ # print(f"Loading {fpath} as {shader_type}")
1078
+ INCLUDE_MARK = "%%include"
1079
+ source_lines = []
1080
+ for line in source.readlines():
1081
+ if line.strip().startswith(INCLUDE_MARK):
1082
+ fn = shader_dir / line.replace(INCLUDE_MARK, "").strip()
1083
+ logging.debug(f"Including {fn} in {fpath}")
1084
+ source_lines.extend([f"\n// Included from {fn}\n\n"])
1085
+ with open(fn) as include:
1086
+ source_lines.extend(include.readlines())
1087
+ else:
1088
+ source_lines.append(line)
1089
+
1090
+ full_text = "".join(source_lines)
1091
+
1092
+ logging.debug(f"Loaded {fpath}")
1093
+ if log_path:
1094
+ logged_path = f"{log_path / fpath.name}_log"
1095
+ with open(logged_path, "w") as logged:
1096
+ logged.write(full_text)
1097
+ else:
1098
+ logged_path = None
1099
+
1100
+ shader_id = self.load_shader_from_source(shader_type, full_text)
1101
+ assert shader_dir not in self.shaders_names, "The shader is new, so it must be unknown to us"
1102
+ self.shaders_names[shader_id] = fpath.name
1103
+ return shader_id
1104
+ except Exception as ex:
1105
+ if logged_path is not None:
1106
+ lm = f"Log file at {logged_path}"
1107
+ else:
1108
+ lm = ""
1109
+ raise Exception(f"Error while loading {fpath}. {lm}") from ex
1110
+
1111
+
1112
+ def create_frame_buffer_for_computation(self,
1113
+ destination_texture:Union[int,list[int]],
1114
+ out_to_fragdata:bool=False,
1115
+ depth_buffer:bool=False,
1116
+ texture_target=GL_TEXTURE_RECTANGLE,
1117
+ ):
1118
+ """
1119
+ destination_texture: a texture or a list of textures. If list of textures,
1120
+ one frame buffer will be attached to each texture, via
1121
+ COLOR_ATTACHMENT0,1,2,... (in list order)
1122
+
1123
+ out_to_fragdata: if the FB will be used as output of a shader that issues
1124
+ FragData instead of colors. In that case, we create link buffers so that
1125
+ will receive these FragData.
1126
+
1127
+ depth_buffer: attach a depth buffer to the framebuffer. None or pass in the
1128
+ dimensions of the buffer. # FIXME don't pass the dimensions, guess them from
1129
+ the texture.
1130
+
1131
+ """
1132
+
1133
+ # texture is where the result of the render will be
1134
+ # the stencil and depth information will be stored in a
1135
+ # RenderBuffer (which we don't make available here)
1136
+
1137
+ fb = glGenFramebuffers(1)
1138
+ glBindFramebuffer(GL_FRAMEBUFFER, fb)
1139
+
1140
+ # Wire the destination texture(s)
1141
+ if type(destination_texture) == list:
1142
+ assert len(destination_texture) <= len(
1143
+ GL_COLOR_ATTACHMENTS
1144
+ ), "The GL_COLOR_ATTACHMENTS predefined values are not numerous enough !"
1145
+
1146
+ for i in range(len(destination_texture)):
1147
+ assert (
1148
+ destination_texture[i] is not None
1149
+ ), f"The {i+1}th texture in the list of textures is None ?!"
1150
+ assert (
1151
+ destination_texture[i] in self.textures_formats
1152
+ ), f"The {i+1}th texture in the list of textures has not format defined. Was it correctly initialized ?"
1153
+ # We read from the cached texture formats.
1154
+ context, format = self.textures_formats[destination_texture[i]]
1155
+ # glFramebufferTexture2D: attach a texture image to a framebuffer object
1156
+ glFramebufferTexture2D(
1157
+ GL_FRAMEBUFFER,
1158
+ GL_COLOR_ATTACHMENTS[i],
1159
+ context,
1160
+ destination_texture[i],
1161
+ 0,
1162
+ )
1163
+
1164
+ if out_to_fragdata:
1165
+ # FIXME CRITICAL Is this really needed ? When I read:
1166
+ # https://registry.khronos.org/OpenGL-Refpages/gl4/html/glDrawBuffers.xhtml
1167
+ # I get the impression that glDrawBuffers must be called when a shader
1168
+ # is in context (as glDrawBuffers will wire the shader to some buffer).
1169
+ # Therefore doing this here, without some shader context, is useless...
1170
+
1171
+ # Redirect FragData out's of fragment shader to the destination texture
1172
+ # (they're connected via GL_COLOR_ATTACHMENTx)
1173
+ # FragData[0] corresponds to GL_COLOR_ATTACHMENT0 which was set to destination_textures[0] above.
1174
+ # So the buffer we create here is quite small, it's just an array of pointers.
1175
+
1176
+ # FIXME Replace the complicated ctypes code below with the simpler one right below.
1177
+ # The complex expression just pass texture id's to glDrawBuffers
1178
+ # under one or more GL_COLOR_ATTACHMENT0,1,2,...
1179
+ # glDrawBuffers — Specifies a list of color buffers to be drawn into
1180
+ # drawBuffers = gl_ca[0:len(destination_texture)]
1181
+ drawBuffers = (ctypes.c_int32 * len(destination_texture))(
1182
+ *[int(ca) for ca in GL_COLOR_ATTACHMENTS[0 : len(destination_texture)]]
1183
+ )
1184
+ # print(drawBuffers)
1185
+ # print([int(ca) for ca in gl_ca[0:len(destination_texture)]])
1186
+ glDrawBuffers(
1187
+ drawBuffers
1188
+ ) # pyOpenGl figures the count based on drawBuffers array length
1189
+ else:
1190
+ assert destination_texture is not None, "Null texture id ???"
1191
+ assert type(destination_texture) in (
1192
+ np.uintc,
1193
+ int,
1194
+ ), f"I want an integer texture id (you gave '{type(destination_texture)}')"
1195
+ glFramebufferTexture2D(
1196
+ GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture_target, destination_texture, 0
1197
+ )
1198
+
1199
+ if not out_to_fragdata:
1200
+ glDrawBuffers(GL_NONE)
1201
+ # glReadBuffer(GL_NONE) # FIXME Maybe useless
1202
+
1203
+ if depth_buffer:
1204
+ if True:
1205
+ # see http://www.songho.ca/opengl/gl_fbo.html
1206
+ rb_id = glGenRenderbuffers(1) # create one render buffer
1207
+ glBindRenderbuffer(GL_RENDERBUFFER, rb_id)
1208
+ glRenderbufferStorage(
1209
+ GL_RENDERBUFFER, GL_DEPTH_COMPONENT32F, depth_buffer[0], depth_buffer[1]
1210
+ )
1211
+ glBindRenderbuffer(GL_RENDERBUFFER, 0) # unbind
1212
+ # Attach to currently bound F.B.
1213
+ glFramebufferRenderbuffer(
1214
+ GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb_id
1215
+ )
1216
+ else:
1217
+ pass
1218
+ assert glGetError() == GL_NO_ERROR
1219
+
1220
+ assert glGetError() == GL_NO_ERROR
1221
+ # GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT=_C('GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT',0x8CD6)
1222
+ assert glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, f"Framebuffer is not complete, it is 0x{glCheckFramebufferStatus(GL_FRAMEBUFFER):04X}"
1223
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
1224
+
1225
+ return fb
1226
+
1227
+
1228
+ def load_program(self,
1229
+ vertex_shader:int=None,
1230
+ fragment_shader:int=None,
1231
+ geometry_shader:int=None,
1232
+ compute_shader:int=None
1233
+ ):
1234
+ assert compute_shader is not None or vertex_shader is not None
1235
+ # returns the program
1236
+
1237
+ program = glCreateProgram()
1238
+ if program == 0:
1239
+ return 0
1240
+
1241
+ # At this point, shaders are compiled but not linked to each other
1242
+
1243
+ shaders = []
1244
+ if compute_shader is None:
1245
+ glAttachShader(program, vertex_shader)
1246
+ shaders.append(vertex_shader)
1247
+ if geometry_shader is not None:
1248
+ glAttachShader(program, geometry_shader)
1249
+ shaders.append(geometry_shader)
1250
+ if fragment_shader is not None:
1251
+ glAttachShader(program, fragment_shader)
1252
+ shaders.append(fragment_shader)
1253
+ else:
1254
+ # Compute shader
1255
+ glAttachShader(program, compute_shader)
1256
+ shaders.append(compute_shader)
1257
+
1258
+ glLinkProgram(program)
1259
+
1260
+ if glGetProgramiv(program, GL_LINK_STATUS, None) == GL_FALSE:
1261
+ glDeleteProgram(program)
1262
+ raise Exception("Failed to create a rpogram")
1263
+
1264
+ assert glGetError() == GL_NO_ERROR
1265
+ self.shaders_programs[program] = shaders
1266
+
1267
+ # Mark all shaders for removal once their corresponding programs will be
1268
+ # gone (read the documentation of glDeleteShader).
1269
+ # So be sure to do this *after* the shaders have been attached to their
1270
+ # program.
1271
+ for shader in shaders:
1272
+ glDeleteShader(shader)
1273
+
1274
+ return program
1275
+
1276
+
1277
+
1278
+ def set_uniform(self, program:int, name:str, value):
1279
+ # We cache the uniform settings because calls to `glUniform`
1280
+ # are super expensive (like *unbelievably* *expensive*)
1281
+ k = f"{program},{name}"
1282
+ if k in self._set_uniform_cache:
1283
+ current = self._set_uniform_cache[k]
1284
+ if isinstance(value, np.ndarray):
1285
+ if np.allclose(value, current):
1286
+ return
1287
+ elif current == value:
1288
+ return
1289
+ self._set_uniform_cache[k] = value
1290
+
1291
+ try:
1292
+ location = glGetUniformLocation(program, name)
1293
+ except Exception as ex:
1294
+ raise Exception(f"Can't get uniform location for '{name}'") from ex
1295
+
1296
+ if not location >= 0:
1297
+ logging.error(
1298
+ f"Can't find '{name}' uniform in *compiled* GL/SL program {program} (with shaders {[self.shaders_names[sid] for sid in self.shaders_programs[program]]}). Maybe you mixed uniforms and texture samplers ? Remember that shaders' compiler may remove uniforms that are not actually used in the GLSL code !"
1299
+ )
1300
+ return
1301
+
1302
+ # OpenGl: This (glGetUniformLocation) function returns -1 if name does not
1303
+ # correspond to an active uniform variable in program, if name starts with
1304
+ # the reserved prefix "gl_", or if name is associated with an atomic counter
1305
+ # or a named uniform block.
1306
+ assert (
1307
+ location >= 0
1308
+ ), f"Can't find '{name}' uniform in *compiled* GL/SL program. Maybe you mixed uniforms and texture samplers ? Remember that shaders' compiler may remove uniforms that are not actually used in the GLSL code !"
1309
+
1310
+ if type(value) == float:
1311
+ glUniform1f(location, value)
1312
+ elif type(value) == bool:
1313
+ glUniform1ui(location, int(value))
1314
+ elif type(value) in (int, np.intc):
1315
+ try:
1316
+ #logging.debug(f"glUniform1i({name} as {location}, {value})")
1317
+ glUniform1i(location, value)
1318
+ except Exception as ex:
1319
+ logging.error(
1320
+ f"Error while setting integer uniform '{name}' at location '{location}' to value '{value}' (of type {type(value)})"
1321
+ )
1322
+ raise ex
1323
+ elif type(value) == np.uintc: # A 32 bit (uint32 is "at least 32 bits")
1324
+ try:
1325
+ glUniform1ui(location, value)
1326
+ except Exception as ex:
1327
+ logging.error(
1328
+ f"Error while setting integer uniform '{name}' at location '{location}' to value '{value}' (of type {type(value)})"
1329
+ )
1330
+ raise ex
1331
+ elif (
1332
+ type(value) in (tuple, list)
1333
+ and len(value) == 2
1334
+ and type(value[0]) == float
1335
+ and type(value[1]) == float
1336
+ ):
1337
+ glUniform2f(location, value[0], value[1])
1338
+ elif isinstance(value, np.ndarray) and value.shape == (4, 4):
1339
+ # Attention! OpenGL matrices are column-major
1340
+ # At this point of the code we don't make any hypothesis
1341
+ # about the ordering of data in the numpy array => the caller
1342
+ # must ensure the OpenGL order (column major).
1343
+
1344
+ # glUniformMatrix4fv( loc, count, transpose, value)
1345
+ glUniformMatrix4fv(location, 1, GL_FALSE, value) # GL_FALSE= do not transpose
1346
+ else:
1347
+ if type(value) in (tuple, list):
1348
+ raise Exception(f"Error while setting uniform '{name}' at location '{location}' to value '{value}': Unsupported type: {type(value)} or types of parts are not supported.")
1349
+ else:
1350
+ raise Exception(f"Error while setting uniform '{name}' at location '{location}' to value '{value}': Unsupported type: {type(value)}")
1351
+ assert glGetError() == GL_NO_ERROR
1352
+
1353
+
1354
+ def set_texture(self, program:int, unif_name:str, tex_index:int, tex_unit):
1355
+ glActiveTexture(TEXTURE_UNITS[tex_unit])
1356
+ glBindTexture(GL_TEXTURE_RECTANGLE, tex_index)
1357
+ self.set_uniform(program, unif_name, tex_unit)
1358
+
1359
+
1360
+ def wire_program(self, program: int, uniforms=dict(), textures=dict()):
1361
+ """
1362
+ Binds texture to (read) sampler.
1363
+ Sets uniforms.
1364
+ This doesn't touch the framebuffer bindings.
1365
+
1366
+ `program` : OpenGL id of the program
1367
+ `uniforms`: map uniform names (str) to their values
1368
+ `textures`: map texture sampler names to their texture id. Instead of texture_id you can
1369
+ pass a tuple (texture_id, access) where access is either: GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE.
1370
+ See https://www.khronos.org/opengl/wiki/Image_Load_Store
1371
+ """
1372
+
1373
+ # logging.debug(f"Wiring program {textures}")
1374
+ assert glGetIntegerv(GL_CURRENT_PROGRAM) == program, "Seems like you program is no glUseProgram'ed"
1375
+
1376
+ # Wire uniforms
1377
+ for unif_name, unif_value in uniforms.items():
1378
+ self.set_uniform(program, unif_name, unif_value)
1379
+
1380
+ # Wire the texture samplers to texture units
1381
+ # We arbitrarily set the texture unit
1382
+ texture_unit = texture_image_unit = 0
1383
+ for sampler_name, tex_index in textures.items():
1384
+ if type(tex_index) == tuple:
1385
+ # We wire an image texture, with a image unit (instead of a texture
1386
+ # with a texture unit)
1387
+ tex_index, access = tex_index
1388
+ else:
1389
+ access = GL_READ_ONLY
1390
+
1391
+ assert type(tex_index) in (
1392
+ int,
1393
+ np.uintc,
1394
+ ), f"Texture ID must be integer (sampler='{sampler_name}'). You gave {type(tex_index)}."
1395
+
1396
+ # Check that textures of a given type are compatible with their
1397
+ # sampler's type (for example a *u*sampler will make sense
1398
+ # only on a uint texture).
1399
+ # This is done because OpenGL is completely silent on these
1400
+ # mesimatches and it makes debugging very difficult.
1401
+
1402
+ # Check all shaders associated with the `program`
1403
+ sampler_found = False
1404
+ context = None
1405
+ for shader in self.shaders_programs[program]:
1406
+ k = (shader, sampler_name)
1407
+ if k in self.samplers_in_shader:
1408
+ image_texture = None
1409
+ sampler_type = self.samplers_in_shader[k]
1410
+ context, format = self.textures_formats[tex_index]
1411
+ msg = f"Wiring sampler '{sampler_name}' of type '{sampler_type}' to shader {shader} in program {program} to a texture of context {context} seems wrong. You gave {format} which looks incompatible/unsupported."
1412
+ if sampler_type == "usampler2DRect":
1413
+ assert (
1414
+ context == GL_TEXTURE_RECTANGLE
1415
+ ), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
1416
+ assert format in (GL_R32UI, GL_R8UI, GL_RGBA8, GL_RG16UI, GL_RGB16UI, GL_RGB32UI), msg
1417
+ image_texture = False
1418
+ elif sampler_type == "sampler2DRect":
1419
+ assert (
1420
+ context == GL_TEXTURE_RECTANGLE
1421
+ ), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
1422
+ assert format in (GL_R32F, GL_RGB32F, GL_RGBA32F), msg
1423
+ image_texture = False
1424
+ elif sampler_type == "isampler2DRect":
1425
+ assert (
1426
+ context == GL_TEXTURE_RECTANGLE
1427
+ ), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
1428
+ assert format in (GL_R32I,), msg
1429
+ image_texture = False
1430
+ elif sampler_type == "image2D":
1431
+ # This is introduced to support compute shaders
1432
+ assert (
1433
+ context == GL_TEXTURE_2D
1434
+ ), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_2D context"
1435
+ assert format in (GL_RGBA32F,), msg
1436
+ image_texture = True
1437
+ elif sampler_type == "image2DRect":
1438
+ # This is introduced to support compute shaders
1439
+ assert (
1440
+ context == GL_TEXTURE_RECTANGLE
1441
+ ), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
1442
+ assert format in (GL_RGBA32F, GL_RGB32F, GL_R32F, GL_RG32F), msg
1443
+ image_texture = True
1444
+ elif sampler_type == "uimage2DRect":
1445
+ # This is introduced to support compute shaders
1446
+ assert (
1447
+ context == GL_TEXTURE_RECTANGLE
1448
+ ), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
1449
+ assert format in (GL_R8UI, GL_RGBA32UI, GL_RG16UI), msg
1450
+ image_texture = True
1451
+ else:
1452
+ raise Exception(f"Unsupported sampler type: {sampler_type}")
1453
+ sampler_found = True
1454
+ logging.debug(
1455
+ f"Ready to wire {['Texture','ImageTexture'][image_texture]} number {tex_index} on sampler '{sampler_name}' of type '{sampler_type}' to shader {self.shaders_names[shader]} ({shader}) in program {program} to a texture of context {context}. Format is {format}"
1456
+ )
1457
+ break
1458
+
1459
+ assert (
1460
+ sampler_found
1461
+ ), f"Wiring program: Unknown sampler '{sampler_name}' in program {program} ({self.describe_program(program)})"
1462
+
1463
+ # How to bind texture and images is described here :
1464
+ # https://www.khronos.org/opengl/wiki/Texture#GLSL_binding
1465
+
1466
+ assert image_texture is not None
1467
+ if not image_texture:
1468
+ # glActiveTexture selects which texture unit subsequent texture state
1469
+ # calls will affect.
1470
+ glActiveTexture(TEXTURE_UNITS[texture_unit])
1471
+
1472
+ # Bind texture to active texture unit
1473
+ glBindTexture(context, tex_index)
1474
+
1475
+ logging.debug(f"glActiveTexture(texture_unit={TEXTURE_UNITS[texture_unit]}); glBindTexture({context}, texture_name={tex_index})")
1476
+
1477
+ # Tell the shader to use the texture unit `tex_unit` which we just
1478
+ # have wired a texture to.
1479
+ self.set_uniform(program, sampler_name, texture_unit)
1480
+ texture_unit += 1
1481
+
1482
+ elif image_texture:
1483
+ # https://stackoverflow.com/questions/37136813/what-is-the-difference-between-glbindimagetexture-and-glbindtexture
1484
+ # Right now this is used for compute shaders.
1485
+ # From: https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction
1486
+ # Here the glBindImageTexture function is used to bind a specific level of a texture to an image unit.
1487
+
1488
+ # So one can have a single texture associated to one texture unit
1489
+ # and several images linked to that texture unit, each associated to
1490
+ # one image unit. (there's one level of indirection when compared to
1491
+ # the usual texture/texture unit connections).
1492
+
1493
+ # Now to simplify things, since we don't (so far) use sveral
1494
+ # layers of the same texture in different image unit, we'll
1495
+ # make a STRONG assumption: if one chooses an image unit "i"
1496
+ # then we automatically binds it to the texture unit "i".
1497
+
1498
+ # If we have three texture we want access to, we'll
1499
+ # need three image units. Each of them will be wired
1500
+ # to the corresponding texture unit.
1501
+
1502
+ # Given the current texture, binds a single image of it to
1503
+ # the shader program.
1504
+ logging.debug(
1505
+ f"glBindImageTexture( texunit= {texture_image_unit}, tex_id={tex_index}, level=0, layered=GL_FALSE, layer=0, {access}, {format})"
1506
+ )
1507
+ if format == GL_RGB32F:
1508
+ logging.error(
1509
+ "ImageTexture can't be GL_RGB32F. See OpengGL documentation."
1510
+ )
1511
+
1512
+ # Bind the texture tex_index to the texture unit texture_image_unit.
1513
+ glBindImageTexture(
1514
+ texture_image_unit, tex_index, 0, GL_FALSE, 0, access, format
1515
+ )
1516
+
1517
+
1518
+ # It's not done in the OpenGl tutorial here : https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction
1519
+ # But according to the official doc here : https://www.khronos.org/opengl/wiki/Texture#GLSL_binding
1520
+ # it should be done too...
1521
+ self.set_uniform(program, sampler_name, texture_image_unit)
1522
+ #texture_image_unit += 1
1523
+ texture_image_unit += 1
1524
+
1525
+ mtu = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
1526
+ # FIXME I'm absolutely NOT sure of this test.
1527
+ # First question are texture_image_unit and textre_unit the same thing ???
1528
+ if texture_unit + texture_image_unit > mtu - 1:
1529
+ raise Exception(f"Not enough TEXTURE_UNITS in gl (max is {mtu})")
1530
+
1531
+
1532
+ def init_gl(width, height):
1533
+ """ Initialize the OpenGL context and create a window with pygame """
1534
+ pygame.init()
1535
+
1536
+ display_nfo = pygame.display.Info()
1537
+ res = pygame.display.set_mode((width, height), pygame.DOUBLEBUF | pygame.OPENGL)
1538
+ return res
1539
+
1540
+
1541
+ if __name__ == "__main__":
1542
+ logging.getLogger().setLevel(logging.DEBUG)
1543
+ init_gl(256, 256)
1544
+ query_gl_caps()