Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declared the `style` trait and honoured it with `col += sigGrain(uv)` and nothing else. A declared trait is a contract — the disqualification rule in Personality.js is the only thing keeping off-design scenes out of a track — so honouring it with dirt meant these four could not take the `texture: 0` opt-out the grain work introduced. Phase 9 measured their style response at exactly 0. They sat at texture: 0.35 as a stopgap, which kept speckle on the library's cleanest scenes purely to keep a gate green. Each already had the knob; it just was not wired to the track: - Classic Wave contrasts its wave through a bare smoothstep(0.2, 0.8). The transition width now comes from u_sigSoft and u_sigLine, centred on 0.5 so changing the hand does not change the exposure. Crest concentration is driven separately by u_sigLine, because a track is free to sample the scene's own u_softness at zero and the art direction must still show. - Silk Ribbon's strand width is u_sigLine and its falloff exponent u_sigSoft: a sharp track gets a filament with a defined edge, a soft one a haze. - Kaleido Tunnel drew its grid against a bare 0.42 — a line weight with no name. It is now a weight and a feather, both from the track. - Slow Orb's body edge multiplies the scene's softness by the video's, and gains a sigEdge rim so a sharp-handed track gets a defined limb. All four are now texture: 0. Style response measured against a maximally soft-handed versus maximally sharp-handed personality: Classic Wave 111, Silk Ribbon 216, Kaleido Tunnel 206, Slow Orb 102, out of 255 — previously 0. 104/104 checks pass including the slow set. See SIDE-QUESTS.md §1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
4.8 KiB
HTML
132 lines
4.8 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head><title>encoder probe</title></head>
|
||
<body>
|
||
<pre id="out">running…</pre>
|
||
<canvas id="c" width="1920" height="1080" style="display:none"></canvas>
|
||
<script type="module">
|
||
const out = document.getElementById('out');
|
||
const log = (m) => { out.textContent += m + '\n'; console.log(m); };
|
||
|
||
// A cheap animated source. The point is to test the ENCODER, so the frames must
|
||
// be genuinely different (a static source would let the encoder coast) but must
|
||
// cost nothing to produce — no shaders, no Show, no GPU readback.
|
||
const canvas = document.getElementById('c');
|
||
const ctx = canvas.getContext('2d');
|
||
function drawFrame(i) {
|
||
ctx.fillStyle = `hsl(${(i * 3) % 360} 60% 12%)`;
|
||
ctx.fillRect(0, 0, 1920, 1080);
|
||
ctx.fillStyle = `hsl(${(i * 7) % 360} 80% 60%)`;
|
||
for (let k = 0; k < 40; k++) {
|
||
const x = ((i * 11 + k * 137) % 1920);
|
||
const y = ((i * 5 + k * 271) % 1080);
|
||
ctx.fillRect(x, y, 90, 90);
|
||
}
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = '120px monospace';
|
||
ctx.fillText(String(i), 60, 200);
|
||
}
|
||
|
||
const FPS = 60;
|
||
const FRAMES = 600;
|
||
const WIDTH = 1920, HEIGHT = 1080, BITRATE = 16_000_000;
|
||
const MAX_QUEUE = 8;
|
||
|
||
function waitForQueue(encoder) {
|
||
if (encoder.encodeQueueSize < MAX_QUEUE) return Promise.resolve();
|
||
return new Promise((resolve) => {
|
||
const onDequeue = () => {
|
||
if (encoder.encodeQueueSize < MAX_QUEUE) {
|
||
encoder.removeEventListener('dequeue', onDequeue);
|
||
resolve();
|
||
}
|
||
};
|
||
encoder.addEventListener('dequeue', onDequeue);
|
||
});
|
||
}
|
||
|
||
async function run(label, extra) {
|
||
const config = {
|
||
codec: 'avc1.640034',
|
||
width: WIDTH, height: HEIGHT, bitrate: BITRATE, framerate: FPS,
|
||
avc: { format: 'avc' },
|
||
...extra,
|
||
};
|
||
let support;
|
||
try {
|
||
support = await VideoEncoder.isConfigSupported(config);
|
||
} catch (e) {
|
||
log(`${label.padEnd(34)} isConfigSupported threw: ${e.message}`);
|
||
return;
|
||
}
|
||
if (!support.supported) {
|
||
log(`${label.padEnd(34)} UNSUPPORTED`);
|
||
return;
|
||
}
|
||
|
||
let outputs = 0;
|
||
let keyframes = 0;
|
||
const stamps = [];
|
||
let error = null;
|
||
const encoder = new VideoEncoder({
|
||
output: (chunk) => {
|
||
outputs++;
|
||
if (chunk.type === 'key') keyframes++;
|
||
stamps.push(chunk.timestamp);
|
||
},
|
||
error: (e) => { error = e; },
|
||
});
|
||
encoder.configure(config);
|
||
|
||
const t0 = performance.now();
|
||
let encoded = 0;
|
||
for (let i = 0; i < FRAMES; i++) {
|
||
if (error) break;
|
||
drawFrame(i);
|
||
await waitForQueue(encoder);
|
||
const frame = new VideoFrame(canvas, {
|
||
timestamp: Math.round((i * 1e6) / FPS),
|
||
duration: Math.round(1e6 / FPS),
|
||
});
|
||
encoder.encode(frame, { keyFrame: i % (FPS * 2) === 0 });
|
||
frame.close();
|
||
encoded++;
|
||
if (i % 10 === 0) await new Promise((r) => setTimeout(r, 0));
|
||
}
|
||
try { await encoder.flush(); } catch (e) { error = error || e; }
|
||
const ms = performance.now() - t0;
|
||
try { encoder.close(); } catch { /* already closed */ }
|
||
|
||
// Do NOT sort: the order chunks ARRIVE in is the whole point. WebCodecs
|
||
// delivers in decode order, so a timestamp that goes backwards means the
|
||
// encoder emitted B-frames — and a muxer handed presentation timestamps as
|
||
// if they were decode timestamps will reject exactly those chunks.
|
||
const period = 1e6 / FPS;
|
||
const backwards = stamps.filter((t, i) => i > 0 && t < stamps[i - 1]).length;
|
||
const sorted = stamps.slice().sort((a, b) => a - b);
|
||
const gaps = sorted.slice(1).map((t, i) => Math.round((t - sorted[i]) / period));
|
||
const maxGap = gaps.length ? Math.max(...gaps) : 0;
|
||
|
||
const verdict = error ? `ERROR ${error.message}`
|
||
: backwards ? `REORDERED — ${backwards} chunks arrived out of order (B-frames)`
|
||
: outputs === encoded ? 'OK'
|
||
: `DROPPED ${encoded - outputs} (${((1 - outputs / encoded) * 100).toFixed(0)}%)`;
|
||
log(`${label.padEnd(34)} in ${encoded} → out ${outputs} keys ${keyframes} maxGap ${maxGap} reordered ${backwards} ${(ms / 1000).toFixed(1)}s ${verdict}`);
|
||
}
|
||
|
||
log(`source ${WIDTH}×${HEIGHT} @ ${FPS} · ${FRAMES} frames · bitrate ${BITRATE / 1e6} Mbps\n`);
|
||
|
||
await run('default (no hints)', {});
|
||
await run('prefer-hardware', { hardwareAcceleration: 'prefer-hardware' });
|
||
await run('prefer-software', { hardwareAcceleration: 'prefer-software' });
|
||
await run('latency=realtime', { latencyMode: 'realtime' });
|
||
await run('prefer-hardware + realtime', { hardwareAcceleration: 'prefer-hardware', latencyMode: 'realtime' });
|
||
await run('prefer-software + realtime', { hardwareAcceleration: 'prefer-software', latencyMode: 'realtime' });
|
||
await run('baseline avc1.42003e', { codec: 'avc1.42003e' });
|
||
|
||
log('\nDONE');
|
||
window.__DONE__ = true;
|
||
</script>
|
||
</body>
|
||
</html>
|