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,667 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter - main interface for shader-based texture filtering.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from functools import partial
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from meltygui.graphics.shader_compiler import ProgramCompiler
|
|
12
|
+
from meltygui.graphics.shader_compiler import CompiledProgram
|
|
13
|
+
from meltygui.graphics.shader_compiler import ShaderCompilationError
|
|
14
|
+
from meltygui.graphics.executor import FilterExecutor
|
|
15
|
+
from meltygui.graphics.registry import get_registry
|
|
16
|
+
|
|
17
|
+
import meltygui.graphics.texture_min_max as texture_min_max
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FilterChain:
|
|
21
|
+
"""
|
|
22
|
+
A chainable sequence of filters.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
chain = filter.chain()
|
|
26
|
+
result = (chain
|
|
27
|
+
.brightness_contrast(brightness=0.2)
|
|
28
|
+
.gaussian_blur(radius=3.0)
|
|
29
|
+
.apply(texture_id))
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, filter_instance: 'Filter'):
|
|
33
|
+
"""
|
|
34
|
+
Initialize a filter chain.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
filter_instance: The parent Filter instance
|
|
38
|
+
"""
|
|
39
|
+
self._filter = filter_instance
|
|
40
|
+
self._steps: list = []
|
|
41
|
+
|
|
42
|
+
def __getattr__(self, name: str):
|
|
43
|
+
"""Add a filter step to the chain."""
|
|
44
|
+
if name.startswith('_'):
|
|
45
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
46
|
+
|
|
47
|
+
if not self._filter.has_shader(name):
|
|
48
|
+
available = ', '.join(self._filter.list_shaders())
|
|
49
|
+
raise AttributeError(
|
|
50
|
+
f"No shader named '{name}'. Available shaders: {available}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def add_step(**uniforms):
|
|
54
|
+
self._steps.append((name, uniforms))
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
return add_step
|
|
58
|
+
|
|
59
|
+
def apply(self, texture_id: int, in_place: bool = False) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Apply the filter chain to a texture.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
texture_id: Input texture ID
|
|
65
|
+
in_place: If True, modify the input texture (final output)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The output texture ID
|
|
69
|
+
"""
|
|
70
|
+
if not self._steps:
|
|
71
|
+
return texture_id
|
|
72
|
+
|
|
73
|
+
current_texture = texture_id
|
|
74
|
+
|
|
75
|
+
for i, (shader_name, uniforms) in enumerate(self._steps):
|
|
76
|
+
is_last = (i == len(self._steps) - 1)
|
|
77
|
+
|
|
78
|
+
if is_last and in_place:
|
|
79
|
+
# Last step - render to original texture
|
|
80
|
+
current_texture = self._filter.apply(
|
|
81
|
+
shader_name, current_texture,
|
|
82
|
+
in_place=True, **uniforms
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
# Intermediate step - render to new texture
|
|
86
|
+
current_texture = self._filter.apply(
|
|
87
|
+
shader_name, current_texture,
|
|
88
|
+
in_place=False, **uniforms
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return current_texture
|
|
92
|
+
|
|
93
|
+
def clear(self) -> 'FilterChain':
|
|
94
|
+
"""Clear all steps from the chain."""
|
|
95
|
+
self._steps.clear()
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __len__(self) -> int:
|
|
99
|
+
"""Get the number of steps in the chain."""
|
|
100
|
+
return len(self._steps)
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
steps = ' -> '.join(name for name, _ in self._steps)
|
|
104
|
+
return f"FilterChain([{steps}])"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Filter:
|
|
108
|
+
"""
|
|
109
|
+
Main interface for shader-based texture filtering.
|
|
110
|
+
|
|
111
|
+
Provides attribute-style access to shader filters and manages
|
|
112
|
+
shader lifecycle, compilation, and execution.
|
|
113
|
+
|
|
114
|
+
Usage:
|
|
115
|
+
filter = Filter()
|
|
116
|
+
|
|
117
|
+
# Apply filters using attribute access
|
|
118
|
+
texture_id = filter.brightness_contrast(texture_id, brightness=0.5)
|
|
119
|
+
texture_id = filter.vignette(texture_id, in_place=True)
|
|
120
|
+
|
|
121
|
+
# Chain multiple filters
|
|
122
|
+
result = (filter.chain()
|
|
123
|
+
.brightness_contrast(brightness=0.2)
|
|
124
|
+
.gaussian_blur_h(radius=3.0)
|
|
125
|
+
.apply(texture_id))
|
|
126
|
+
|
|
127
|
+
# Clean up when done
|
|
128
|
+
filter.cleanup()
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, auto_compile: bool = True):
|
|
132
|
+
"""
|
|
133
|
+
Initialize the Filter manager.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
auto_compile: If True, compile shaders on first use.
|
|
137
|
+
"""
|
|
138
|
+
self._auto_compile = auto_compile
|
|
139
|
+
self._registry = get_registry()
|
|
140
|
+
self._compiler = ProgramCompiler()
|
|
141
|
+
self._executor = FilterExecutor(self._compiler)
|
|
142
|
+
self._compiled: Dict[str, CompiledProgram] = {}
|
|
143
|
+
|
|
144
|
+
# Dynamically generate methods for all registered shaders
|
|
145
|
+
# This enables IDE autocomplete and type hints
|
|
146
|
+
self._generate_shader_methods()
|
|
147
|
+
|
|
148
|
+
def _generate_shader_methods(self) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Dynamically generate methods for all registered shaders.
|
|
151
|
+
|
|
152
|
+
This creates actual methods on the instance for each shader,
|
|
153
|
+
enabling IDE autocomplete and type hints.
|
|
154
|
+
"""
|
|
155
|
+
for shader_name in self._registry.shaders.keys():
|
|
156
|
+
# Create a closure to capture shader_name
|
|
157
|
+
def make_method(name: str):
|
|
158
|
+
def shader_method(
|
|
159
|
+
texture_id: int = 0,
|
|
160
|
+
in_place: bool = False,
|
|
161
|
+
output_texture: Optional[int] = None,
|
|
162
|
+
output_framebuffer: Optional[int] = None,
|
|
163
|
+
input_framebuffer: Optional[int] = None,
|
|
164
|
+
output_size: Optional[Tuple[int, int]] = None,
|
|
165
|
+
**uniforms
|
|
166
|
+
) -> int:
|
|
167
|
+
"""
|
|
168
|
+
Apply this shader filter to a texture or framebuffer.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
texture_id: Input texture ID (ignored if input_framebuffer is set)
|
|
172
|
+
in_place: If True, modify the input texture directly
|
|
173
|
+
output_texture: Optional specific output texture to render to
|
|
174
|
+
output_framebuffer: Optional framebuffer to render to (e.g., 0 for main screen)
|
|
175
|
+
input_framebuffer: Optional framebuffer to read from (e.g., 0 for main screen)
|
|
176
|
+
output_size: Optional (width, height) render resolution. Runs the
|
|
177
|
+
shader at a lower resolution than the input while still sampling
|
|
178
|
+
the full-res input - the resulting smaller texture upscales
|
|
179
|
+
automatically when sampled downstream.
|
|
180
|
+
**uniforms: Uniform values to pass to the shader
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The output texture ID (same as input if in_place=True, 0 if output_framebuffer)
|
|
184
|
+
"""
|
|
185
|
+
return self.apply(name, texture_id, in_place, output_texture, output_framebuffer, input_framebuffer, output_size=output_size, **uniforms)
|
|
186
|
+
|
|
187
|
+
# Set the method name for better debugging
|
|
188
|
+
shader_method.__name__ = name
|
|
189
|
+
return shader_method
|
|
190
|
+
|
|
191
|
+
# Attach the method to this instance
|
|
192
|
+
setattr(self, shader_name, make_method(shader_name))
|
|
193
|
+
|
|
194
|
+
def __getattr__(self, name: str):
|
|
195
|
+
"""
|
|
196
|
+
Fallback attribute access for dynamically registered shaders.
|
|
197
|
+
|
|
198
|
+
Note: Most shaders have real methods generated at __init__ time.
|
|
199
|
+
This fallback handles edge cases like shaders registered after init.
|
|
200
|
+
"""
|
|
201
|
+
# Don't intercept private attributes
|
|
202
|
+
if name.startswith('_'):
|
|
203
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
204
|
+
|
|
205
|
+
# Check if this shader exists
|
|
206
|
+
if not self.has_shader(name):
|
|
207
|
+
available = ', '.join(self.list_shaders())
|
|
208
|
+
raise AttributeError(
|
|
209
|
+
f"No shader named '{name}'. Available shaders: {available}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Return a callable that applies this filter
|
|
213
|
+
return partial(self.apply, name)
|
|
214
|
+
|
|
215
|
+
def apply(self, shader_name: str, texture_id: int = 0,
|
|
216
|
+
in_place: bool = False,
|
|
217
|
+
output_texture: Optional[int] = None,
|
|
218
|
+
output_framebuffer: Optional[int] = None,
|
|
219
|
+
input_framebuffer: Optional[int] = None,
|
|
220
|
+
output_size: Optional[Tuple[int, int]] = None,
|
|
221
|
+
**uniforms) -> int:
|
|
222
|
+
"""
|
|
223
|
+
Apply a shader filter to a texture or framebuffer.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
shader_name: Name of the shader to apply
|
|
227
|
+
texture_id: Input texture ID (ignored if input_framebuffer is set)
|
|
228
|
+
in_place: If True, modify the input texture directly
|
|
229
|
+
output_texture: Optional specific output texture to render to
|
|
230
|
+
output_framebuffer: Optional framebuffer to render to (e.g., 0 for main screen).
|
|
231
|
+
If set, renders directly to framebuffer instead of creating output texture.
|
|
232
|
+
input_framebuffer: Optional framebuffer to read from (e.g., 0 for main screen).
|
|
233
|
+
If set, reads from this framebuffer instead of texture_id.
|
|
234
|
+
**uniforms: Uniform values to pass to the shader
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
The output texture ID (same as input if in_place=True, 0 if output_framebuffer)
|
|
238
|
+
"""
|
|
239
|
+
program = self._get_compiled(shader_name)
|
|
240
|
+
if program is None:
|
|
241
|
+
raise KeyError(f"Shader '{shader_name}' not found or failed to compile")
|
|
242
|
+
|
|
243
|
+
# Fill in default uniform values
|
|
244
|
+
full_uniforms = {}
|
|
245
|
+
for uname, (_, default) in program.shader.uniforms.items():
|
|
246
|
+
if default is not None:
|
|
247
|
+
full_uniforms[uname] = default
|
|
248
|
+
full_uniforms.update(uniforms)
|
|
249
|
+
|
|
250
|
+
return self._executor.execute(
|
|
251
|
+
program, texture_id, full_uniforms,
|
|
252
|
+
in_place=in_place, output_texture=output_texture,
|
|
253
|
+
output_framebuffer=output_framebuffer,
|
|
254
|
+
input_framebuffer=input_framebuffer,
|
|
255
|
+
output_size=output_size
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def chain(self) -> FilterChain:
|
|
259
|
+
"""Create a new filter chain."""
|
|
260
|
+
return FilterChain(self)
|
|
261
|
+
|
|
262
|
+
def compile_shader(self, name: str) -> CompiledProgram:
|
|
263
|
+
"""
|
|
264
|
+
Compile a shader by name.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
name: The shader name (snake_case)
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
The compiled program
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
KeyError: If the shader is not registered
|
|
274
|
+
ShaderCompilationError: If compilation fails
|
|
275
|
+
"""
|
|
276
|
+
shader = self._registry.get_shader(name)
|
|
277
|
+
if shader is None:
|
|
278
|
+
raise KeyError(f"No shader registered with name '{name}'")
|
|
279
|
+
|
|
280
|
+
shader_type = self._registry.get_type(shader.shader_type)
|
|
281
|
+
if shader_type is None:
|
|
282
|
+
raise KeyError(f"No shader type registered with name '{shader.shader_type}'")
|
|
283
|
+
|
|
284
|
+
compiled = self._compiler.compile(shader, shader_type)
|
|
285
|
+
self._compiled[name] = compiled
|
|
286
|
+
return compiled
|
|
287
|
+
|
|
288
|
+
def compile_all(self) -> Dict[str, CompiledProgram]:
|
|
289
|
+
"""
|
|
290
|
+
Compile all registered shaders.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dict mapping shader names to compiled programs
|
|
294
|
+
"""
|
|
295
|
+
results = {}
|
|
296
|
+
for name in self._registry.shaders.keys():
|
|
297
|
+
try:
|
|
298
|
+
results[name] = self.compile_shader(name)
|
|
299
|
+
except ShaderCompilationError as e:
|
|
300
|
+
print(f"Warning: Failed to compile '{name}': {e}")
|
|
301
|
+
return results
|
|
302
|
+
|
|
303
|
+
def _get_compiled(self, name: str) -> Optional[CompiledProgram]:
|
|
304
|
+
"""
|
|
305
|
+
Get a compiled program by name.
|
|
306
|
+
|
|
307
|
+
If auto_compile is enabled and the shader isn't compiled yet,
|
|
308
|
+
it will be compiled automatically.
|
|
309
|
+
"""
|
|
310
|
+
if name not in self._compiled and self._auto_compile:
|
|
311
|
+
if self._registry.get_shader(name) is not None:
|
|
312
|
+
return self.compile_shader(name)
|
|
313
|
+
return self._compiled.get(name)
|
|
314
|
+
|
|
315
|
+
def has_shader(self, name: str) -> bool:
|
|
316
|
+
"""Check if a shader is registered."""
|
|
317
|
+
return self._registry.get_shader(name) is not None
|
|
318
|
+
|
|
319
|
+
def list_shaders(self) -> List[str]:
|
|
320
|
+
"""List all registered shader names."""
|
|
321
|
+
return list(self._registry.shaders.keys())
|
|
322
|
+
|
|
323
|
+
def list_shader_types(self) -> List[str]:
|
|
324
|
+
"""List all registered shader type names."""
|
|
325
|
+
return list(self._registry.shader_types.keys())
|
|
326
|
+
|
|
327
|
+
def get_shader_info(self, name: str) -> Optional[Dict[str, Any]]:
|
|
328
|
+
"""
|
|
329
|
+
Get information about a registered shader.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
name: The shader name
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Dict with shader info, or None if not found
|
|
336
|
+
"""
|
|
337
|
+
shader = self._registry.get_shader(name)
|
|
338
|
+
if shader is None:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
'name': shader.name,
|
|
343
|
+
'shader_type': shader.shader_type,
|
|
344
|
+
'uniforms': {
|
|
345
|
+
uname: {'type': utype.value, 'default': default}
|
|
346
|
+
for uname, (utype, default) in shader.uniforms.items()
|
|
347
|
+
},
|
|
348
|
+
'compiled': name in self._compiled
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@contextmanager
|
|
352
|
+
def use_shader(self, name: str):
|
|
353
|
+
"""
|
|
354
|
+
Context manager for using a shader program.
|
|
355
|
+
|
|
356
|
+
Useful for manual rendering when you need more control.
|
|
357
|
+
|
|
358
|
+
Usage:
|
|
359
|
+
with filter.use_shader('brightness_contrast') as program:
|
|
360
|
+
# Set up your own rendering...
|
|
361
|
+
pass
|
|
362
|
+
"""
|
|
363
|
+
from OpenGL import GL
|
|
364
|
+
|
|
365
|
+
program = self._get_compiled(name)
|
|
366
|
+
if program is None:
|
|
367
|
+
raise KeyError(f"Shader '{name}' not found or failed to compile")
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
GL.glUseProgram(program.program_id)
|
|
371
|
+
yield program
|
|
372
|
+
finally:
|
|
373
|
+
GL.glUseProgram(0)
|
|
374
|
+
|
|
375
|
+
def clear_texture_cache(self) -> None:
|
|
376
|
+
"""
|
|
377
|
+
Clear all cached output textures.
|
|
378
|
+
|
|
379
|
+
This frees up GPU memory by deleting all textures in the cache.
|
|
380
|
+
Call this periodically if you're concerned about memory usage.
|
|
381
|
+
"""
|
|
382
|
+
self._executor.clear_texture_cache()
|
|
383
|
+
|
|
384
|
+
def clear_fbo_cache(self) -> None:
|
|
385
|
+
"""
|
|
386
|
+
Clear all cached FBOs and temp textures.
|
|
387
|
+
|
|
388
|
+
This frees up GPU resources by deleting all cached framebuffers
|
|
389
|
+
and their associated temporary textures.
|
|
390
|
+
"""
|
|
391
|
+
self._executor.clear_fbo_cache()
|
|
392
|
+
|
|
393
|
+
def normalize(self, texture_id: int, in_place: bool = False,
|
|
394
|
+
output_texture: Optional[int] = None, min_value=None, max_value=None) -> Any:
|
|
395
|
+
"""
|
|
396
|
+
Normalize texture values to [0, 1] range by calculating min/max.
|
|
397
|
+
|
|
398
|
+
This uses GPU-based parallel reduction to efficiently find the global
|
|
399
|
+
min and max values without reading all pixels. The texture is progressively
|
|
400
|
+
downsampled until reaching a 1x1 pixel, then that single pixel is read
|
|
401
|
+
to get the min/max values.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
texture_id: Input texture ID
|
|
405
|
+
in_place: If True, modify the input texture directly
|
|
406
|
+
output_texture: Optional specific output texture to render to
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
(output_texture_id, min_value, max_value). For the auto-computed
|
|
410
|
+
path min/max stay on the GPU and are returned as None — callers that
|
|
411
|
+
need the CPU values should call texture_min_max.get_texture_min_max.
|
|
412
|
+
"""
|
|
413
|
+
# The remap reads (min, max) from a 1x1 GPU texture instead of float
|
|
414
|
+
# uniforms, so the range never round-trips through the CPU (the old
|
|
415
|
+
# get_texture_min_max readback was a per-frame GPU sync stall). Both
|
|
416
|
+
# paths below leave the value in that texture, which normalize_remap
|
|
417
|
+
# samples.
|
|
418
|
+
if min_value is not None and max_value is not None:
|
|
419
|
+
# Explicit range: tiny upload, no reduction.
|
|
420
|
+
min_max_tex = texture_min_max.set_min_max_texture(texture_id, min_value, max_value)
|
|
421
|
+
elif min_value is None and max_value is None:
|
|
422
|
+
# Auto range: GPU reduction to single result texel, no readback.
|
|
423
|
+
min_max_tex = texture_min_max.compute_min_max_to_texture(texture_id)
|
|
424
|
+
else:
|
|
425
|
+
# Partial override (rare): the GPU path can't blend a caller value
|
|
426
|
+
# with a computed one, so fall back to the CPU reduction to get the
|
|
427
|
+
# missing bound, then upload the resolved range.
|
|
428
|
+
mn, mx = texture_min_max.get_texture_min_max(texture_id)
|
|
429
|
+
if min_value is None:
|
|
430
|
+
min_value = mn
|
|
431
|
+
if max_value is None:
|
|
432
|
+
max_value = mx
|
|
433
|
+
min_max_tex = texture_min_max.set_min_max_texture(texture_id, min_value, max_value)
|
|
434
|
+
|
|
435
|
+
result = self.apply(
|
|
436
|
+
'normalize_remap',
|
|
437
|
+
texture_id,
|
|
438
|
+
in_place=in_place,
|
|
439
|
+
output_texture=output_texture,
|
|
440
|
+
min_max_tex=min_max_tex,
|
|
441
|
+
)
|
|
442
|
+
return result, min_value, max_value
|
|
443
|
+
|
|
444
|
+
def get_cache_stats(self) -> Dict[str, Any]:
|
|
445
|
+
"""
|
|
446
|
+
Get statistics about all caches.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dict with the following keys:
|
|
450
|
+
- 'textures': Dict mapping input_texture_id -> (output_texture_id, width, height)
|
|
451
|
+
- 'fbos': Dict mapping (width, height) -> True (one FBO per size)
|
|
452
|
+
- 'temp_textures': Dict mapping (width, height) -> True (one temp per size)
|
|
453
|
+
"""
|
|
454
|
+
return {
|
|
455
|
+
'textures': dict(self._executor._texture_cache),
|
|
456
|
+
'fbos': {
|
|
457
|
+
size: True
|
|
458
|
+
for size in self._executor._fbo_cache.keys()
|
|
459
|
+
},
|
|
460
|
+
'temp_textures': {
|
|
461
|
+
size: True
|
|
462
|
+
for size in self._executor._temp_texture_cache.keys()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
def cleanup(self) -> None:
|
|
467
|
+
"""Clean up all OpenGL resources."""
|
|
468
|
+
self._compiler.cleanup()
|
|
469
|
+
self._executor.cleanup()
|
|
470
|
+
self._compiled.clear()
|
|
471
|
+
|
|
472
|
+
def __enter__(self) -> 'Filter':
|
|
473
|
+
"""Context manager entry."""
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
477
|
+
"""Context manager exit - cleanup resources."""
|
|
478
|
+
self.cleanup()
|
|
479
|
+
|
|
480
|
+
def __dir__(self):
|
|
481
|
+
"""List available attributes for tab completion."""
|
|
482
|
+
return (list(self._registry.shaders.keys()) +
|
|
483
|
+
['apply', 'chain', 'compile_shader', 'compile_all',
|
|
484
|
+
'has_shader', 'list_shaders', 'list_shader_types',
|
|
485
|
+
'get_shader_info', 'use_shader', 'normalize',
|
|
486
|
+
'clear_texture_cache', 'clear_fbo_cache', 'get_cache_stats', 'cleanup'])
|
|
487
|
+
|
|
488
|
+
def __repr__(self) -> str:
|
|
489
|
+
n_shaders = len(self._registry.shaders)
|
|
490
|
+
n_types = len(self._registry.shader_types)
|
|
491
|
+
n_compiled = len(self._compiled)
|
|
492
|
+
return f"Filter(shaders={n_shaders}, types={n_types}, compiled={n_compiled})"
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
import numpy as np
|
|
496
|
+
from OpenGL.GL import *
|
|
497
|
+
from OpenGL.GL import shaders
|
|
498
|
+
import ctypes
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class TextureMinMax:
|
|
502
|
+
"""Compute shader-based min/max finder for OpenGL textures."""
|
|
503
|
+
|
|
504
|
+
COMPUTE_SHADER_SOURCE = """
|
|
505
|
+
#version 430
|
|
506
|
+
layout(local_size_x = 16, local_size_y = 16) in;
|
|
507
|
+
|
|
508
|
+
layout(rgba32f, binding = 0) readonly uniform image2D inputImage;
|
|
509
|
+
|
|
510
|
+
layout(std430, binding = 1) buffer ResultBuffer {
|
|
511
|
+
vec2 workgroupResults[]; // Each workgroup writes (min, max)
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
uniform ivec2 numWorkgroups;
|
|
515
|
+
|
|
516
|
+
shared float localMin[256];
|
|
517
|
+
shared float localMax[256];
|
|
518
|
+
|
|
519
|
+
void main() {
|
|
520
|
+
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
|
|
521
|
+
ivec2 size = imageSize(inputImage);
|
|
522
|
+
uint lid = gl_LocalInvocationIndex;
|
|
523
|
+
|
|
524
|
+
// Initialize with extreme values
|
|
525
|
+
float myMin = 1e38;
|
|
526
|
+
float myMax = -1e38;
|
|
527
|
+
|
|
528
|
+
if (pos.x < size.x && pos.y < size.y) {
|
|
529
|
+
vec4 pixel = imageLoad(inputImage, pos);
|
|
530
|
+
// Get min/max across RGB channels (ignore alpha)
|
|
531
|
+
myMin = min(min(pixel.r, pixel.g), pixel.b);
|
|
532
|
+
myMax = max(max(pixel.r, pixel.g), pixel.b);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
localMin[lid] = myMin;
|
|
536
|
+
localMax[lid] = myMax;
|
|
537
|
+
barrier();
|
|
538
|
+
|
|
539
|
+
// Parallel reduction in shared memory
|
|
540
|
+
for (uint s = 128; s > 0; s >>= 1) {
|
|
541
|
+
if (lid < s) {
|
|
542
|
+
localMin[lid] = min(localMin[lid], localMin[lid + s]);
|
|
543
|
+
localMax[lid] = max(localMax[lid], localMax[lid + s]);
|
|
544
|
+
}
|
|
545
|
+
barrier();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Workgroup leader writes result
|
|
549
|
+
if (lid == 0) {
|
|
550
|
+
uint workgroupIndex = gl_WorkGroupID.y * numWorkgroups.x + gl_WorkGroupID.x;
|
|
551
|
+
workgroupResults[workgroupIndex] = vec2(localMin[0], localMax[0]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
def __init__(self):
|
|
557
|
+
self._program = None
|
|
558
|
+
self._ssbo = None
|
|
559
|
+
self._initialized = False
|
|
560
|
+
|
|
561
|
+
def _ensure_initialized(self):
|
|
562
|
+
"""Lazy initialization of shader and buffer."""
|
|
563
|
+
if self._initialized:
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
# Compile compute shader
|
|
567
|
+
compute_shader = shaders.compileShader(
|
|
568
|
+
self.COMPUTE_SHADER_SOURCE,
|
|
569
|
+
GL_COMPUTE_SHADER
|
|
570
|
+
)
|
|
571
|
+
self._program = shaders.compileProgram(compute_shader)
|
|
572
|
+
|
|
573
|
+
# Create SSBO (will resize as needed)
|
|
574
|
+
self._ssbo = glGenBuffers(1)
|
|
575
|
+
self._ssbo_size = 0
|
|
576
|
+
|
|
577
|
+
self._initialized = True
|
|
578
|
+
|
|
579
|
+
def get_min_max(self, texture_id: int, width: int, height: int) -> tuple[float, float]:
|
|
580
|
+
"""
|
|
581
|
+
Compute min/max pixel values of an OpenGL texture.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
texture_id: OpenGL texture ID (must be rgba32f format)
|
|
585
|
+
width: Texture width in pixels
|
|
586
|
+
height: Texture height in pixels
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Tuple of (min_value, max_value) across RGB channels
|
|
590
|
+
"""
|
|
591
|
+
self._ensure_initialized()
|
|
592
|
+
|
|
593
|
+
# Calculate workgroup dimensions
|
|
594
|
+
workgroup_size = 16
|
|
595
|
+
num_workgroups_x = (width + workgroup_size - 1) // workgroup_size
|
|
596
|
+
num_workgroups_y = (height + workgroup_size - 1) // workgroup_size
|
|
597
|
+
total_workgroups = num_workgroups_x * num_workgroups_y
|
|
598
|
+
|
|
599
|
+
# Resize SSBO if needed (2 floats per workgroup: min and max)
|
|
600
|
+
required_size = total_workgroups * 2 * 4 # 2 floats * 4 bytes
|
|
601
|
+
if required_size > self._ssbo_size:
|
|
602
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self._ssbo)
|
|
603
|
+
glBufferData(GL_SHADER_STORAGE_BUFFER, required_size, None, GL_DYNAMIC_READ)
|
|
604
|
+
self._ssbo_size = required_size
|
|
605
|
+
|
|
606
|
+
# Bind resources
|
|
607
|
+
glUseProgram(self._program)
|
|
608
|
+
glBindImageTexture(0, texture_id, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F)
|
|
609
|
+
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, self._ssbo)
|
|
610
|
+
|
|
611
|
+
# Set uniforms
|
|
612
|
+
loc = glGetUniformLocation(self._program, "numWorkgroups")
|
|
613
|
+
glUniform2i(loc, num_workgroups_x, num_workgroups_y)
|
|
614
|
+
|
|
615
|
+
# Dispatch compute shader
|
|
616
|
+
glDispatchCompute(num_workgroups_x, num_workgroups_y, 1)
|
|
617
|
+
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT)
|
|
618
|
+
|
|
619
|
+
# Read back results
|
|
620
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self._ssbo)
|
|
621
|
+
results = np.empty(total_workgroups * 2, dtype=np.float32)
|
|
622
|
+
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, results.nbytes, results)
|
|
623
|
+
|
|
624
|
+
# Final reduction on CPU (very fast for small arrays)
|
|
625
|
+
results = results.reshape(-1, 2)
|
|
626
|
+
min_val = float(np.min(results[:, 0]))
|
|
627
|
+
max_val = float(np.max(results[:, 1]))
|
|
628
|
+
|
|
629
|
+
return min_val, max_val
|
|
630
|
+
|
|
631
|
+
def cleanup(self):
|
|
632
|
+
"""Delete OpenGL resources."""
|
|
633
|
+
if self._ssbo is not None:
|
|
634
|
+
glDeleteBuffers(1, [self._ssbo])
|
|
635
|
+
self._ssbo = None
|
|
636
|
+
if self._program is not None:
|
|
637
|
+
glDeleteProgram(self._program)
|
|
638
|
+
self._program = None
|
|
639
|
+
self._initialized = False
|
|
640
|
+
|
|
641
|
+
def __del__(self):
|
|
642
|
+
# Note: OpenGL context must still be active for this to work
|
|
643
|
+
# Prefer explicit cleanup() call
|
|
644
|
+
pass
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# Convenience functions for one global use
|
|
648
|
+
_global_instance = None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def get_texture_min_max(texture_id: int, width: int, height: int) -> tuple[float, float]:
|
|
652
|
+
"""
|
|
653
|
+
Convenience function to get min/max of a texture.
|
|
654
|
+
Uses a cached shader program for efficiency.
|
|
655
|
+
"""
|
|
656
|
+
global _global_instance
|
|
657
|
+
if _global_instance is None:
|
|
658
|
+
_global_instance = TextureMinMax()
|
|
659
|
+
return _global_instance.get_min_max(texture_id, width, height)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def cleanup_texture_min_max():
|
|
663
|
+
"""Cleanup global resources."""
|
|
664
|
+
global _global_instance
|
|
665
|
+
if _global_instance is not None:
|
|
666
|
+
_global_instance.cleanup()
|
|
667
|
+
_global_instance = None
|