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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- 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))
|