meltygui 0.1.0__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.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,658 @@
1
+ """GL resource lifecycle for Melty views.
2
+
3
+ GLState is the auto-injected per-view owner of GL objects (shader programs,
4
+ FBOs, textures, buffers, VAOs). Declare `gl_state: GLState` in a @render_func
5
+ signature and the injection machinery (set_default in core_render) creates one
6
+ in draw_state.misc and hands the SAME instance back every frame — exactly like
7
+ `code_state: CodeState`. The injector also stamps `_owner_ds`, which is what
8
+ ties the instance to Melty's window lifecycle.
9
+
10
+ Every resource goes through `get(key, create, delete, deps)`:
11
+
12
+ fb = gl_state.get("fbo", create=make_fbo, delete=del_fbo, deps=(w, h))
13
+
14
+ - same key + same deps → cached value, no GL calls
15
+ - deps changed → create() the replacement FIRST, then queue the
16
+ old one for deletion. If create() raises, the
17
+ OLD resource is kept (last-good semantics —
18
+ this is what lets a broken shader edit fall
19
+ back to the previous program).
20
+ - deps must be plain comparable values (tuples/strs/ints — no arrays).
21
+
22
+ Deletion never happens inline: deleters are queued and drained once per frame
23
+ by `flush_deletes()` (called from Melty.end_frame on the render thread with
24
+ the context current), because `release()` / `__del__` can fire from any
25
+ thread and GL calls off the main thread are invalid.
26
+
27
+ Lifecycle events (wired in meltygui.py):
28
+ - window delete → GLState.on_window_deleted(window_ds): every live state
29
+ whose owner draw_state sits under that window releases its resources. The
30
+ draw_state (and the GLState in its misc) persists, so a re-created window
31
+ lazily re-allocates on the next render.
32
+ - app shutdown → GLState.shutdown_all()
33
+ - GC → __del__ queues anything not yet released (safety net for
34
+ draw_states that get pruned without a window-delete event).
35
+
36
+ The registries below survive hotswap: recompile execs into the existing
37
+ module dict, so re-running this module reuses the live containers instead of
38
+ orphaning queued deletions and tracked states.
39
+ """
40
+
41
+ import threading
42
+ import weakref
43
+
44
+ import numpy as np
45
+ import OpenGL.GL as gl
46
+ from meltygui.core.rendering.core_decoration import defaults
47
+
48
+
49
+ def _persistent(name, factory):
50
+ """Reuse a module global across hotswap re-exec (recompile execs into the
51
+ existing module.__dict__), so live registries survive code reloads."""
52
+ val = globals().get(name)
53
+ return val if val is not None else factory()
54
+
55
+
56
+ _live_states = _persistent("_live_states", weakref.WeakSet)
57
+ _delete_queue = _persistent("_delete_queue", list) # [(key, value, deleter, context)]
58
+ _queue_lock = _persistent("_queue_lock", threading.RLock)
59
+
60
+
61
+ def current_context():
62
+ """The current GL context as a hashable key (the GLFW window pointer),
63
+ or None when unknown. GL object NAMES are per context (VAOs, FBOs) or
64
+ per share group (textures, buffers), so a deferred delete queued by a
65
+ state born in one context must run in that context — with several OS
66
+ windows (surface.py) the same name means a different object elsewhere."""
67
+ try:
68
+ import meltygui.core.windowing.window_api as glfw
69
+ import ctypes
70
+ ctx = glfw.get_current_context()
71
+ return ctypes.cast(ctx, ctypes.c_void_p).value if ctx else None
72
+ except Exception:
73
+ return None
74
+
75
+ # The thread that owns the GL context. NOT the Python main thread - the studio
76
+ # renders on its visualization thread while the main thread runs the chat
77
+ # loop. The render loop claims it every frame (end_frame → flush_deletes);
78
+ # before the first claim, whoever does GL work first is assumed to be it.
79
+ _gl_thread = _persistent("_gl_thread", lambda: None)
80
+
81
+
82
+ def is_gl_thread():
83
+ """True on the GL-context thread (claiming it if nobody has yet). Guards
84
+ every code path that would issue GL calls from converter/Background
85
+ threads."""
86
+ global _gl_thread
87
+ if _gl_thread is None:
88
+ _gl_thread = threading.current_thread()
89
+ return True
90
+ return threading.current_thread() is _gl_thread
91
+
92
+
93
+ def _scalar(x):
94
+ """PyOpenGL scalar gets sometimes come back as length-1 arrays."""
95
+ return int(x[0]) if hasattr(x, "__len__") else int(x)
96
+
97
+
98
+ # Driver limits, queried ONCE on the GL thread and cached for the process
99
+ # (they never change for a context). Survives hotswap like the registries.
100
+ _gl_limits = _persistent("_gl_limits", dict)
101
+
102
+ # GL_NVX_gpu_memory_info enums (absent from many PyOpenGL builds' namespace).
103
+ _GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX = 0x9048
104
+ _GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX = 0x9049
105
+ # GL_RENDERER substrings of CPU rasterizers, whose memory figures are not VRAM.
106
+ _SOFTWARE_RENDERERS = (b"llvmpipe", b"softpipe", b"swrast")
107
+
108
+
109
+ def _has_extension(name):
110
+ """Whether the current core-profile context lists `name`. An unsupported
111
+ glGet enum leaves the result unwritten (garbage, not an exception, once
112
+ error checking is off), so optional queries ask here first."""
113
+ try:
114
+ count = _scalar(gl.glGetIntegerv(gl.GL_NUM_EXTENSIONS))
115
+ return any(gl.glGetStringi(gl.GL_EXTENSIONS, i) == name.encode() for i in range(count))
116
+ except Exception:
117
+ return False
118
+
119
+
120
+ def gl_limits():
121
+ """{'max_3d': int, 'max_2d': int, 'total_vram_kb': int|None,
122
+ 'vram_info': bool (GL_NVX_gpu_memory_info is present)} — the
123
+ context's texture-size limits (GL_MAX_3D_TEXTURE_SIZE et al). Cached
124
+ after the first successful query; off the GL thread (or before any
125
+ context exists) returns conservative spec minimums WITHOUT caching, so
126
+ a real query still lands once the render thread asks."""
127
+ if "vram_info" in _gl_limits: # a pre-hotswap cache without it is queried again
128
+ return _gl_limits
129
+ limits = {"max_3d": 2048, "max_2d": 16384, "total_vram_kb": None, "vram_info": False}
130
+ if not is_gl_thread():
131
+ return limits
132
+ try:
133
+ max_3d = _scalar(gl.glGetIntegerv(gl.GL_MAX_3D_TEXTURE_SIZE))
134
+ max_2d = _scalar(gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE))
135
+ except Exception:
136
+ return limits # no context yet - don't cache the guess
137
+ if max_3d <= 0 or max_2d <= 0:
138
+ return limits # no current context: glGet returns 0 silently
139
+ limits["max_3d"], limits["max_2d"] = max_3d, max_2d
140
+ # Mesa's software rasterizers list the extension too, but have no VRAM:
141
+ # llvmpipe on Windows reports a few hundred KiB free, which refused every volume.
142
+ renderer = (gl.glGetString(gl.GL_RENDERER) or b"").lower()
143
+ software = any(name in renderer for name in _SOFTWARE_RENDERERS)
144
+ limits["vram_info"] = not software and _has_extension("GL_NVX_gpu_memory_info")
145
+ if limits["vram_info"]:
146
+ try:
147
+ limits["total_vram_kb"] = _scalar(
148
+ gl.glGetIntegerv(_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX))
149
+ except Exception:
150
+ limits["total_vram_kb"] = None
151
+ _gl_limits.update(limits)
152
+ return _gl_limits
153
+
154
+
155
+ def gl_free_vram_kb():
156
+ """Currently free VRAM in KiB via GL_NVX_gpu_memory_info, or None when
157
+ the extension is unavailable. Cheap (one glGet) but only meaningful on
158
+ the GL thread."""
159
+ if not is_gl_thread() or not gl_limits()["vram_info"]:
160
+ return None
161
+ try:
162
+ return _scalar(gl.glGetIntegerv(_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX))
163
+ except Exception:
164
+ return None
165
+
166
+
167
+ def texture3d_fit(shape, itemsize, max_bytes=None):
168
+ """Decide BEFORE any upload whether a (depth, height, width) 3-D texture
169
+ of `itemsize`-byte texels can exist here. Returns
170
+ (clamped_shape, problems): `clamped_shape` is `shape` with every extent
171
+ cut to GL_MAX_3D_TEXTURE_SIZE, `problems` a list of human-readable
172
+ strings — an over-limit extent (recoverable: display the clamped
173
+ prefix), a byte total past `max_bytes` (default: half of the reported
174
+ free VRAM, or 2 GiB when the driver won't say — NOT recoverable by
175
+ clamping, the caller should refuse), or a degenerate (empty / non-3-D)
176
+ shape. Empty `problems` = safe to allocate as-is."""
177
+ problems = []
178
+ dims = tuple(int(s) for s in shape)
179
+ if len(dims) != 3:
180
+ return dims, [f"volume must be 3-D, got shape {dims}"]
181
+ if any(d <= 0 for d in dims):
182
+ return dims, [f"empty volume (shape {dims}) — nothing to display"]
183
+ lim = gl_limits()
184
+ max_3d = int(lim["max_3d"])
185
+ clamped = tuple(min(d, max_3d) for d in dims)
186
+ if clamped != dims:
187
+ over = ", ".join(f"{'zyx'[i]}={dims[i]}" for i in range(3) if dims[i] > max_3d)
188
+ problems.append(f"{over} exceeds GL_MAX_3D_TEXTURE_SIZE={max_3d}; "
189
+ f"showing the first {max_3d} along that axis")
190
+ if max_bytes is None:
191
+ free_kb = gl_free_vram_kb()
192
+ max_bytes = (free_kb * 1024) // 2 if free_kb else 2 * 1024 ** 3
193
+ nbytes = clamped[0] * clamped[1] * clamped[2] * int(itemsize)
194
+ if nbytes > max_bytes:
195
+ problems.append(f"volume needs {nbytes / 2**20:.0f} MiB of texture memory "
196
+ f"(budget {max_bytes / 2**20:.0f} MiB) — pin or average "
197
+ f"more dims to shrink it")
198
+ return clamped, problems
199
+
200
+
201
+ class tight_unpack:
202
+ """Save → canonical tight-row GL_UNPACK_* state → restore, around texture
203
+ uploads. Leftover pitch state (ROW_LENGTH/SKIP_* from any other GL code in
204
+ the process) shears our rows — the classic "every other row missing"
205
+ corruption — and our ALIGNMENT=1 must not leak out either."""
206
+
207
+ _PNAMES = ("GL_UNPACK_ALIGNMENT", "GL_UNPACK_ROW_LENGTH", "GL_UNPACK_IMAGE_HEIGHT",
208
+ "GL_UNPACK_SKIP_ROWS", "GL_UNPACK_SKIP_PIXELS", "GL_UNPACK_SKIP_IMAGES")
209
+ _CANON = (1, 0, 0, 0, 0, 0)
210
+
211
+ def __enter__(self):
212
+ self._saved = []
213
+ for name, canon in zip(self._PNAMES, self._CANON):
214
+ pname = getattr(gl, name)
215
+ self._saved.append((pname, _scalar(gl.glGetIntegerv(pname))))
216
+ gl.glPixelStorei(pname, canon)
217
+ return self
218
+
219
+ def __exit__(self, *exc):
220
+ for pname, value in self._saved:
221
+ gl.glPixelStorei(pname, value)
222
+ return False
223
+
224
+
225
+ class GLTexture:
226
+ """A GL texture handle plus the metadata shader_func needs to bind it:
227
+ `target` decides sampler2D vs sampler3D at uniform-injection time."""
228
+ tint = (0.0667, 0.07, 0.07)
229
+ def __init__(self, texture_id, target, shape=(), internal_format=0):
230
+ self.texture_id = int(texture_id)
231
+ self.target = int(target)
232
+ self.shape = tuple(shape)
233
+ self.internal_format = int(internal_format)
234
+
235
+ def __repr__(self):
236
+ kind = {gl.GL_TEXTURE_1D: "1d", gl.GL_TEXTURE_2D: "2d",
237
+ gl.GL_TEXTURE_3D: "3d"}.get(self.target, hex(self.target))
238
+ return f"GLTexture({kind} id={self.texture_id} shape={self.shape})"
239
+
240
+
241
+ class FBO:
242
+ """Offscreen render target (color RGBA8 + depth 24). Context manager:
243
+ `with fb:` binds the FBO and sets the viewport, restoring both on exit —
244
+ the save/restore dance every render-to-texture pass needs inside an
245
+ imgui frame."""
246
+
247
+ def __init__(self, fbo, color: GLTexture, depth: GLTexture, width, height):
248
+ self.fbo = int(fbo)
249
+ self.color = color
250
+ self.depth = depth
251
+ self.width = int(width)
252
+ self.height = int(height)
253
+ self._prev_fbo = 0
254
+ self._prev_viewport = None
255
+
256
+ @property
257
+ def texture_id(self):
258
+ return self.color.texture_id
259
+
260
+ def bind(self):
261
+ """Bind + set the viewport, remembering what was bound (unbind
262
+ restores it). The explicit pair behind the context manager, for
263
+ callers that bracket a pass with plain calls (pbr.begin_scene)."""
264
+ self._prev_fbo = _scalar(gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING))
265
+ self._prev_viewport = gl.glGetIntegerv(gl.GL_VIEWPORT)
266
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.fbo)
267
+ gl.glViewport(0, 0, self.width, self.height)
268
+ return self
269
+
270
+ def unbind(self):
271
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._prev_fbo)
272
+ pv = self._prev_viewport
273
+ if pv is not None:
274
+ gl.glViewport(int(pv[0]), int(pv[1]), int(pv[2]), int(pv[3]))
275
+ return self
276
+
277
+ def __enter__(self):
278
+ return self.bind()
279
+
280
+ def __exit__(self, *exc):
281
+ self.unbind()
282
+ return False
283
+
284
+ def __repr__(self):
285
+ return f"FBO(id={self.fbo} {self.width}x{self.height})"
286
+
287
+
288
+ class ResourceDeletionDeferred(Exception):
289
+ """A resource is still owned by another API; retry on the next drain."""
290
+
291
+
292
+ class _Resource:
293
+ __slots__ = ("value", "deleter", "deps")
294
+
295
+ def __init__(self, value, deleter, deps):
296
+ self.value = value
297
+ self.deleter = deleter
298
+ self.deps = deps
299
+
300
+
301
+ class GLState:
302
+
303
+ # Stamped by core_render's set_default when the state is created into
304
+ # draw_state.misc - the owning draw_state, whose parent_window chain is
305
+ # how on_window_deleted decides when to release.
306
+ _owner_ds = None
307
+
308
+ def __init__(self):
309
+ self._resources = {}
310
+ self._context = current_context() # while its names are valid
311
+ _live_states.add(self)
312
+
313
+ # ── core ────────────────────────────────────────────────────────────
314
+
315
+ def get(self, key, create, delete=None, deps=None):
316
+ """The cached resource for `key`, (re)created when `deps` differ from
317
+ the cached generation. On create() failure the previous resource (and
318
+ its deps) survive untouched and the exception propagates — callers
319
+ that want last-good fallback catch it and use peek()."""
320
+ rec = self._resources.get(key)
321
+ if rec is not None and rec.deps == deps:
322
+ return rec.value
323
+ value = create()
324
+ if rec is not None:
325
+ self._queue(key, rec)
326
+ self._resources[key] = _Resource(value, delete, deps)
327
+ return value
328
+
329
+ def peek(self, key):
330
+ rec = self._resources.get(key)
331
+ return rec.value if rec is not None else None
332
+
333
+ def drop(self, key):
334
+ """Queue one resource for deletion (forces re-create on next get)."""
335
+ rec = self._resources.pop(key, None)
336
+ if rec is not None:
337
+ self._queue(key, rec)
338
+ return rec is not None
339
+
340
+ def defer_delete(self, key, value, deleter):
341
+ """Own cleanup of a partial allocation even if its factory failed."""
342
+ self._queue(key, _Resource(value, deleter, None))
343
+
344
+ def release(self):
345
+ """Queue every resource for deletion. The instance stays usable — a
346
+ later get() simply re-allocates (closed windows can re-open)."""
347
+ with _queue_lock:
348
+ for key, rec in self._resources.items():
349
+ if rec.deleter is not None:
350
+ _delete_queue.append((key, rec.value, rec.deleter, self._context))
351
+ self._resources.clear()
352
+
353
+ @classmethod
354
+ def release_context(cls, context):
355
+ """Release all owners before a surface destroys their GL context."""
356
+ for state in list(_live_states):
357
+ if state._context == context:
358
+ state.release()
359
+
360
+ def __del__(self):
361
+ # Any thread, any time (GC) - only queues, never touches GL.
362
+ try:
363
+ self.release()
364
+ except Exception:
365
+ pass
366
+
367
+ def _queue(self, key, rec):
368
+ if rec.deleter is None:
369
+ return
370
+ with _queue_lock:
371
+ _delete_queue.append((key, rec.value, rec.deleter, self._context))
372
+
373
+ # ── lifecycle callbacks (called from meltygui.py) ──────────────────────────
374
+
375
+ @staticmethod
376
+ def flush_deletes():
377
+ """Drain queued deleters. GL-thread only — GL calls are invalid
378
+ elsewhere, so off-thread calls are a silent no-op (the queue keeps
379
+ everything until a frame can run it)."""
380
+ if not is_gl_thread():
381
+ return 0
382
+ n = 0
383
+ current = current_context()
384
+ deferred = [] # another context's names: run when it's current
385
+ while True:
386
+ with _queue_lock:
387
+ if not _delete_queue:
388
+ break
389
+ key, value, deleter, context = _delete_queue.pop()
390
+ if context is not None and current is not None and context != current:
391
+ deferred.append((key, value, deleter, context))
392
+ continue
393
+ try:
394
+ deleter(value)
395
+ except ResourceDeletionDeferred:
396
+ # Keep the object alive and retry once next frame, never spin
397
+ # in this drain or free storage another API still references.
398
+ deferred.append((key, value, deleter, context))
399
+ continue
400
+ except Exception as e:
401
+ print(f"[gl_state] delete failed for {key!r}: {e}")
402
+ n += 1
403
+ if deferred:
404
+ with _queue_lock:
405
+ _delete_queue.extend(deferred)
406
+ return n
407
+
408
+ @staticmethod
409
+ def discard_context(context):
410
+ """A GL context is gone (surface.py destroyed its window): drop the
411
+ deletes queued for it — their names died with it, and run in
412
+ another context they would delete THAT context's objects."""
413
+ with _queue_lock:
414
+ _delete_queue[:] = [e for e in _delete_queue if e[3] != context]
415
+
416
+ @classmethod
417
+ def on_window_deleted(cls, window_ds):
418
+ """Release every live state owned by a draw_state under the deleted
419
+ window (walking parent_window chains, which terminate at a top-level
420
+ window or self-loop)."""
421
+ if window_ds is None:
422
+ return
423
+ for state in list(_live_states):
424
+ node = state._owner_ds
425
+ hops = 0
426
+ while node is not None and hops < 64:
427
+ if node is window_ds:
428
+ state.release()
429
+ break
430
+ nxt = getattr(node, "parent_window", None)
431
+ if nxt is None or nxt is node:
432
+ break
433
+ node = nxt
434
+ hops += 1
435
+
436
+ @classmethod
437
+ def states_under(cls, window_ds):
438
+ """Every live state owned by a draw_state under `window_ds` (the
439
+ on_window_deleted ownership walk, as a list)."""
440
+ out = []
441
+ if window_ds is None:
442
+ return out
443
+ for state in list(_live_states):
444
+ node = state._owner_ds
445
+ hops = 0
446
+ while node is not None and hops < 64:
447
+ if node is window_ds:
448
+ out.append(state)
449
+ break
450
+ nxt = getattr(node, "parent_window", None)
451
+ if nxt is None or nxt is node:
452
+ break
453
+ node = nxt
454
+ hops += 1
455
+ return out
456
+
457
+ @classmethod
458
+ def drop_under(cls, window_ds, keys):
459
+ """Drop just `keys` from every live state owned under `window_ds` —
460
+ the selective sibling of on_window_deleted for releasing what PINS a
461
+ value while the window keeps its FBO / last image."""
462
+ for state in cls.states_under(window_ds):
463
+ for k in keys:
464
+ state.drop(k)
465
+
466
+ @classmethod
467
+ def shutdown_all(cls):
468
+ for state in list(_live_states):
469
+ state.release()
470
+ return cls.flush_deletes()
471
+
472
+ @classmethod
473
+ def stats(cls):
474
+ states = list(_live_states)
475
+ with _queue_lock:
476
+ queued = len(_delete_queue)
477
+ return {
478
+ "states": len(states),
479
+ "resources": sum(len(s._resources) for s in states),
480
+ "queued_deletes": queued,
481
+ }
482
+
483
+ def __repr__(self):
484
+ return f"GLState({len(self._resources)} resources: {sorted(map(str, self._resources))})"
485
+
486
+ # ── conveniences ────────────────────────────────────────────────────
487
+
488
+ def fbo(self, key, width, height):
489
+ """Offscreen target, re-created on resize. RGBA8 color + 24-bit depth
490
+ textures, matching the volume renderer's original FBO setup."""
491
+ width, height = max(1, int(width)), max(1, int(height))
492
+
493
+ def create():
494
+ fbo = _scalar(gl.glGenFramebuffers(1))
495
+ color_id = _scalar(gl.glGenTextures(1))
496
+ gl.glBindTexture(gl.GL_TEXTURE_2D, color_id)
497
+ # fp16: to composite with the linear scRGB scene (hdr_color.py)
498
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA16F, width, height, 0,
499
+ gl.GL_RGBA, gl.GL_HALF_FLOAT, None)
500
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
501
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
502
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
503
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
504
+ depth_id = _scalar(gl.glGenTextures(1))
505
+ gl.glBindTexture(gl.GL_TEXTURE_2D, depth_id)
506
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_DEPTH_COMPONENT24, width, height, 0,
507
+ gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT, None)
508
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
509
+
510
+ prev = _scalar(gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING))
511
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
512
+ gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0,
513
+ gl.GL_TEXTURE_2D, color_id, 0)
514
+ gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_DEPTH_ATTACHMENT,
515
+ gl.GL_TEXTURE_2D, depth_id, 0)
516
+ status = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER)
517
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, prev)
518
+ if status != gl.GL_FRAMEBUFFER_COMPLETE:
519
+ gl.glDeleteFramebuffers(1, [fbo])
520
+ gl.glDeleteTextures([color_id, depth_id])
521
+ raise RuntimeError(f"FBO incomplete: {hex(status)} ({width}x{height})")
522
+ color = GLTexture(color_id, gl.GL_TEXTURE_2D, (height, width), gl.GL_RGBA16F)
523
+ depth = GLTexture(depth_id, gl.GL_TEXTURE_2D, (height, width), gl.GL_DEPTH_COMPONENT24)
524
+ return FBO(fbo, color, depth, width, height)
525
+
526
+ def delete(fb):
527
+ gl.glDeleteFramebuffers(1, [fb.fbo])
528
+ gl.glDeleteTextures([fb.color.texture_id, fb.depth.texture_id])
529
+
530
+ return self.get(key, create, delete, deps=(width, height))
531
+
532
+ def texture3d(self, key, data, nearest=True, version=None):
533
+ """3D single-channel texture from a numpy array shaped (depth, height,
534
+ width). The internal format strictly pairs with the data dtype:
535
+ float16 → GL_R16F + GL_HALF_FLOAT, anything else converts to
536
+ float32 → GL_R32F + GL_FLOAT. Allocation is NULL-pointer + the data
537
+ staged through a PIXEL_UNPACK PBO — the proven upload recipe (direct
538
+ client-memory TexImage was a historical source of row corruption).
539
+ Pass `version` (any comparable token — a counter, id(tensor)) to
540
+ force re-upload when the same-shaped data changes."""
541
+ if data.dtype == np.float16:
542
+ internal, gl_type = gl.GL_R16F, gl.GL_HALF_FLOAT
543
+ else:
544
+ internal, gl_type = gl.GL_R32F, gl.GL_FLOAT
545
+ if data.dtype != np.float32:
546
+ data = data.astype(np.float32)
547
+ depth, height, width = (int(s) for s in data.shape)
548
+ filt = gl.GL_NEAREST if nearest else gl.GL_LINEAR
549
+ def create():
550
+ import ctypes
551
+ # Refuse up front with a readable reason rather than letting the
552
+ # driver raise GL_INVALID_VALUE out of glTexImage3D (which also
553
+ # leaks the half-configured texture object). Inside create() on
554
+ # purpose: the VRAM budget is measured against FREE memory, so
555
+ # a cache hit must never be re-judged against what its own
556
+ # allocation consumed.
557
+ _, problems = texture3d_fit(data.shape, data.dtype.itemsize)
558
+ if problems:
559
+ raise ValueError("texture3d: " + "; ".join(problems))
560
+ tex = _scalar(gl.glGenTextures(1))
561
+ gl.glBindTexture(gl.GL_TEXTURE_3D, tex)
562
+ gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MIN_FILTER, filt)
563
+ gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MAG_FILTER, filt)
564
+ gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
565
+ gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
566
+ gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE)
567
+ payload = np.ascontiguousarray(data)
568
+ with tight_unpack():
569
+ # Allocate only - data arrives via the PBO below.
570
+ gl.glTexImage3D(gl.GL_TEXTURE_3D, 0, internal, width, height, depth, 0,
571
+ gl.GL_RED, gl_type, ctypes.c_void_p(0))
572
+ pbo = _scalar(gl.glGenBuffers(1))
573
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, pbo)
574
+ gl.glBufferData(gl.GL_PIXEL_UNPACK_BUFFER, payload.nbytes, payload,
575
+ gl.GL_STREAM_DRAW)
576
+ gl.glTexSubImage3D(gl.GL_TEXTURE_3D, 0, 0, 0, 0, width, height, depth,
577
+ gl.GL_RED, gl_type, None)
578
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0)
579
+ # This defers the actual delete until the transfer completes.
580
+ gl.glDeleteBuffers(1, [pbo])
581
+ gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
582
+ return GLTexture(tex, gl.GL_TEXTURE_3D, (depth, height, width), internal)
583
+
584
+ def delete(tex):
585
+ gl.glDeleteTextures([tex.texture_id])
586
+
587
+ deps = ((depth, height, width), str(data.dtype), nearest, version)
588
+ return self.get(key, create, delete, deps=deps)
589
+
590
+ def texture1d(self, key, data, nearest=False, version=None):
591
+ """1-D RGB texture from a flat [r,g,b, r,g,b, ...] float list (the LUT
592
+ shape — anything reshapable to (n, 3) works). Linear-filtered and
593
+ edge-clamped by default so a color ramp samples smoothly. The payload
594
+ is tiny, so it uploads via plain client-memory TexImage1D — no PBO
595
+ staging needed. Pass `version` (any comparable token, e.g. a content
596
+ hash) to force re-upload when same-length data changes."""
597
+ payload = np.ascontiguousarray(np.asarray(data, dtype=np.float32).reshape(-1, 3))
598
+ n = int(payload.shape[0])
599
+ filt = gl.GL_NEAREST if nearest else gl.GL_LINEAR
600
+
601
+ def create():
602
+ tex = _scalar(gl.glGenTextures(1))
603
+ gl.glBindTexture(gl.GL_TEXTURE_1D, tex)
604
+ gl.glTexParameteri(gl.GL_TEXTURE_1D, gl.GL_TEXTURE_MIN_FILTER, filt)
605
+ gl.glTexParameteri(gl.GL_TEXTURE_1D, gl.GL_TEXTURE_MAG_FILTER, filt)
606
+ gl.glTexParameteri(gl.GL_TEXTURE_1D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
607
+ with tight_unpack():
608
+ gl.glTexImage1D(gl.GL_TEXTURE_1D, 0, gl.GL_RGB32F, n, 0,
609
+ gl.GL_RGB, gl.GL_FLOAT, payload)
610
+ gl.glBindTexture(gl.GL_TEXTURE_1D, 0)
611
+ return GLTexture(tex, gl.GL_TEXTURE_1D, (n,), gl.GL_RGB32F)
612
+
613
+ def delete(tex):
614
+ gl.glDeleteTextures([tex.texture_id])
615
+
616
+ return self.get(key, create, delete, deps=(n, nearest, version))
617
+
618
+ def vao(self, key, build=None, deps=None):
619
+ """A vertex array. `build()` runs once with the fresh VAO bound — it
620
+ sets up VBOs/attribs and returns the buffer ids it generated so they
621
+ get deleted with the VAO. `build=None` gives an empty VAO (core
622
+ profile requires one bound even for attribute-less fullscreen
623
+ triangles)."""
624
+
625
+ def create():
626
+ vao = _scalar(gl.glGenVertexArrays(1))
627
+ gl.glBindVertexArray(vao)
628
+ bufs = tuple(int(b) for b in (build() or ())) if build is not None else ()
629
+ gl.glBindVertexArray(0)
630
+ return (vao, bufs)
631
+
632
+ def delete(value):
633
+ vao, bufs = value
634
+ for b in bufs:
635
+ gl.glDeleteBuffers(1, [b])
636
+ gl.glDeleteVertexArrays(1, [vao])
637
+
638
+ return self.get(key, create, delete, deps=deps)[0]
639
+
640
+ def buffer(self, key, data=None, nbytes=None, target=gl.GL_ARRAY_BUFFER,
641
+ usage=gl.GL_DYNAMIC_DRAW, version=None):
642
+ """A plain GL buffer, sized from `data` (numpy) or `nbytes`. This is
643
+ the allocation the CUDA-interop step will register against."""
644
+ if data is not None:
645
+ data = np.ascontiguousarray(data)
646
+ nbytes = data.nbytes
647
+
648
+ def create():
649
+ buf = _scalar(gl.glGenBuffers(1))
650
+ gl.glBindBuffer(target, buf)
651
+ gl.glBufferData(target, int(nbytes), data, usage)
652
+ gl.glBindBuffer(target, 0)
653
+ return buf
654
+
655
+ def delete(buf):
656
+ gl.glDeleteBuffers(1, [buf])
657
+
658
+ return self.get(key, create, delete, deps=(int(nbytes), int(target), version))