#!/usr/bin/env python3 """ Mipmap Compatible Texture Sampling Deblocking Shader Testbed Copyright (C) 2026 Binomial LLC. LICENSE: Apache 2.0 Usage: python testbed.py shader.glsl block_w block_h mip0.png mip1.png [mip2.png ...] block_w, block_h: Block size in texels (e.g. 8 8 for 8x8 DCT blocks) Controls: Arrows Move quad left/right/up/down W / S Move closer / farther A / D Rotate yaw (cube mode) Q / E Rotate pitch (cube mode) C Toggle cube / quad mode B Bilinear filtering T Trilinear filtering P Point filtering R Reload shader 1 Toggle deblocking shader off/on 2 Toggle edge visualization (only when deblocking active) 3-4 Toggle shader const0.x/y/z/w (0 <-> 1) 5-8 Toggle shader const1.x/y/z/w (0 <-> 1) Space Reset to initial state Esc Quit """ import sys, os, importlib.util print("=== DIAG ===") print("exe:", sys.executable) print("ver:", sys.version) print("cwd:", os.getcwd()) print("glfw spec:", importlib.util.find_spec("glfw")) print("OpenGL spec:", importlib.util.find_spec("OpenGL")) print("============") import sys import ctypes import numpy as np from PIL import Image, ImageDraw, ImageFont from pathlib import Path import glfw from OpenGL.GL import * # ----------------------------------------------------------------------------- # Globals # ----------------------------------------------------------------------------- WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720 FOV_DEGREES = 90.0 Z_MIN = .40 Z_MAX = -50.0 Z_SPEED = 2.0 XY_SPEED = 1.5 ROT_SPEED = 90.0 # degrees per second # Block size (set from command line) BLOCK_WIDTH = 12 BLOCK_HEIGHT = 12 g_state = { 'x': 0.0, 'y': 0.0, 'z': -3.0, 'yaw': 0.0, 'pitch': 0.0, 'mode': 'QUAD', # 'QUAD' or 'CUBE' 'filter_mode': 'BILINEAR', 'shader_path': None, 'program': None, 'texture': None, 'tex_size': (0, 0), 'quad_vao': None, 'cube_vao': None, 'cube_index_count': 0, 'debug_vao': None, 'debug_texture': None, 'debug_dirty': True, 'last_time': 0.0, 'const0': [0.0, 0.0, 0.0, 0.0], 'const1': [0.0, 0.0, 0.0, 0.0], } INIT_X = 0.0 INIT_Y = 0.0 INIT_Z = -3.0 INIT_YAW = 0.0 INIT_PITCH = 0.0 INIT_CONST0 = [0.0, 0.0, 0.0, 0.0] INIT_CONST1 = [0.0, 0.0, 0.0, 0.0] # ----------------------------------------------------------------------------- # Shader Loading # ----------------------------------------------------------------------------- def parse_shader_file(path): """Parse shader file with #vertex and #fragment markers.""" text = Path(path).read_text() vertex_src = None fragment_src = None parts = text.split('#vertex') if len(parts) < 2: print(f"ERROR: No #vertex marker found in {path}") return None, None rest = parts[1] frag_parts = rest.split('#fragment') if len(frag_parts) < 2: print(f"ERROR: No #fragment marker found in {path}") return None, None vertex_src = frag_parts[0].strip() fragment_src = frag_parts[1].strip() return vertex_src, fragment_src def compile_shader(source, shader_type): """Compile a shader, return handle or None on error.""" shader = glCreateShader(shader_type) glShaderSource(shader, source) glCompileShader(shader) if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE: error = glGetShaderInfoLog(shader) if isinstance(error, bytes): error = error.decode('utf-8') type_name = "VERTEX" if shader_type == GL_VERTEX_SHADER else "FRAGMENT" print(f"{type_name} SHADER ERROR:\n{error}") glDeleteShader(shader) return None return shader def link_program(vertex_shader, fragment_shader): """Link shaders into program, return handle or None on error.""" program = glCreateProgram() glAttachShader(program, vertex_shader) glAttachShader(program, fragment_shader) glLinkProgram(program) if glGetProgramiv(program, GL_LINK_STATUS) != GL_TRUE: error = glGetProgramInfoLog(program) if isinstance(error, bytes): error = error.decode('utf-8') print(f"LINK ERROR:\n{error}") glDeleteProgram(program) return None return program def load_shader(path): """Load, compile, and link shader from file. Returns program or None.""" print(f"Loading shader: {path}") vertex_src, fragment_src = parse_shader_file(path) if vertex_src is None or fragment_src is None: return None vertex_shader = compile_shader(vertex_src, GL_VERTEX_SHADER) if vertex_shader is None: return None fragment_shader = compile_shader(fragment_src, GL_FRAGMENT_SHADER) if fragment_shader is None: glDeleteShader(vertex_shader) return None program = link_program(vertex_shader, fragment_shader) glDeleteShader(vertex_shader) glDeleteShader(fragment_shader) if program: print("Shader compiled successfully.") return program def reload_shader(): """Attempt to reload shader. Keep old one if failed.""" new_program = load_shader(g_state['shader_path']) if new_program is not None: if g_state['program'] is not None: glDeleteProgram(g_state['program']) g_state['program'] = new_program else: print("Shader reload failed, keeping previous shader.") # ----------------------------------------------------------------------------- # Texture Loading # ----------------------------------------------------------------------------- def load_mipmap_texture(paths): """Load PNG files as mipmap levels. Returns texture handle and base size.""" images = [] for i, path in enumerate(paths): img = Image.open(path).convert('RGBA') images.append(img) print(f"Loaded mip {i}: {path} ({img.width}x{img.height})") # Validate dimensions for i in range(1, len(images)): expected_w = images[i - 1].width // 2 expected_h = images[i - 1].height // 2 actual_w = images[i].width actual_h = images[i].height if actual_w != expected_w or actual_h != expected_h: print(f"ERROR: Mip {i} should be {expected_w}x{expected_h}, got {actual_w}x{actual_h}") sys.exit(1) # Create texture texture = glGenTextures(1) glBindTexture(GL_TEXTURE_2D, texture) # Upload each mip level for level, img in enumerate(images): data = np.array(img, dtype=np.uint8) glTexImage2D( GL_TEXTURE_2D, level, GL_RGBA8, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data ) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, len(images) - 1) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) base_size = (images[0].width, images[0].height) return texture, base_size def set_filter_mode(mode): """Set texture filtering mode.""" glBindTexture(GL_TEXTURE_2D, g_state['texture']) if mode == 'BILINEAR': glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) elif mode == 'TRILINEAR': glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) else: # POINT glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) g_state['filter_mode'] = mode g_state['debug_dirty'] = True # ----------------------------------------------------------------------------- # Geometry # ----------------------------------------------------------------------------- def create_quad(aspect_ratio): """Create a quad VAO centered at origin with given aspect ratio.""" # Normalize so longest dimension is 1.0 if aspect_ratio >= 1.0: half_w = 1.0 half_h = 1.0 / aspect_ratio else: half_w = aspect_ratio half_h = 1.0 # Position (x, y, z) + UV (u, v) vertices = np.array([ -half_w, -half_h, 0.0, 0.0, 1.0, half_w, -half_h, 0.0, 1.0, 1.0, half_w, half_h, 0.0, 1.0, 0.0, -half_w, half_h, 0.0, 0.0, 0.0, ], dtype=np.float32) indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32) vao = glGenVertexArrays(1) vbo = glGenBuffers(1) ebo = glGenBuffers(1) glBindVertexArray(vao) glBindBuffer(GL_ARRAY_BUFFER, vbo) glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo) glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW) # Position attribute (location 0) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(0)) glEnableVertexAttribArray(0) # UV attribute (location 1) glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(12)) glEnableVertexAttribArray(1) glBindVertexArray(0) return vao def create_cube(size=1.0): """Create a textured cube VAO centered at origin.""" h = size / 2.0 # Each face: 4 vertices with position (x,y,z) + UV (u,v) # Front face (z = +h) front = [ -h, -h, h, 0.0, 1.0, h, -h, h, 1.0, 1.0, h, h, h, 1.0, 0.0, -h, h, h, 0.0, 0.0, ] # Back face (z = -h) back = [ h, -h, -h, 0.0, 1.0, -h, -h, -h, 1.0, 1.0, -h, h, -h, 1.0, 0.0, h, h, -h, 0.0, 0.0, ] # Right face (x = +h) right = [ h, -h, h, 0.0, 1.0, h, -h, -h, 1.0, 1.0, h, h, -h, 1.0, 0.0, h, h, h, 0.0, 0.0, ] # Left face (x = -h) left = [ -h, -h, -h, 0.0, 1.0, -h, -h, h, 1.0, 1.0, -h, h, h, 1.0, 0.0, -h, h, -h, 0.0, 0.0, ] # Top face (y = +h) top = [ -h, h, h, 0.0, 1.0, h, h, h, 1.0, 1.0, h, h, -h, 1.0, 0.0, -h, h, -h, 0.0, 0.0, ] # Bottom face (y = -h) bottom = [ -h, -h, -h, 0.0, 1.0, h, -h, -h, 1.0, 1.0, h, -h, h, 1.0, 0.0, -h, -h, h, 0.0, 0.0, ] vertices = np.array(front + back + right + left + top + bottom, dtype=np.float32) # 6 faces, each with 2 triangles (6 indices per face) indices = [] for i in range(6): base = i * 4 indices.extend([base, base+1, base+2, base, base+2, base+3]) indices = np.array(indices, dtype=np.uint32) vao = glGenVertexArrays(1) vbo = glGenBuffers(1) ebo = glGenBuffers(1) glBindVertexArray(vao) glBindBuffer(GL_ARRAY_BUFFER, vbo) glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo) glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW) # Position attribute (location 0) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(0)) glEnableVertexAttribArray(0) # UV attribute (location 1) glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(12)) glEnableVertexAttribArray(1) glBindVertexArray(0) return vao, len(indices) def create_debug_quad(): """Create a screen-space quad for debug text overlay.""" # Screen-space quad at top-left # NDC: x=-1 is left, y=1 is top w = 680.0 / WINDOW_WIDTH * 2.0 h = 60.0 / WINDOW_HEIGHT * 2.0 vertices = np.array([ -1.0, 1.0, 0.0, 0.0, -1.0 + w, 1.0, 1.0, 0.0, -1.0 + w, 1.0 - h, 1.0, 1.0, -1.0, 1.0 - h, 0.0, 1.0, ], dtype=np.float32) indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32) vao = glGenVertexArrays(1) vbo = glGenBuffers(1) ebo = glGenBuffers(1) glBindVertexArray(vao) glBindBuffer(GL_ARRAY_BUFFER, vbo) glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo) glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW) # Position attribute (location 0) - xy only glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(0)) glEnableVertexAttribArray(0) # UV attribute (location 1) glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(8)) glEnableVertexAttribArray(1) glBindVertexArray(0) return vao # ----------------------------------------------------------------------------- # Debug Text # ----------------------------------------------------------------------------- DEBUG_VERTEX = """ #version 330 core layout(location = 0) in vec2 aPos; layout(location = 1) in vec2 aUV; out vec2 vUV; void main() { vUV = aUV; gl_Position = vec4(aPos, 0.0, 1.0); } """ DEBUG_FRAGMENT = """ #version 330 core uniform sampler2D tex; in vec2 vUV; out vec4 fragColor; void main() { fragColor = texture(tex, vUV); } """ g_debug_program = None def init_debug_rendering(): """Initialize debug text rendering resources.""" global g_debug_program vs = compile_shader(DEBUG_VERTEX, GL_VERTEX_SHADER) fs = compile_shader(DEBUG_FRAGMENT, GL_FRAGMENT_SHADER) if vs is None or fs is None: print("ERROR: Failed to compile debug shaders") if vs: glDeleteShader(vs) if fs: glDeleteShader(fs) return g_debug_program = link_program(vs, fs) glDeleteShader(vs) glDeleteShader(fs) if g_debug_program is None: print("ERROR: Failed to link debug program") return g_state['debug_vao'] = create_debug_quad() g_state['debug_texture'] = glGenTextures(1) def update_debug_text(): """Render debug text to texture.""" if not g_state['debug_dirty']: return c0 = g_state['const0'] c1 = g_state['const1'] # Build status lines lines = [ f"Mode:{g_state['mode']:4s} Filter:{g_state['filter_mode']:9s} Block:{BLOCK_WIDTH}x{BLOCK_HEIGHT} Deblock: [{int(c0[0])}{int(c0[1])}{int(c0[2])}{int(c0[3])}][{int(c1[0])}{int(c1[1])}{int(c1[2])}{int(c1[3])}]", f"X:{g_state['x']:+5.1f} Y:{g_state['y']:+5.1f} Z:{g_state['z']:5.1f} Yaw:{g_state['yaw']:+6.1f} Pitch:{g_state['pitch']:+6.1f}", "Arrows:move, W/S:zoom, A/D:yaw, Q/E:pitch, C:cube, B/T/P:filter, 1=deblocking toggle, 2=edge vis, R:reload, Space:reset", ] img = Image.new('RGBA', (680, 60), (0, 0, 0, 180)) draw = ImageDraw.Draw(img) try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 14) except: font = ImageFont.load_default() y = 4 for line in lines: draw.text((6, y), line, fill=(255, 255, 255, 255), font=font) y += 18 data = np.array(img, dtype=np.uint8) glBindTexture(GL_TEXTURE_2D, g_state['debug_texture']) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) g_state['debug_dirty'] = False def draw_debug_text(): """Draw debug text overlay.""" if g_debug_program is None: return update_debug_text() glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glDisable(GL_DEPTH_TEST) glUseProgram(g_debug_program) glBindTexture(GL_TEXTURE_2D, g_state['debug_texture']) glBindVertexArray(g_state['debug_vao']) glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None) glEnable(GL_DEPTH_TEST) glDisable(GL_BLEND) # ----------------------------------------------------------------------------- # Math # ----------------------------------------------------------------------------- def perspective_matrix(fov_deg, aspect, near, far): """Create perspective projection matrix.""" fov_rad = np.radians(fov_deg) f = 1.0 / np.tan(fov_rad / 2.0) m = np.zeros((4, 4), dtype=np.float32) m[0, 0] = f / aspect m[1, 1] = f m[2, 2] = (far + near) / (near - far) m[2, 3] = (2 * far * near) / (near - far) m[3, 2] = -1.0 return m def translation_matrix(x, y, z): """Create translation matrix.""" m = np.eye(4, dtype=np.float32) m[0, 3] = x m[1, 3] = y m[2, 3] = z return m def rotation_matrix_y(deg): """Create rotation matrix around Y axis (yaw).""" rad = np.radians(deg) c, s = np.cos(rad), np.sin(rad) m = np.eye(4, dtype=np.float32) m[0, 0] = c m[0, 2] = s m[2, 0] = -s m[2, 2] = c return m def rotation_matrix_x(deg): """Create rotation matrix around X axis (pitch).""" rad = np.radians(deg) c, s = np.cos(rad), np.sin(rad) m = np.eye(4, dtype=np.float32) m[1, 1] = c m[1, 2] = -s m[2, 1] = s m[2, 2] = c return m # ----------------------------------------------------------------------------- # Input # ----------------------------------------------------------------------------- def framebuffer_size_callback(window, width, height): """Handle window resize.""" global WINDOW_WIDTH, WINDOW_HEIGHT WINDOW_WIDTH = width WINDOW_HEIGHT = height glViewport(0, 0, width, height) g_state['debug_dirty'] = True def key_callback(window, key, scancode, action, mods): if action == glfw.PRESS: if key == glfw.KEY_ESCAPE: glfw.set_window_should_close(window, True) elif key == glfw.KEY_R: reload_shader() elif key == glfw.KEY_B: set_filter_mode('BILINEAR') print("Filter: BILINEAR") elif key == glfw.KEY_P: set_filter_mode('POINT') print("Filter: POINT") elif key == glfw.KEY_T: set_filter_mode('TRILINEAR') print("Filter: TRILINEAR") # Toggle const0 components (keys 1-4) elif key == glfw.KEY_1: g_state['const0'][0] = 1.0 - g_state['const0'][0] print(f"const0: {g_state['const0']}") g_state['debug_dirty'] = True elif key == glfw.KEY_2: g_state['const0'][1] = 1.0 - g_state['const0'][1] print(f"const0: {g_state['const0']}") g_state['debug_dirty'] = True elif key == glfw.KEY_3: g_state['const0'][2] = 1.0 - g_state['const0'][2] print(f"const0: {g_state['const0']}") g_state['debug_dirty'] = True elif key == glfw.KEY_4: g_state['const0'][3] = 1.0 - g_state['const0'][3] print(f"const0: {g_state['const0']}") g_state['debug_dirty'] = True # Toggle const1 components (keys 5-8) elif key == glfw.KEY_5: g_state['const1'][0] = 1.0 - g_state['const1'][0] print(f"const1: {g_state['const1']}") g_state['debug_dirty'] = True elif key == glfw.KEY_6: g_state['const1'][1] = 1.0 - g_state['const1'][1] print(f"const1: {g_state['const1']}") g_state['debug_dirty'] = True elif key == glfw.KEY_7: g_state['const1'][2] = 1.0 - g_state['const1'][2] print(f"const1: {g_state['const1']}") g_state['debug_dirty'] = True elif key == glfw.KEY_8: g_state['const1'][3] = 1.0 - g_state['const1'][3] print(f"const1: {g_state['const1']}") g_state['debug_dirty'] = True elif key == glfw.KEY_C: g_state['mode'] = 'CUBE' if g_state['mode'] == 'QUAD' else 'QUAD' print(f"Mode: {g_state['mode']}") g_state['debug_dirty'] = True elif key == glfw.KEY_SPACE: g_state['x'] = INIT_X g_state['y'] = INIT_Y g_state['z'] = INIT_Z g_state['yaw'] = INIT_YAW g_state['pitch'] = INIT_PITCH g_state['const0'] = INIT_CONST0.copy() g_state['const1'] = INIT_CONST1.copy() g_state['debug_dirty'] = True print("Reset to initial state") def process_held_keys(window, dt): """Process continuously held keys.""" moved = False if glfw.get_key(window, glfw.KEY_W) == glfw.PRESS: g_state['z'] += Z_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_S) == glfw.PRESS: g_state['z'] -= Z_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_LEFT) == glfw.PRESS: g_state['x'] += XY_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_RIGHT) == glfw.PRESS: g_state['x'] -= XY_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_UP) == glfw.PRESS: g_state['y'] += XY_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_DOWN) == glfw.PRESS: g_state['y'] -= XY_SPEED * dt moved = True # Rotation (A/D for yaw, Q/E for pitch) if glfw.get_key(window, glfw.KEY_A) == glfw.PRESS: g_state['yaw'] += ROT_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_D) == glfw.PRESS: g_state['yaw'] -= ROT_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_Q) == glfw.PRESS: g_state['pitch'] += ROT_SPEED * dt moved = True if glfw.get_key(window, glfw.KEY_E) == glfw.PRESS: g_state['pitch'] -= ROT_SPEED * dt moved = True # Clamp Z g_state['z'] = max(Z_MAX, min(Z_MIN, g_state['z'])) if moved: g_state['debug_dirty'] = True # ----------------------------------------------------------------------------- # Main # ----------------------------------------------------------------------------- def main(): global BLOCK_WIDTH, BLOCK_HEIGHT if len(sys.argv) < 5: print(__doc__) print("ERROR: Need shader, block_w, block_h, and at least one mipmap PNG") print("Example: python testbed.py shader.glsl 8 8 mip0.png mip1.png") sys.exit(1) shader_path = sys.argv[1] try: BLOCK_WIDTH = int(sys.argv[2]) BLOCK_HEIGHT = int(sys.argv[3]) except ValueError: print(f"ERROR: block_w and block_h must be integers, got '{sys.argv[2]}' '{sys.argv[3]}'") sys.exit(1) if BLOCK_WIDTH < 1 or BLOCK_HEIGHT < 1: print(f"ERROR: block size must be positive, got {BLOCK_WIDTH}x{BLOCK_HEIGHT}") sys.exit(1) mip_paths = sys.argv[4:] print(f"Block size: {BLOCK_WIDTH}x{BLOCK_HEIGHT}") g_state['shader_path'] = shader_path # Init GLFW if not glfw.init(): print("ERROR: Failed to initialize GLFW") sys.exit(1) glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3) glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) glfw.window_hint(glfw.RESIZABLE, glfw.TRUE) glfw.window_hint(glfw.FOCUSED, glfw.TRUE) glfw.window_hint(glfw.FOCUS_ON_SHOW, glfw.TRUE) window = glfw.create_window(WINDOW_WIDTH, WINDOW_HEIGHT, "Deblock Shader Testbed", None, None) if not window: glfw.terminate() print("ERROR: Failed to create window") sys.exit(1) glfw.make_context_current(window) glfw.set_key_callback(window, key_callback) glfw.set_framebuffer_size_callback(window, framebuffer_size_callback) glfw.swap_interval(1) # VSync glfw.focus_window(window) print(f"OpenGL: {glGetString(GL_VERSION).decode()}") # Load shader (exit on failure at startup) g_state['program'] = load_shader(shader_path) if g_state['program'] is None: glfw.terminate() sys.exit(1) # Load texture g_state['texture'], g_state['tex_size'] = load_mipmap_texture(mip_paths) set_filter_mode('BILINEAR') # Create quad aspect = g_state['tex_size'][0] / g_state['tex_size'][1] g_state['quad_vao'] = create_quad(aspect) # Create cube g_state['cube_vao'], g_state['cube_index_count'] = create_cube(1.0) # Init debug rendering init_debug_rendering() glEnable(GL_DEPTH_TEST) glClearColor(0.2, 0.2, 0.2, 1.0) g_state['last_time'] = glfw.get_time() # Main loop while not glfw.window_should_close(window): # Delta time now = glfw.get_time() dt = now - g_state['last_time'] g_state['last_time'] = now # Input glfw.poll_events() process_held_keys(window, dt) # Projection matrix (recalculate for resize) proj = perspective_matrix(FOV_DEGREES, WINDOW_WIDTH / WINDOW_HEIGHT, 0.001, 100.0) # Clear glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # Draw quad or cube glUseProgram(g_state['program']) # MVP (include rotation for cube mode) trans = translation_matrix(g_state['x'], g_state['y'], g_state['z']) rot_y = rotation_matrix_y(g_state['yaw']) rot_x = rotation_matrix_x(g_state['pitch']) model = trans @ rot_y @ rot_x mvp = proj @ model loc = glGetUniformLocation(g_state['program'], "mvp") if loc >= 0: glUniformMatrix4fv(loc, 1, GL_TRUE, mvp) loc = glGetUniformLocation(g_state['program'], "tex") if loc >= 0: glUniform1i(loc, 0) loc = glGetUniformLocation(g_state['program'], "texSize") if loc >= 0: glUniform4f(loc, float(g_state['tex_size'][0]), float(g_state['tex_size'][1]), BLOCK_WIDTH, BLOCK_HEIGHT); loc = glGetUniformLocation(g_state['program'], "const0") if loc >= 0: c = g_state['const0'] glUniform4f(loc, c[0], c[1], c[2], c[3]) loc = glGetUniformLocation(g_state['program'], "const1") if loc >= 0: c = g_state['const1'] glUniform4f(loc, c[0], c[1], c[2], c[3]) glActiveTexture(GL_TEXTURE0) glBindTexture(GL_TEXTURE_2D, g_state['texture']) if g_state['mode'] == 'CUBE': glBindVertexArray(g_state['cube_vao']) glDrawElements(GL_TRIANGLES, g_state['cube_index_count'], GL_UNSIGNED_INT, None) else: glBindVertexArray(g_state['quad_vao']) glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None) # Draw debug overlay draw_debug_text() glfw.swap_buffers(window) glfw.terminate() print("Done.") if __name__ == "__main__": main()