Fix scrambled GPU chrome upload and pin veil gradient direction

glTexSubImage2D has no row-stride concept on GLES2, so uploading the
chrome sub-rect directly from the full-surface pixmap shifted every row
after the first by (w - rw) pixels — the clock/pill rendered as garbage.
Pack the dirty rows into a tightly-strided buffer before upload instead
(GL_UNPACK_ROW_LENGTH is ES 3.0-only).

Add a regression test for the pack, plus one pinning the background
shader's veil gradient to the software dim_rows (gl_FragCoord.y is 0 at
the bottom of the framebuffer, which is easy to get backwards).
This commit is contained in:
Breadway 2026-08-21 15:35:10 +08:00
parent 3d2e35e003
commit 55cf57a8ba

View file

@ -68,7 +68,11 @@ uniform float u_veil_alpha;
uniform float u_screen_h; uniform float u_screen_h;
void main() { void main() {
vec4 c = texture2D(u_tex, v_uv) * u_color; vec4 c = texture2D(u_tex, v_uv) * u_color;
float row = 1.0 - gl_FragCoord.y / u_screen_h; // 1 at top // Same gradient as the software dim_rows: top = u_dim_top, bottom =
// u_dim_bottom, interpolated by screen-space y. gl_FragCoord has y=0 at
// the BOTTOM of the framebuffer, so `row = 1.0 - y/h` is 1 at the bottom
// and 0 at the top; mixing with `row` yields top = u_dim_top.
float row = 1.0 - gl_FragCoord.y / u_screen_h; // 1 at bottom, 0 at top
float dim = mix(u_dim_top, u_dim_bottom, row) * u_veil_alpha; float dim = mix(u_dim_top, u_dim_bottom, row) * u_veil_alpha;
gl_FragColor = vec4(c.rgb * (1.0 - dim), 1.0); gl_FragColor = vec4(c.rgb * (1.0 - dim), 1.0);
}"; }";
@ -533,24 +537,30 @@ impl GpuRenderer {
self.chrome_tex_size = (w, h); self.chrome_tex_size = (w, h);
} }
let rw = (x1 - x0) as usize;
let rh = (y1 - y0) as usize;
let data = pixmap.data(); let data = pixmap.data();
let stride = w as usize * 4;
let offset = y0 as usize * stride + x0 as usize * 4;
let rw = (x1 - x0) as i32;
let rh = (y1 - y0) as i32;
let gl = &self.gl; let gl = &self.gl;
unsafe { unsafe {
gl.bind_texture(glow::TEXTURE_2D, Some(self.chrome_tex)); gl.bind_texture(glow::TEXTURE_2D, Some(self.chrome_tex));
// glTexSubImage2D reads `rw` pixels contiguously per row with no
// knowledge of the source's row stride. We're on GLES2 (where
// GL_UNPACK_ROW_LENGTH doesn't exist), so pack the sub-rect rows
// into a tightly-strided buffer first — otherwise every row after
// the first is shifted by (w - rw) pixels and the chrome texture
// comes out horizontally scrambled (the clock/pill/status rect is
// narrower than the surface, so this always triggered).
let packed = pack_rows(data, w as usize, x0 as usize, y0 as usize, rw, rh);
gl.tex_sub_image_2d( gl.tex_sub_image_2d(
glow::TEXTURE_2D, glow::TEXTURE_2D,
0, 0,
x0, x0 as i32,
y0, y0 as i32,
rw, rw as i32,
rh, rh as i32,
glow::RGBA, glow::RGBA,
glow::UNSIGNED_BYTE, glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(Some(&data[offset..])), glow::PixelUnpackData::Slice(Some(&packed)),
); );
gl.use_program(Some(self.chrome_program)); gl.use_program(Some(self.chrome_program));
gl.bind_vertex_array(Some(self.quad_vao)); gl.bind_vertex_array(Some(self.quad_vao));
@ -607,6 +617,18 @@ fn pan_region(wp: (u32, u32), target: (u32, u32), ken_burns: bool, t_secs: f32)
(-tx, -ty, scaled_w, scaled_h) (-tx, -ty, scaled_w, scaled_h)
} }
/// Packs the `(x0, y0, rw, rh)` sub-rect of a `w`-wide RGBA row-major buffer
/// into a tightly-strided `rw`-per-row buffer for `glTexSubImage2D`, which
/// reads rows contiguously and has no stride concept on GLES2.
fn pack_rows(data: &[u8], w: usize, x0: usize, y0: usize, rw: usize, rh: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(rw * rh * 4);
for row in 0..rh {
let src = (y0 + row) * w * 4 + x0 * 4;
out.extend_from_slice(&data[src..src + rw * 4]);
}
out
}
fn compile_program(gl: &glow::Context, vs_src: &str, fs_src: &str) -> Option<glow::Program> { fn compile_program(gl: &glow::Context, vs_src: &str, fs_src: &str) -> Option<glow::Program> {
unsafe { unsafe {
let program = gl.create_program().ok()?; let program = gl.create_program().ok()?;
@ -782,6 +804,42 @@ mod tests {
} }
} }
/// The GPU background shader's veil math, replicated in Rust: on GLES,
/// `gl_FragCoord.y` is 0 at the BOTTOM of the framebuffer, so
/// `row = 1.0 - y/h` is 1 at the bottom and 0 at the top, and
/// `dim = mix(u_dim_top, u_dim_bottom, row)` dims the TOP by u_dim_top.
/// This must match the software `dim_rows` (which indexes y=0 at the
/// top) exactly, or the GPU veil renders upside-down.
fn shader_dim_at(frag_y: f32, h: f32) -> f32 {
let row = 1.0 - frag_y / h; // 1 at bottom (frag_y=0), 0 at top
crate::render::DIM_ALPHA_TOP + (crate::render::DIM_ALPHA_BOTTOM - crate::render::DIM_ALPHA_TOP) * row
}
#[test]
fn veil_gradient_matches_software_dim_rows() {
use crate::render::DIM_ALPHA_BOTTOM;
use crate::render::DIM_ALPHA_TOP;
let h = 720.0;
// Software dim_rows: alpha = lerp(TOP, BOTTOM, y/h) with y=0 at top,
// so the top of the screen gets the DEEPER dim (DIM_ALPHA_TOP).
assert!(DIM_ALPHA_TOP > DIM_ALPHA_BOTTOM);
// Shader at the very top (gl_FragCoord.y = h): row = 0 -> dim = TOP.
assert!((shader_dim_at(h, h) - DIM_ALPHA_TOP).abs() < 1e-6);
// Shader at the very bottom (gl_FragCoord.y = 0): row = 1 -> dim = BOTTOM.
assert!((shader_dim_at(0.0, h) - DIM_ALPHA_BOTTOM).abs() < 1e-6);
// Match the software formula at many interior rows.
for y in 0..=720 {
let yf = y as f32;
let software = DIM_ALPHA_TOP + (DIM_ALPHA_BOTTOM - DIM_ALPHA_TOP) * (yf / h);
// Same physical row: pixmap row y (top-anchored) == frag y = h - y.
let shader = shader_dim_at(h - yf, h);
assert!(
(shader - software).abs() < 1e-5,
"veil mismatch at row {y}: shader {shader} vs software {software}"
);
}
}
#[test] #[test]
fn egl_attribs_are_none_terminated_pairs() { fn egl_attribs_are_none_terminated_pairs() {
assert_eq!(EGL_ATTRIBS.len() % 2, 1, "attribs must be key/value pairs + NONE"); assert_eq!(EGL_ATTRIBS.len() % 2, 1, "attribs must be key/value pairs + NONE");
@ -793,4 +851,29 @@ mod tests {
let v: [f32; 12] = [0.0; 12]; let v: [f32; 12] = [0.0; 12];
assert_eq!(f32s_as_bytes(&v).len(), 12 * 4); assert_eq!(f32s_as_bytes(&v).len(), 12 * 4);
} }
#[test]
fn pack_rows_keeps_row_ordering_and_stride() {
// A 4x2 RGBA buffer where pixel (x, y) has value (x, y, 0, 255).
let mut data = vec![0u8; 4 * 2 * 4];
for y in 0..2 {
for x in 0..4 {
let i = (y * 4 + x) * 4;
data[i] = x as u8;
data[i + 1] = y as u8;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
// Take the 2x1 rect at (1, 0).
let packed = pack_rows(&data, 4, 1, 0, 2, 1);
assert_eq!(packed.len(), 2 * 1 * 4);
assert_eq!(packed[0], 1);
assert_eq!(packed[4], 2);
// Two rows: the second row must NOT be shifted by (w - rw) — the bug.
let packed2 = pack_rows(&data, 4, 1, 0, 2, 2);
assert_eq!(packed2[8], 1, "row 2 col 0 must be x=1, not shifted");
assert_eq!(packed2[12], 2, "row 2 col 1 must be x=2, not shifted");
assert_eq!(packed2[9], 1, "row 2 must carry y=1");
}
} }