mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Client-side GL Transitions v1 across all three players with never-blank degradation: shader lib + generated manifest + real-WebGL CI, transition-as-widget normalization, persistent WebGL/GLES2 compositors (web/Tizen/native Android), image↔video wipes, SSRF-hardened media proxy, decode-gated image preload, dashboard picker. Fixes the playlist change-fingerprint that dropped transition edits. Alpha + on-device soak validated.
114 lines
5.5 KiB
JavaScript
114 lines
5.5 KiB
JavaScript
'use strict';
|
|
// Shader compile test — compiles + links all 14 transition shaders in a REAL WebGL context
|
|
// (Chrome via puppeteer-core, ANGLE/SwiftShader) so we catch GLSL ES errors the way a panel would,
|
|
// not a lenient CPU validator. Also enforces manifest<->file consistency and that the manifest's
|
|
// params never drift from what params.js parses out of the shader source (the single source of truth).
|
|
//
|
|
// Run: npm run test:shaders (from repo root)
|
|
// CI : .github/workflows/shaders.yml
|
|
//
|
|
// puppeteer-core is Apache-2.0 and uses the system Chrome — no bundled binary is downloaded.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const DIR = __dirname;
|
|
const { PREAMBLE, EPILOGUE, VERTEX } = require('./params.js');
|
|
const genManifest = require('./generate-manifest.js');
|
|
|
|
// Resolve puppeteer-core: prefer a real install, else the repo-relative copy in video/ (portable).
|
|
function loadPuppeteer() {
|
|
const tries = ['puppeteer-core', path.resolve(DIR, '../../video/node_modules/puppeteer-core')];
|
|
for (const t of tries) { try { return require(t); } catch (e) { /* next */ } }
|
|
console.error('FATAL: puppeteer-core not found. `npm install` at repo root, or set it up in video/.');
|
|
process.exit(2);
|
|
}
|
|
function chromePath() {
|
|
const env = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_BIN;
|
|
const cands = [env, '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/snap/bin/chromium'].filter(Boolean);
|
|
for (const c of cands) { try { if (fs.existsSync(c)) return c; } catch (e) {} }
|
|
return null; // let puppeteer try its own resolution
|
|
}
|
|
|
|
function loadShaders() {
|
|
const glslFiles = fs.readdirSync(DIR).filter((f) => f.endsWith('.glsl')).sort();
|
|
const problems = [];
|
|
|
|
// The manifest is GENERATED from the shaders (generate-manifest.js). Assert the committed bytes
|
|
// equal a fresh regeneration — a mismatch means someone edited a shader without running
|
|
// `npm run build:manifest`, so the manifest (and dashboard) would be stale. Staleness = test failure.
|
|
const committed = fs.readFileSync(path.join(DIR, 'manifest.json'), 'utf8');
|
|
const regenerated = genManifest.serialize(genManifest.build());
|
|
if (committed !== regenerated) {
|
|
problems.push('manifest.json is stale — run `npm run build:manifest` (shaders changed since it was generated)');
|
|
}
|
|
|
|
// Compile every shader on disk (the manifest is derived, so disk is the authoritative list).
|
|
const shaders = glslFiles.map((file) => ({ id: file.replace(/\.glsl$/, ''), file, src: fs.readFileSync(path.join(DIR, file), 'utf8') }));
|
|
return { shaders, problems, glslCount: glslFiles.length };
|
|
}
|
|
|
|
// Runs in the browser: compile+link each shader, return [{id, ok, error}]
|
|
function browserCompile(jobs) {
|
|
const canvas = document.createElement('canvas');
|
|
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
if (!gl) return [{ id: '(context)', ok: false, error: 'no WebGL context available' }];
|
|
const compile = (type, src) => {
|
|
const s = gl.createShader(type);
|
|
gl.shaderSource(s, src); gl.compileShader(s);
|
|
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(s); gl.deleteShader(s); return { err: log }; }
|
|
return { shader: s };
|
|
};
|
|
const out = [];
|
|
const vs = compile(gl.VERTEX_SHADER, jobs.VERTEX);
|
|
for (const j of jobs.shaders) {
|
|
if (vs.err) { out.push({ id: j.id, ok: false, error: 'vertex: ' + vs.err }); continue; }
|
|
const fs_ = compile(gl.FRAGMENT_SHADER, jobs.PREAMBLE + '\n' + j.src + '\n' + jobs.EPILOGUE);
|
|
if (fs_.err) { out.push({ id: j.id, ok: false, error: 'fragment: ' + fs_.err }); continue; }
|
|
const prog = gl.createProgram();
|
|
gl.attachShader(prog, vs.shader); gl.attachShader(prog, fs_.shader);
|
|
gl.bindAttribLocation(prog, 0, 'aPos'); gl.linkProgram(prog);
|
|
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { out.push({ id: j.id, ok: false, error: 'link: ' + gl.getProgramInfoLog(prog) }); }
|
|
else out.push({ id: j.id, ok: true });
|
|
gl.deleteShader(fs_.shader); gl.deleteProgram(prog);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
(async () => {
|
|
const { shaders, problems, glslCount } = loadShaders();
|
|
console.log(`Found ${glslCount} .glsl files, ${shaders.length} in manifest.\n`);
|
|
|
|
const puppeteer = loadPuppeteer();
|
|
const exe = chromePath();
|
|
const browser = await puppeteer.launch({
|
|
executablePath: exe || undefined,
|
|
headless: 'new',
|
|
args: ['--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl'],
|
|
});
|
|
let results;
|
|
try {
|
|
const page = await browser.newPage();
|
|
page.on('pageerror', (e) => console.error('pageerror:', e.message));
|
|
await page.goto('about:blank');
|
|
results = await page.evaluate(browserCompile, { shaders, VERTEX, PREAMBLE, EPILOGUE });
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
let failed = 0;
|
|
for (const r of results) {
|
|
if (r.ok) { console.log(` PASS ${r.id}`); }
|
|
else { failed++; console.log(` FAIL ${r.id}\n ${String(r.error).trim().replace(/\n/g, '\n ')}`); }
|
|
}
|
|
if (problems.length) {
|
|
console.log('\nManifest/consistency problems:');
|
|
for (const p of problems) console.log(' ✗ ' + p);
|
|
} else {
|
|
console.log('\nmanifest.json is up to date with the shader sources (byte-identical to a fresh build).');
|
|
}
|
|
|
|
const compiledOk = results.filter((r) => r.ok).length;
|
|
console.log(`\n${compiledOk}/${results.length} shaders compiled+linked; ${problems.length} consistency problems.`);
|
|
process.exit(failed === 0 && problems.length === 0 ? 0 : 1);
|
|
})().catch((e) => { console.error('runner error:', e); process.exit(2); });
|