opengl_base_backend.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. from imgui_bundle import imgui
  2. import OpenGL.GL as gl # pip install PyOpenGL
  3. def _log__update_texture(msg: str) -> None:
  4. pass
  5. # import logging
  6. # logging.warning(msg)
  7. class BaseOpenGLRenderer(object):
  8. def __init__(self) -> None:
  9. if not imgui.get_current_context():
  10. raise RuntimeError(
  11. "No valid ImGui context. Use imgui.create_context() first and/or "
  12. "imgui.set_current_context()."
  13. )
  14. self.io = imgui.get_io()
  15. self.io.delta_time = 1.0 / 60.0
  16. self._create_device_objects()
  17. # Honor RendererHasTextures
  18. # cf https://github.com/ocornut/imgui/commit/ff3f471ab2af25f1cc11c20356711aaa4e6833f8
  19. imgui.get_io().backend_flags |= imgui.BackendFlags_.renderer_has_textures.value
  20. max_texture_size = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE)
  21. imgui.get_platform_io().renderer_texture_max_width = max_texture_size
  22. imgui.get_platform_io().renderer_texture_max_height = max_texture_size
  23. def render(self, draw_data: imgui.ImDrawData) -> None:
  24. raise NotImplementedError
  25. def _update_textures(self) -> None:
  26. # Honor RendererHasTextures
  27. # cf https://github.com/ocornut/imgui/commit/ff3f471ab2af25f1cc11c20356711aaa4e6833f8
  28. for tex in imgui.get_platform_io().textures:
  29. if tex.status != imgui.ImTextureStatus.ok:
  30. self._update_texture(tex)
  31. def _destroy_all_textures(self) -> None:
  32. for tex in imgui.get_platform_io().textures:
  33. if tex.ref_count == 1:
  34. tex.status = imgui.ImTextureStatus.want_destroy
  35. self._update_texture(tex)
  36. def _update_texture(self, tex: imgui.ImTextureData) -> None:
  37. # Honor RendererHasTextures
  38. # cf https://github.com/ocornut/imgui/commit/ff3f471ab2af25f1cc11c20356711aaa4e6833f8
  39. # This method is a port of the C++ function ImGui_ImplOpenGL3_UpdateTexture
  40. # where we use
  41. # pixels = tex.get_pixels_array()
  42. # to get a numpy array of the pixel data
  43. # When doing updates, we use a sub-view of the full_pixels array to avoid copying data
  44. if tex.status == imgui.ImTextureStatus.want_create:
  45. # Create and upload new texture to graphics system
  46. _log__update_texture(f"UpdateTexture #{tex.unique_id}: WantCreate {tex.width}x{tex.height}")
  47. assert tex.tex_id == 0
  48. assert tex.backend_user_data is None
  49. assert tex.format == imgui.ImTextureFormat.rgba32
  50. # Upload texture to graphics system
  51. # (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
  52. last_texture = gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D)
  53. gl_texture_id = gl.glGenTextures(1)
  54. gl.glBindTexture(gl.GL_TEXTURE_2D, gl_texture_id)
  55. gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
  56. gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
  57. gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
  58. gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
  59. if hasattr(gl, "GL_UNPACK_ROW_LENGTH"):
  60. gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0)
  61. pixels_array = tex.get_pixels_array()
  62. gl.glTexImage2D(
  63. gl.GL_TEXTURE_2D,
  64. 0,
  65. gl.GL_RGBA,
  66. tex.width,
  67. tex.height,
  68. 0,
  69. gl.GL_RGBA,
  70. gl.GL_UNSIGNED_BYTE,
  71. pixels_array,
  72. )
  73. # Store identifiers: store the new GL texture ID back into ImGui's structure
  74. tex.set_tex_id(gl_texture_id)
  75. tex.status = imgui.ImTextureStatus.ok
  76. # Restore state
  77. gl.glBindTexture(gl.GL_TEXTURE_2D, last_texture)
  78. elif tex.status == imgui.ImTextureStatus.want_updates:
  79. _log__update_texture(f"UpdateTexture #{tex.unique_id}: WantUpdate {len(tex.updates)}")
  80. # Update selected blocks. We only ever write to textures regions that have never been used before!
  81. # This backend chooses to use tex.Updates[], but you can use tex.UpdateRect to upload a single region.
  82. last_texture = gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D)
  83. gl.glBindTexture(gl.GL_TEXTURE_2D, tex.tex_id)
  84. # We assume desktop OpenGL where GL_UNPACK_ROW_LENGTH is supported.
  85. # This allows partial updates without line-by-line copies in Python.
  86. gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, tex.width)
  87. # Get the full 1D array of pixels (shape=(width*height*bpp,))
  88. full_pixels = tex.get_pixels_array()
  89. for r in tex.updates:
  90. # Compute offset into the 1D array for the sub-rectangle's top-left pixel:
  91. offset = (r.y * tex.width + r.x) * tex.bytes_per_pixel
  92. # Then get a slice from that offset to the end. We only need the pointer’s start address:
  93. sub_view = full_pixels[offset:] # shape is still 1D, but it starts at the correct place
  94. # glTexSubImage2D will read only r.h rows and r.w columns per row,
  95. # with the stride controlled by GL_UNPACK_ROW_LENGTH:
  96. gl.glTexSubImage2D(
  97. gl.GL_TEXTURE_2D,
  98. 0, # mip level
  99. r.x,
  100. r.y,
  101. r.w,
  102. r.h,
  103. gl.GL_RGBA,
  104. gl.GL_UNSIGNED_BYTE,
  105. sub_view
  106. )
  107. # Restore the row-length to 0 (the driver default)
  108. gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0)
  109. tex.status = imgui.ImTextureStatus.ok
  110. gl.glBindTexture(gl.GL_TEXTURE_2D, last_texture) # Restore state
  111. elif tex.status == imgui.ImTextureStatus.want_destroy:
  112. _log__update_texture(f"UpdateTexture #{tex.unique_id}: WantDestroy")
  113. gl_tex_id = tex.tex_id
  114. gl.glDeleteTextures([gl_tex_id])
  115. # Clear identifiers and mark as destroyed (so e.g. InvalidateDeviceObjects can be called at runtime)
  116. ImTextureID_Invalid = 0
  117. tex.set_tex_id(ImTextureID_Invalid)
  118. tex.status = imgui.ImTextureStatus.destroyed
  119. def _create_device_objects(self) -> None:
  120. raise NotImplementedError
  121. def _invalidate_device_objects(self) -> None:
  122. raise NotImplementedError
  123. def shutdown(self) -> None:
  124. self._destroy_all_textures()
  125. imgui.get_io().backend_flags &= ~imgui.BackendFlags_.renderer_has_textures.value
  126. self._invalidate_device_objects()
  127. def get_common_gl_state():
  128. """
  129. Backups the current OpenGL state
  130. Returns a tuple of results for glGet / glIsEnabled calls
  131. NOTE: when adding more backuped state in the future,
  132. make sure to update function `restore_common_gl_state`
  133. """
  134. last_texture = gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D)
  135. last_viewport = gl.glGetIntegerv(gl.GL_VIEWPORT)
  136. last_enable_blend = gl.glIsEnabled(gl.GL_BLEND)
  137. last_enable_cull_face = gl.glIsEnabled(gl.GL_CULL_FACE)
  138. last_enable_depth_test = gl.glIsEnabled(gl.GL_DEPTH_TEST)
  139. last_enable_scissor_test = gl.glIsEnabled(gl.GL_SCISSOR_TEST)
  140. last_scissor_box = gl.glGetIntegerv(gl.GL_SCISSOR_BOX)
  141. last_blend_src = gl.glGetIntegerv(gl.GL_BLEND_SRC)
  142. last_blend_dst = gl.glGetIntegerv(gl.GL_BLEND_DST)
  143. last_blend_equation_rgb = gl.glGetIntegerv(gl.GL_BLEND_EQUATION_RGB)
  144. last_blend_equation_alpha = gl.glGetIntegerv(gl.GL_BLEND_EQUATION_ALPHA)
  145. last_front_and_back_polygon_mode, _ = gl.glGetIntegerv(gl.GL_POLYGON_MODE)
  146. return (
  147. last_texture,
  148. last_viewport,
  149. last_enable_blend,
  150. last_enable_cull_face,
  151. last_enable_depth_test,
  152. last_enable_scissor_test,
  153. last_scissor_box,
  154. last_blend_src,
  155. last_blend_dst,
  156. last_blend_equation_rgb,
  157. last_blend_equation_alpha,
  158. last_front_and_back_polygon_mode,
  159. )
  160. def restore_common_gl_state(common_gl_state_tuple):
  161. """
  162. Takes a tuple after calling function `get_common_gl_state`,
  163. to set the given OpenGL state back as it was before rendering the UI
  164. """
  165. (
  166. last_texture,
  167. last_viewport,
  168. last_enable_blend,
  169. last_enable_cull_face,
  170. last_enable_depth_test,
  171. last_enable_scissor_test,
  172. last_scissor_box,
  173. last_blend_src,
  174. last_blend_dst,
  175. last_blend_equation_rgb,
  176. last_blend_equation_alpha,
  177. last_front_and_back_polygon_mode,
  178. ) = common_gl_state_tuple
  179. gl.glBindTexture(gl.GL_TEXTURE_2D, last_texture)
  180. gl.glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha)
  181. gl.glBlendFunc(last_blend_src, last_blend_dst)
  182. gl.glPolygonMode(gl.GL_FRONT_AND_BACK, last_front_and_back_polygon_mode)
  183. if last_enable_blend:
  184. gl.glEnable(gl.GL_BLEND)
  185. else:
  186. gl.glDisable(gl.GL_BLEND)
  187. if last_enable_cull_face:
  188. gl.glEnable(gl.GL_CULL_FACE)
  189. else:
  190. gl.glDisable(gl.GL_CULL_FACE)
  191. if last_enable_depth_test:
  192. gl.glEnable(gl.GL_DEPTH_TEST)
  193. else:
  194. gl.glDisable(gl.GL_DEPTH_TEST)
  195. if last_enable_scissor_test:
  196. gl.glEnable(gl.GL_SCISSOR_TEST)
  197. else:
  198. gl.glDisable(gl.GL_SCISSOR_TEST)
  199. gl.glScissor(
  200. last_scissor_box[0],
  201. last_scissor_box[1],
  202. last_scissor_box[2],
  203. last_scissor_box[3],
  204. )
  205. gl.glViewport(
  206. last_viewport[0], last_viewport[1], last_viewport[2], last_viewport[3]
  207. )