From f297e7a3bd741a926db5ead481faf2b65f8e0a55 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 13 Mar 2026 14:48:43 -0400 Subject: [PATCH] feat(shaders): Implement FBO capture lifecycle in ShaderManager --- src/shader_manager.py | 32 ++++++++++++++++++++++++++++++++ tests/test_fbo_capture.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/test_fbo_capture.py diff --git a/src/shader_manager.py b/src/shader_manager.py index 512d474..fd4b67f 100644 --- a/src/shader_manager.py +++ b/src/shader_manager.py @@ -6,6 +6,38 @@ class ShaderManager: self.bg_program = None self.pp_program = None self.blur_program = None + self.capture_fbo = None + self.capture_tex = None + self.fbo_width = 0 + self.fbo_height = 0 + + def setup_capture_fbo(self, width, height): + if self.capture_fbo is not None: + gl.glDeleteFramebuffers(1, [self.capture_fbo]) + if self.capture_tex is not None: + gl.glDeleteTextures(1, [self.capture_tex]) + self.capture_fbo = gl.glGenFramebuffers(1) + self.capture_tex = gl.glGenTextures(1) + gl.glBindTexture(gl.GL_TEXTURE_2D, self.capture_tex) + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.capture_fbo) + gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.capture_tex, 0) + if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: + raise RuntimeError("Framebuffer not complete") + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) + self.fbo_width = width + self.fbo_height = height + + def capture_begin(self, width, height): + if self.capture_fbo is None or self.fbo_width != width or self.fbo_height != height: + self.setup_capture_fbo(width, height) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.capture_fbo) + gl.glViewport(0, 0, width, height) + + def capture_end(self): + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) def compile_shader(self, vertex_src: str, fragment_src: str) -> int: program = gl.glCreateProgram() diff --git a/tests/test_fbo_capture.py b/tests/test_fbo_capture.py new file mode 100644 index 0000000..129840a --- /dev/null +++ b/tests/test_fbo_capture.py @@ -0,0 +1,37 @@ +import pytest +from unittest.mock import patch, MagicMock +import OpenGL.GL as gl + +def test_shader_manager_fbo_initialization(): + with patch("src.shader_manager.gl") as mock_gl: + mock_gl.glGenFramebuffers.return_value = 1 + mock_gl.glGenTextures.return_value = 2 + mock_gl.glCheckFramebufferStatus.return_value = mock_gl.GL_FRAMEBUFFER_COMPLETE + + from src.shader_manager import ShaderManager + manager = ShaderManager() + + manager.setup_capture_fbo(800, 600) + + assert manager.capture_fbo == 1 + assert manager.capture_tex == 2 + assert mock_gl.glGenFramebuffers.called + assert mock_gl.glGenTextures.called + assert mock_gl.glCheckFramebufferStatus.called + +def test_shader_manager_capture_lifecycle(): + with patch("src.shader_manager.gl") as mock_gl: + mock_gl.glCheckFramebufferStatus.return_value = mock_gl.GL_FRAMEBUFFER_COMPLETE + from src.shader_manager import ShaderManager + manager = ShaderManager() + + # Ensure setup is called on first capture + manager.capture_begin(1024, 768) + assert manager.fbo_width == 1024 + assert manager.fbo_height == 768 + assert mock_gl.glBindFramebuffer.called + + mock_gl.glBindFramebuffer.reset_mock() + manager.capture_end() + # Verify unbind (glBindFramebuffer(..., 0)) + mock_gl.glBindFramebuffer.assert_called_with(mock_gl.GL_FRAMEBUFFER, 0)