feat(shader): Add BlurPipeline class for frosted glass FBO setup
- Add BlurPipeline class with downsampled FBO support (scene, blur_a, blur_b) - Implement 2-pass Gaussian blur shaders (horizontal + vertical) - Add setup_fbos(), compile_blur_shaders(), prepare_blur(), cleanup() methods - Add tests for BlurPipeline initialization, FBO setup, shader compilation, blur execution, and cleanup Task: Phase 1, Task 1 of frosted_glass_20260313 track
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: Tier 2 Tech Lead for architectural design and track execution with persistent memory
|
description: Tier 2 Tech Lead for architectural design and track execution with persistent memory
|
||||||
mode: primary
|
mode: primary
|
||||||
model: MiniMax-M2.5
|
|
||||||
temperature: 0.4
|
temperature: 0.4
|
||||||
permission:
|
permission:
|
||||||
edit: ask
|
edit: ask
|
||||||
@@ -14,9 +13,9 @@ ONLY output the requested text. No pleasantries.
|
|||||||
|
|
||||||
## Context Management
|
## Context Management
|
||||||
|
|
||||||
**MANUAL COMPACTION ONLY** — Never rely on automatic context summarization.
|
**MANUAL COMPACTION ONLY** <EFBFBD> Never rely on automatic context summarization.
|
||||||
Use `/compact` command explicitly when context needs reduction.
|
Use `/compact` command explicitly when context needs reduction.
|
||||||
You maintain PERSISTENT MEMORY throughout track execution — do NOT apply Context Amnesia to your own session.
|
You maintain PERSISTENT MEMORY throughout track execution <EFBFBD> do NOT apply Context Amnesia to your own session.
|
||||||
|
|
||||||
## CRITICAL: MCP Tools Only (Native Tools Banned)
|
## CRITICAL: MCP Tools Only (Native Tools Banned)
|
||||||
|
|
||||||
@@ -134,14 +133,14 @@ Before implementing:
|
|||||||
- Zero-assertion ban: Tests MUST have meaningful assertions
|
- Zero-assertion ban: Tests MUST have meaningful assertions
|
||||||
- Delegate test creation to Tier 3 Worker via Task tool
|
- Delegate test creation to Tier 3 Worker via Task tool
|
||||||
- Run tests and confirm they FAIL as expected
|
- Run tests and confirm they FAIL as expected
|
||||||
- **CONFIRM FAILURE** — this is the Red phase
|
- **CONFIRM FAILURE** <EFBFBD> this is the Red phase
|
||||||
|
|
||||||
### 3. Green Phase: Implement to Pass
|
### 3. Green Phase: Implement to Pass
|
||||||
|
|
||||||
- **Pre-delegation checkpoint**: Stage current progress (`git add .`)
|
- **Pre-delegation checkpoint**: Stage current progress (`git add .`)
|
||||||
- Delegate implementation to Tier 3 Worker via Task tool
|
- Delegate implementation to Tier 3 Worker via Task tool
|
||||||
- Run tests and confirm they PASS
|
- Run tests and confirm they PASS
|
||||||
- **CONFIRM PASS** — this is the Green phase
|
- **CONFIRM PASS** <EFBFBD> this is the Green phase
|
||||||
|
|
||||||
### 4. Refactor Phase (Optional)
|
### 4. Refactor Phase (Optional)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Implementation Plan: Frosted Glass Background Effect (REPAIR - TRUE GPU)
|
# Implementation Plan: Frosted Glass Background Effect (REPAIR - TRUE GPU)
|
||||||
|
|
||||||
## Phase 1: Robust Shader & FBO Foundation
|
## Phase 1: Robust Shader & FBO Foundation
|
||||||
- [ ] Task: Implement: Create `ShaderManager` methods for downsampled FBO setup (scene, temp, blur).
|
- [~] Task: Implement: Create `ShaderManager` methods for downsampled FBO setup (scene, temp, blur).
|
||||||
- [ ] Task: Implement: Develop the "Deep Sea" background shader and integrate it as the FBO source.
|
- [ ] Task: Implement: Develop the "Deep Sea" background shader and integrate it as the FBO source.
|
||||||
- [ ] Task: Implement: Develop the 2-pass Gaussian blur shaders with a wide tap distribution.
|
- [ ] Task: Implement: Develop the 2-pass Gaussian blur shaders with a wide tap distribution.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Robust Foundation' (Protocol in workflow.md)
|
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Robust Foundation' (Protocol in workflow.md)
|
||||||
|
|||||||
@@ -150,4 +150,182 @@ void main() {
|
|||||||
gl.glUniform1f(u_time_loc, float(time))
|
gl.glUniform1f(u_time_loc, float(time))
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
||||||
|
|
||||||
|
class BlurPipeline:
|
||||||
|
def __init__(self):
|
||||||
|
self.scene_fbo: int | None = None
|
||||||
|
self.scene_tex: int | None = None
|
||||||
|
self.blur_fbo_a: int | None = None
|
||||||
|
self.blur_tex_a: int | None = None
|
||||||
|
self.blur_fbo_b: int | None = None
|
||||||
|
self.blur_tex_b: int | None = None
|
||||||
|
self.h_blur_program: int | None = None
|
||||||
|
self.v_blur_program: int | None = None
|
||||||
|
self._fb_width: int = 0
|
||||||
|
self._fb_height: int = 0
|
||||||
|
|
||||||
|
def _compile_shader(self, vertex_src: str, fragment_src: str) -> int:
|
||||||
|
program = gl.glCreateProgram()
|
||||||
|
def _compile(src, shader_type):
|
||||||
|
shader = gl.glCreateShader(shader_type)
|
||||||
|
gl.glShaderSource(shader, src)
|
||||||
|
gl.glCompileShader(shader)
|
||||||
|
if not gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS):
|
||||||
|
info_log = gl.glGetShaderInfoLog(shader)
|
||||||
|
if hasattr(info_log, "decode"):
|
||||||
|
info_log = info_log.decode()
|
||||||
|
raise RuntimeError(f"Shader compilation failed: {info_log}")
|
||||||
|
return shader
|
||||||
|
vert_shader = _compile(vertex_src, gl.GL_VERTEX_SHADER)
|
||||||
|
frag_shader = _compile(fragment_src, gl.GL_FRAGMENT_SHADER)
|
||||||
|
gl.glAttachShader(program, vert_shader)
|
||||||
|
gl.glAttachShader(program, frag_shader)
|
||||||
|
gl.glLinkProgram(program)
|
||||||
|
if not gl.glGetProgramiv(program, gl.GL_LINK_STATUS):
|
||||||
|
info_log = gl.glGetProgramInfoLog(program)
|
||||||
|
if hasattr(info_log, "decode"):
|
||||||
|
info_log = info_log.decode()
|
||||||
|
raise RuntimeError(f"Program linking failed: {info_log}")
|
||||||
|
gl.glDeleteShader(vert_shader)
|
||||||
|
gl.glDeleteShader(frag_shader)
|
||||||
|
return program
|
||||||
|
|
||||||
|
def _create_fbo(self, width: int, height: int) -> tuple[int, int]:
|
||||||
|
tex = gl.glGenTextures(1)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
|
||||||
|
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA8, 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.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
|
||||||
|
fbo = gl.glGenFramebuffers(1)
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
|
||||||
|
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, tex, 0)
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
||||||
|
return fbo, tex
|
||||||
|
|
||||||
|
def setup_fbos(self, width: int, height: int):
|
||||||
|
blur_w = max(1, width // 4)
|
||||||
|
blur_h = max(1, height // 4)
|
||||||
|
self._fb_width = blur_w
|
||||||
|
self._fb_height = blur_h
|
||||||
|
self.scene_fbo, self.scene_tex = self._create_fbo(width, height)
|
||||||
|
self.blur_fbo_a, self.blur_tex_a = self._create_fbo(blur_w, blur_h)
|
||||||
|
self.blur_fbo_b, self.blur_tex_b = self._create_fbo(blur_w, blur_h)
|
||||||
|
|
||||||
|
def compile_blur_shaders(self):
|
||||||
|
vert_src = """
|
||||||
|
#version 330 core
|
||||||
|
out vec2 v_uv;
|
||||||
|
void main() {
|
||||||
|
vec2 pos = vec2(-1.0, -1.0);
|
||||||
|
vec2 uv = vec2(0.0, 0.0);
|
||||||
|
if (gl_VertexID == 1) { pos = vec2(1.0, -1.0); uv = vec2(1.0, 0.0); }
|
||||||
|
else if (gl_VertexID == 2) { pos = vec2(-1.0, 1.0); uv = vec2(0.0, 1.0); }
|
||||||
|
else if (gl_VertexID == 3) { pos = vec2(1.0, 1.0); uv = vec2(1.0, 1.0); }
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
v_uv = uv;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
h_frag_src = """
|
||||||
|
#version 330 core
|
||||||
|
in vec2 v_uv;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform vec2 u_texel_size;
|
||||||
|
out vec4 FragColor;
|
||||||
|
void main() {
|
||||||
|
vec2 offset = vec2(u_texel_size.x, 0.0);
|
||||||
|
vec4 sum = vec4(0.0);
|
||||||
|
sum += texture(u_texture, v_uv - offset * 4.0) * 0.051;
|
||||||
|
sum += texture(u_texture, v_uv - offset * 3.0) * 0.0918;
|
||||||
|
sum += texture(u_texture, v_uv - offset * 2.0) * 0.12245;
|
||||||
|
sum += texture(u_texture, v_uv - offset * 1.0) * 0.1531;
|
||||||
|
sum += texture(u_texture, v_uv) * 0.1633;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 1.0) * 0.1531;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 2.0) * 0.12245;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 3.0) * 0.0918;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 4.0) * 0.051;
|
||||||
|
FragColor = sum;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
v_frag_src = """
|
||||||
|
#version 330 core
|
||||||
|
in vec2 v_uv;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform vec2 u_texel_size;
|
||||||
|
out vec4 FragColor;
|
||||||
|
void main() {
|
||||||
|
vec2 offset = vec2(0.0, u_texel_size.y);
|
||||||
|
vec4 sum = vec4(0.0);
|
||||||
|
sum += texture(u_texture, v_uv - offset * 4.0) * 0.051;
|
||||||
|
sum += texture(u_texture, v_uv - offset * 3.0) * 0.0918;
|
||||||
|
sum += texture(u_texture, v_uv - offset * 2.0) * 0.12245;
|
||||||
|
sum += texture(u_texture, v_uv - offset * 1.0) * 0.1531;
|
||||||
|
sum += texture(u_texture, v_uv) * 0.1633;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 1.0) * 0.1531;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 2.0) * 0.12245;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 3.0) * 0.0918;
|
||||||
|
sum += texture(u_texture, v_uv + offset * 4.0) * 0.051;
|
||||||
|
FragColor = sum;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
self.h_blur_program = self._compile_shader(vert_src, h_frag_src)
|
||||||
|
self.v_blur_program = self._compile_shader(vert_src, v_frag_src)
|
||||||
|
|
||||||
|
def _render_quad(self, program: int, src_tex: int, texel_size: tuple[float, float]):
|
||||||
|
gl.glUseProgram(program)
|
||||||
|
gl.glActiveTexture(gl.GL_TEXTURE0)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, src_tex)
|
||||||
|
u_tex = gl.glGetUniformLocation(program, "u_texture")
|
||||||
|
if u_tex != -1:
|
||||||
|
gl.glUniform1i(u_tex, 0)
|
||||||
|
u_ts = gl.glGetUniformLocation(program, "u_texel_size")
|
||||||
|
if u_ts != -1:
|
||||||
|
gl.glUniform2f(u_ts, texel_size[0], texel_size[1])
|
||||||
|
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
||||||
gl.glUseProgram(0)
|
gl.glUseProgram(0)
|
||||||
|
|
||||||
|
def prepare_blur(self, width: int, height: int, time: float):
|
||||||
|
if not self.h_blur_program or not self.v_blur_program:
|
||||||
|
return
|
||||||
|
if not self.blur_fbo_a or not self.blur_fbo_b:
|
||||||
|
return
|
||||||
|
blur_w = max(1, self._fb_width)
|
||||||
|
blur_h = max(1, self._fb_height)
|
||||||
|
texel_x = 1.0 / float(blur_w)
|
||||||
|
texel_y = 1.0 / float(blur_h)
|
||||||
|
gl.glViewport(0, 0, blur_w, blur_h)
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo_a)
|
||||||
|
gl.glClearColor(0.0, 0.0, 0.0, 0.0)
|
||||||
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
||||||
|
self._render_quad(self.h_blur_program, self.scene_tex, (texel_x, texel_y))
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo_b)
|
||||||
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
||||||
|
self._render_quad(self.v_blur_program, self.blur_tex_a, (texel_x, texel_y))
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
||||||
|
gl.glViewport(0, 0, width, height)
|
||||||
|
|
||||||
|
def get_blur_texture(self) -> int | None:
|
||||||
|
return self.blur_tex_b
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
fbos = [f for f in [self.scene_fbo, self.blur_fbo_a, self.blur_fbo_b] if f is not None]
|
||||||
|
texs = [t for t in [self.scene_tex, self.blur_tex_a, self.blur_tex_b] if t is not None]
|
||||||
|
progs = [p for p in [self.h_blur_program, self.v_blur_program] if p is not None]
|
||||||
|
if fbos:
|
||||||
|
gl.glDeleteFramebuffers(len(fbos), fbos)
|
||||||
|
if texs:
|
||||||
|
gl.glDeleteTextures(len(texs), texs)
|
||||||
|
if progs:
|
||||||
|
for p in progs:
|
||||||
|
gl.glDeleteProgram(p)
|
||||||
|
self.scene_fbo = None
|
||||||
|
self.scene_tex = None
|
||||||
|
self.blur_fbo_a = None
|
||||||
|
self.blur_tex_a = None
|
||||||
|
self.blur_fbo_b = None
|
||||||
|
self.blur_tex_b = None
|
||||||
|
self.h_blur_program = None
|
||||||
|
self.v_blur_program = None
|
||||||
|
|||||||
@@ -1,6 +1,83 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
def test_blur_pipeline_import():
|
||||||
|
with patch("src.shader_manager.gl") as mock_gl:
|
||||||
|
from src.shader_manager import BlurPipeline
|
||||||
|
pipeline = BlurPipeline()
|
||||||
|
assert pipeline is not None
|
||||||
|
assert pipeline.scene_fbo is None
|
||||||
|
assert pipeline.blur_fbo_a is None
|
||||||
|
assert pipeline.blur_fbo_b is None
|
||||||
|
assert pipeline.scene_tex is None
|
||||||
|
assert pipeline.blur_tex_a is None
|
||||||
|
assert pipeline.blur_tex_b is None
|
||||||
|
assert pipeline.h_blur_program is None
|
||||||
|
assert pipeline.v_blur_program is None
|
||||||
|
|
||||||
|
def test_blur_pipeline_setup_fbos():
|
||||||
|
with patch("src.shader_manager.gl") as mock_gl:
|
||||||
|
tex_counter = iter([10, 20, 30])
|
||||||
|
fbo_counter = iter([1, 2, 3])
|
||||||
|
mock_gl.glGenTextures.side_effect = lambda n: next(tex_counter)
|
||||||
|
mock_gl.glGenFramebuffers.side_effect = lambda n: next(fbo_counter)
|
||||||
|
from src.shader_manager import BlurPipeline
|
||||||
|
pipeline = BlurPipeline()
|
||||||
|
pipeline.setup_fbos(800, 600)
|
||||||
|
assert mock_gl.glGenFramebuffers.called
|
||||||
|
assert mock_gl.glGenTextures.called
|
||||||
|
assert pipeline.scene_fbo is not None
|
||||||
|
assert pipeline.blur_fbo_a is not None
|
||||||
|
assert pipeline.blur_fbo_b is not None
|
||||||
|
|
||||||
|
def test_blur_pipeline_compile_shaders():
|
||||||
|
with patch("src.shader_manager.gl") as mock_gl:
|
||||||
|
mock_gl.glCreateProgram.return_value = 100
|
||||||
|
mock_gl.glCreateShader.return_value = 200
|
||||||
|
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
||||||
|
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
||||||
|
from src.shader_manager import BlurPipeline
|
||||||
|
pipeline = BlurPipeline()
|
||||||
|
pipeline.compile_blur_shaders()
|
||||||
|
assert mock_gl.glCreateProgram.called
|
||||||
|
assert pipeline.h_blur_program is not None
|
||||||
|
assert pipeline.v_blur_program is not None
|
||||||
|
|
||||||
|
def test_blur_pipeline_prepare_blur():
|
||||||
|
with patch("src.shader_manager.gl") as mock_gl:
|
||||||
|
mock_gl.glGenFramebuffers.return_value = None
|
||||||
|
mock_gl.glGenTextures.return_value = None
|
||||||
|
from src.shader_manager import BlurPipeline
|
||||||
|
pipeline = BlurPipeline()
|
||||||
|
pipeline.scene_fbo = 1
|
||||||
|
pipeline.scene_tex = 10
|
||||||
|
pipeline.blur_fbo_a = 2
|
||||||
|
pipeline.blur_tex_a = 20
|
||||||
|
pipeline.blur_fbo_b = 3
|
||||||
|
pipeline.blur_tex_b = 30
|
||||||
|
pipeline.h_blur_program = 100
|
||||||
|
pipeline.v_blur_program = 101
|
||||||
|
pipeline.prepare_blur(800, 600, 0.0)
|
||||||
|
assert mock_gl.glBindFramebuffer.called
|
||||||
|
assert mock_gl.glUseProgram.called
|
||||||
|
|
||||||
|
def test_blur_pipeline_cleanup():
|
||||||
|
with patch("src.shader_manager.gl") as mock_gl:
|
||||||
|
from src.shader_manager import BlurPipeline
|
||||||
|
pipeline = BlurPipeline()
|
||||||
|
pipeline.scene_fbo = 1
|
||||||
|
pipeline.blur_fbo_a = 2
|
||||||
|
pipeline.blur_fbo_b = 3
|
||||||
|
pipeline.scene_tex = 10
|
||||||
|
pipeline.blur_tex_a = 20
|
||||||
|
pipeline.blur_tex_b = 30
|
||||||
|
pipeline.h_blur_program = 100
|
||||||
|
pipeline.v_blur_program = 101
|
||||||
|
pipeline.cleanup()
|
||||||
|
assert mock_gl.glDeleteFramebuffers.called
|
||||||
|
assert mock_gl.glDeleteTextures.called
|
||||||
|
assert mock_gl.glDeleteProgram.called
|
||||||
|
|
||||||
def test_shader_manager_initialization_and_compilation():
|
def test_shader_manager_initialization_and_compilation():
|
||||||
# Import inside test to allow patching OpenGL before import if needed
|
# Import inside test to allow patching OpenGL before import if needed
|
||||||
# In this case, we patch the OpenGL.GL functions used by ShaderManager
|
# In this case, we patch the OpenGL.GL functions used by ShaderManager
|
||||||
|
|||||||
Reference in New Issue
Block a user