removing old sample, replaced with python/shader_deblocking

This commit is contained in:
Richard Geldreich
2026-07-01 13:13:54 -04:00
parent 1f728895a8
commit d2c91746fa
23 changed files with 0 additions and 1094 deletions

View File

@@ -1,135 +0,0 @@
# Python+GLSL Shader Deblocking Sample
*Block boundaries are predictable.*
This sample demonstrates how to use a simple pixel shader to greatly reduce
ASTC texture block artifacts, which can be quite noticeable when the block size goes
beyond roughly 6x6. The basic idea: instead of always sampling the texture using
a single tap, you instead sample the texture either one time or X times with a simple low pass filter,
depending on whether or not the sample location is near a block edge. The multiple filter taps around
the center sample blur across block boundaries of ASTC compressed textures. There are two independent filters, for horizontal and vertical block boundaries.
The example shader is compatible with mipmapping, bilinear filtering, trilinear filtering etc. and is temporally stable. The
shader smoothly lerps between no filtering and edge filtering, and is mipmap-aware by using the pixel shader derivative instructions. Crucially, the block lattice is evaluated in the *effective mip space*, not in base texture space, which is why it's mipmap-aware. The Python sample shows either a textured quad or a cube, with various controls to move the object, rotate the cube, toggle the deblocking shader on/off, trilinear off/on, etc.
It was written to be as simple as possible. The shader's filter coefficients were picked for more blurring vs. our CPU deblocker to demonstrate the effect, but they are easily tuned. The shader could easily be more optimized (most inner block texels don't need any filtering, but we don't exploit this in the shader with a conditional yet). The idea is compatible with other texture formats with noticeable block artifacts, such as BC1 or ETC1, but 4x4 blocks are so tiny it may be a wash.
It's also possible to add adaptivity to this shader, so it doesn't blindly blur across sharp edges - like we do while
CPU deblocking before transcoding to BC7 or other LDR texture formats. It's also possible
to add deblocking filter awareness to our ASTC/XUASTC/etc. encoders.
The shader computes the mipmap LOD index assuming an entire mip chain is present (i.e. down to 1x1 texels). If the mipmap chain is incomplete, the developer should clamp the computed mipmap LOD index in the shader. We'll show this in the next update of the sample.
Note: We're amazed the GPU hardware vendors haven't implemented this feature directly in silicon yet. It's extremely useful, even necessary at the largest ASTC block sizes.
This is a form of "GPU texture compression-aware shading" or "GPU format-informed reconstruction".
## Running the Sample
You'll need these Python dependencies to run it:
```
pip install numpy Pillow glfw PyOpenGL
```
You may also want "PyOpenGL_accelerate", and under Linux you may need to also install the system package "libglfw3". We developed this sample under Windows 11.
Run:
```
python testbed.py shader.glsl 12 12 flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_0_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_1_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_2_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_3_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_4_face_0_layer_0000.png
```
Depending on your setup you may need to use `python3`, or `py -3.12` etc.
Keys: `1` toggles deblocking shader on/off (it defaults to disabled), `2` toggles edges vis (only shows when deblocking is enabled), arrow keys move the object, `W`/`S` keys: forward/backward, `A`/`D`/`Q`/`E`: yaw/pitch, `C`: toggle cube vs. quad. (See the source code remarks for more.)
The shader can be easily simplified to sample the texture less by using less taps. The current shader uses a total of 9 taps, but 5 are possible.
Many variations and optimizations of this basic idea are possible. *Now shader engineers can directly impact memory consumption.* The better your deblocking shaders are tuned or your specific content, the bigger the ASTC block size you can ship. ASTC texture deblocking pixel shader engineering is now a memory optimization skill.
A simpler, [faster 5 tap filter variant is here](https://pastebin.com/jsF6nUZg). (This shader isn't quite compatible with the currently checked in Python sample, as it uses a new `maxLod` uniform to limit the max fetched mipmap level.) In practice, this simple shader usually looks just as good on most textures/images. ` textureQueryLod()` and `textureSize()` could also be used (with some API's) to simplify the shader further. We're continuing to refine and tune this shader.
---
## Performance
The deblocking shader itself can be put inside a dynamic `if` conditional, so the extra ALU/texture ops only kick in near block edges (which are the minority of samples at ASTC 12x12). The extra sample taps are spatially always near the center sample, so they'll hit the texture cache most of the time. In texture bandwidth bound rendering scenarios (quite common on mobile platforms), the extra ALU ops for deblocking likely come for "free".
*Another perspective: The alternative to not deblocking is ~2x-8x more GPU memory bandwidth (and increased download size) to use smaller ASTC block sizes which have less noticeable block artifacts.*
---
## Screenshots - ASTC 12x12 Block Size
**Disabled:**
![Screenshot 1: Off](screenshots/1_off.png)
**Enabled:**
![Screenshot 1: On](screenshots/1_on.png)
---
**Disabled:**
![Screenshot 2: Off](screenshots/2_off.png)
**Enabled:**
![Screenshot 2: On](screenshots/2_on.png)
---
**Disabled:**
![Screenshot 3: Off](screenshots/3_off.png)
**Enabled:**
![Screenshot 3: On](screenshots/3_on.png)
---
**Block Edge Computation Visualization:**
![Edge Vis](screenshots/edge_vis.png)
![Edge Vis 2](screenshots/edge_vis2.png)
These screenshots show how the pixel shader computes texture block boundaries in effective mipmap space. To see this visualization, press '1' to enable deblocking, then '2' to enable block edge visualization. Only white areas in this visualization are modified by this shader, leaving the inner block texels unmodified.
**Obviously, it's crucial that the block size passed into the shader via constants exactly matches the ASTC texture's block size, or the filtering applied won't align with the actual ASTC block artifacts.**
---
## Usage and Controls
The sample either renders a single textured quad or a cube. Press 'C' to toggle between the quad and the cube. The '1' key toggles shader deblocking (off by default). The '2' key enables edge visualization, which only works when deblocking is enabled.
Other keys can be used to move around the quad, rotate the cube etc.:
```
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
```
---
## Credits
The included sunflower image is in the CC0/Public Domain, and was downloaded from here:
https://www.publicdomainpictures.net/en/view-image.php?image=756601&picture=large-yellow-sunflower
"License: CC0 Public Domain - Lynn Greyling has released this “Large Yellow Sunflower” image under Public Domain license."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 B

View File

@@ -1 +0,0 @@
py -3.12 testbed.py shader.glsl 12 12 flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_0_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_1_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_2_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_3_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_4_face_0_layer_0000.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

View File

@@ -1,97 +0,0 @@
#vertex
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aUV;
uniform mat4 mvp;
out vec2 vUV;
void main() {
vUV = aUV;
gl_Position = mvp * vec4(aPos, 1.0);
}
#fragment
#version 330 core
uniform sampler2D tex;
uniform vec4 texSize; // Base mip dimensions (mip 0)
uniform vec4 const0; // User constant 0 (keys 1-4 toggle x,y,z,w)
uniform vec4 const1; // User constant 1 (keys 5-8 toggle x,y,z,w)
in vec2 vUV;
out vec4 fragColor;
void main()
{
vec2 blockSize = vec2(texSize.z, texSize.w);
vec2 du = dFdx(vUV);
vec2 dv = dFdy(vUV);
float rho = max(length(du * texSize.xy), length(dv * texSize.xy));
float lod = max(0.0, log2(max(rho, 1e-8))); // lod index
float mipScale = exp2(floor(lod + .5)); // 2^lod mipmap scale, snaps to dominant mipmap
vec2 texDim = vec2(texSize.x, texSize.y);
vec2 texelStep = 1.0 / texDim;
vec2 texelPos = (vUV * texDim) / mipScale;
vec2 blockPos = mod(texelPos, blockSize);
vec3 color;
color = texture(tex, vUV).rgb;
if (const0.x > 0.5)
{
float falloff = 2.0;
float leftProx = 1.0 - clamp(blockPos.x / falloff, 0.0, 1.0);
float rightProx = 1.0 - clamp((blockSize.x - 1.0 - blockPos.x) / falloff, 0.0, 1.0);
float topProx = 1.0 - clamp(blockPos.y / falloff, 0.0, 1.0);
float bottomProx = 1.0 - clamp((blockSize.y - 1.0 - blockPos.y) / falloff, 0.0, 1.0);
float horizWeight = max(leftProx, rightProx);
float vertWeight = max(topProx, bottomProx);
float edgeWeight = max(horizWeight, vertWeight); // overall proximity
vec3 c0 = color; //texture2D(tex, vUV).rgb;
vec3 l2 = texture2D(tex, vUV - vec2(2 * texelStep.x * mipScale, 0.0)).rgb;
vec3 l1 = texture2D(tex, vUV - vec2(texelStep.x * mipScale, 0.0)).rgb;
vec3 r1 = texture2D(tex, vUV + vec2(texelStep.x * mipScale, 0.0)).rgb;
vec3 r2 = texture2D(tex, vUV + vec2(2 * texelStep.x * mipScale, 0.0)).rgb;
vec3 u2 = texture2D(tex, vUV - vec2(0.0, 2 * texelStep.y * mipScale)).rgb;
vec3 u1 = texture2D(tex, vUV - vec2(0.0, texelStep.y * mipScale)).rgb;
vec3 d1 = texture2D(tex, vUV + vec2(0.0, texelStep.y * mipScale)).rgb;
vec3 d2 = texture2D(tex, vUV + vec2(0.0, 2 * texelStep.y * mipScale)).rgb;
//vec3 filteredH = (l2 + 2 * l1 + 3 * c0 + 2 * r1 + r2) / 9.0;
//vec3 filteredV = (u2 + 2 * u1 + 3 * c0 + 2 * d1 + d2) / 9.0;
vec3 filteredH = (l2 + 2 * l1 + 2 * c0 + 2 * r1 + r2) / 8.0;
vec3 filteredV = (u2 + 2 * u1 + 2 * c0 + 2 * d1 + d2) / 8.0;
float smoothH = 1.0;
float smoothV = 1.0;
if (edgeWeight > 0.0)
{
vec3 horizColor = mix(c0, filteredH, smoothH * horizWeight);
vec3 vertColor = mix(c0, filteredV, smoothV * vertWeight);
float totalW = horizWeight + vertWeight;
if (totalW > 0.0)
color = (horizColor * horizWeight + vertColor * vertWeight) / totalW;
}
// block edge vis
if (const0.y > 0.5)
{
color = vec3(edgeWeight, edgeWeight, edgeWeight);
}
}
fragColor = vec4(color, 1.0);
}

View File

@@ -1,861 +0,0 @@
#!/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()