diff --git a/CHECKPOINT.md b/CHECKPOINT.md new file mode 100644 index 00000000..d3eae7fd --- /dev/null +++ b/CHECKPOINT.md @@ -0,0 +1,38 @@ +# Examples #29 / PR #64 — final frozen revision + +State: PASS under HQ's owner-adopted measured-wrap bar; frozen for parent commit, P2 response, CI/review/sign. No active jobs; no further render or encoding after candidate measurement. + +## Canonical input + +Origin `40db33d91e4aacbe4cd31ac9f3cc557973b4a1d4`; accepted Character 1 `family/roa/out/check/build/`, hash-bound by `family/accepted-inputs.json`. Model SHA256 `b9b61d55614805e468b27f9fb2dbaba65a50a0ce56b32ffef04f8cacd2570632`. All original corpus files remain unchanged. + +Pins: rig-c 2.68.0, spine-core 4.3.13, Bun 1.4.2, FFmpeg 7.0.2-static. Source rig/model/motion/art untouched; no generation, hand pass or retarget. + +## Adopted result and native boundary + +**rigc emits no closure verdict; this is the measured wrap** + +Original max1290 producer logs have zero `loop:` lines. Renderer help supplies no closure flag; `core_commands.ts:1030–1092` prints sample count, physics and sidecar, and `physicsLine` at 1117–1130 counts constraints. Actual native output: ` .. physics: 23 constraint(s) stepped at 12 fps`. Native closure is UNAVAILABLE, not PASS or FAIL. + +HQ adopted measured wrap MAE 0.44359887397323583 ≤ max adjacent 0.7597497951723703 as this card's bar: PASS. First/last max RGB difference 212 and 1,917 pixels over 40 are observations only; exact endpoint equality is NOT a bar. Repeat metadata is recorded separately. + +## Selected artifact + +- Predictor lossless APNG: 492×1290, 41 independently decoded frames, all RGBA bytes identical (max error 0), each delay exactly 1/12 s, infinite plays metadata 0. +- 11,351,964 bytes, SHA256 `bd31ad5b7d952733417a8ac830c40e73719f95ef0cdb8569e092c2bde3161f0e`. +- Original compressed reference: 17,453,151 bytes, SHA256 `462044902d2c1ac080d964238d3d05dab7eddafbe2c0940657cba8bf460c4ff7`, retained remotely throughout and after measurement. Reduction: 6,101,187 bytes, about 35%. +- Palette APNG: 2,638,904 bytes, max RGBA error 66, timing exact; rejected. +- GIF: 2,253,537 bytes, max RGBA error 66, 8/100 and 9/100-second delays; rejected for error and exact-timing failure. All candidates preserve 41 decoded frames and infinite metadata. +- Full candidate hashes/delays/commands are in the trial receipt. No rerender for optimization. + +## Verification and P2 + +- Optimization preflight `20261024T164354Z-ca8fa850a810460797563a2952311be5`: exit 0; actual source APNG decoder, palette readers, codec control and error controls 0/255. +- Candidate measurement `20261024T164502Z-48352c7770aa49189a0445bc3f2d7819`: exit 0; selected measured artifact copied unchanged. +- P2 discussion_r4367769592 at reviewed head b8422d447d7ae91586a61e849fc9b76d9dd1ee4f READ and fixed: absolute TRIAL from a trial-containing checkout, separate INPUT_ROOT with four pinned source hashes. Extracted README setup, fresh frozen install and actual renderer loader dry-run passed without rendering. +- `encode.mjs compress` uses the exact measured predictor arguments. Final revision validation is limited to lightweight hashes, syntax, path/setup consistency and command-argument comparison; no new full encode run is claimed for this final wiring. +- Prior 82 producer PNGs were hash-verified and deleted. Optimization used in-memory decoded RGBA; its temporary workspace was removed. Small logs and compressed candidates remain remotely. No original source deletion. + +## File ownership + +PR revision changes: CHECKPOINT.md plus trial README.md, encode.mjs, receipt.json and idle-2x.apng. The trial sheet.png remains unchanged. No family edits, git/public writes, new review requests, subagents or full suite. Parent owns commit, P2 response, CI, review and sign. diff --git a/trials/2026-10-25-character-1-idle-2x/README.md b/trials/2026-10-25-character-1-idle-2x/README.md new file mode 100644 index 00000000..42397edd --- /dev/null +++ b/trials/2026-10-25-character-1-idle-2x/README.md @@ -0,0 +1,209 @@ +# 2026-10-25 Character 1 idle at twice the recording scale + +## Question + +Does rendering the accepted Character 1 idle at a larger `rigc render --max` +produce a lossless recording at least 492 pixels wide, retaining all 41 +12-fps samples and a measured loop? + +[APNG recording](idle-2x.apng) · [41-frame sheet](sheet.png) · +[Machine-readable receipt](receipt.json) · [Encoder and verification](encode.mjs) + +## Inputs + +The input is the current accepted office Character 1 build, +[`family/roa/out/check/build/`](https://github.com/rig-c/examples/tree/40db33d91e4aacbe4cd31ac9f3cc557973b4a1d4/family/roa/out/check/build), +at examples commit `40db33d91e4aacbe4cd31ac9f3cc557973b4a1d4`. +The [accepted manifest](https://github.com/rig-c/examples/blob/40db33d91e4aacbe4cd31ac9f3cc557973b4a1d4/family/accepted-inputs.json) +and the Family guide identify that serving build as canonical. The receipt +records the exact model, skeleton, atlas and PNG hashes. They were read, +not rebuilt or edited. The site's historical `c239b09` build is not silently +substituted for this current accepted input. + +Withheld: nothing; the inputs already exist in the public corpus. New raw +frame sets are reproducible outputs, deleted after the sheet verdict and +concatenated-PNG hashes were recorded. Acquisition, pelvis, clean and retarget +are not applicable: this trial reads an existing idle and acquires no video +or new pose sequence. + +## Commands and pins + +| Tool | Pin | +| --- | --- | +| rig-c | 2.68.0 | +| official spine-core | 4.3.13 | +| Bun | 1.4.2 | +| FFmpeg, independent decoder | 7.0.2-static | + +Run from a checkout that **contains this trial** (the PR or a later merged +commit), not from the older input commit alone: that older commit predates +`encode.mjs` and `receipt.json`. `TRIAL` is an absolute path to these new trial +files. `INPUT_ROOT` may be this current checkout or a separate input checkout; +the setup verifies all four accepted input hashes before any rendering. The +historical commit identifies input provenance, not where the new script lives. + +Choose a new empty output path for `WORK`. Install only into that workspace. +The exact package manifest and frozen Bun lock are preserved in `receipt.json`. + +```bash +TRIAL="$PWD/trials/2026-10-25-character-1-idle-2x" +INPUT_ROOT="$PWD" +WORK="$PWD/idle-2x-reproduction" +python3 - "$TRIAL" "$INPUT_ROOT" "$WORK" <<'PYSETUP' +import hashlib, json, sys +from pathlib import Path +trial, input_root, work = [Path(p).resolve() for p in sys.argv[1:]] +assert (trial / 'encode.mjs').is_file(), 'TRIAL must contain the new trial script' +receipt = json.loads((trial / 'receipt.json').read_text()) +source = input_root / receipt['source'] +for name, expected in receipt['sourceInputHashes'].items(): + actual = hashlib.sha256((source / name).read_bytes()).hexdigest() + assert actual == expected['sha256'], 'accepted input hash mismatch: ' + name +work.mkdir(exist_ok=False) +(work / 'tools').mkdir() +(work / 'tools/package.json').write_text(json.dumps(receipt['toolchain']['packageJSON'], indent=2) + '\n') +(work / 'tools/bun.lock').write_text(receipt['toolchain']['bunLock']) +print('PASS: current trial files and all four pinned accepted input hashes') +PYSETUP +BUN_INSTALL_CACHE_DIR="$WORK/cache" bun install --cwd "$WORK/tools" --frozen-lockfile --ignore-scripts +export BUN_INSTALL_CACHE_DIR="$WORK/cache" +export BUN_RUNTIME_TRANSPILER_CACHE_PATH="$WORK/transpiler" +bun --no-install "$WORK/tools/node_modules/rig-c/bin/rigc.cjs" render --candidate "$INPUT_ROOT/family/roa/out/check/build" --animation idle --fps 12 --max 1290 --poser core --out "$WORK/frames" +bun "$TRIAL/encode.mjs" preflight "$WORK/frames" "$WORK/tools" +bun "$TRIAL/encode.mjs" encode "$WORK/frames" "$WORK/tools" "$WORK/reference.apng" +bun "$TRIAL/encode.mjs" compress "$WORK/reference.apng" "$WORK/tools" "$WORK/idle-2x.apng" +``` + +Heavy rendering and encoding were run on Nova. The staged order was render +checkpoint → actual PNG/FFmpeg reader dry-run and metric controls → encode +that same checkpoint → independently decode and compare all 41 frames. +`rigc render --help` was read before choosing the flags. Inspect the sheet +and retain the receipt before deleting the newly rendered frame directory. +The script prints the concatenated PNG hash and a separately labelled measured +seam. `compress` uses the exact FFmpeg predictor arguments measured for the +selected candidate, then checks timing/repeat metadata and independent decoded +RGBA equality. The original compressed reference was retained throughout +candidate measurement. No rerender was used for optimization. + +The README setup was extracted and executed with absolute `TRIAL` and +`INPUT_ROOT`: all four pinned source hashes, a fresh frozen install and the +actual renderer model/skeleton/atlas/PNG loader passed. This setup dry-run did +not render. It fixes PR #64 review +[discussion_r4367769592](https://github.com/rig-c/examples/pull/64#discussion_r4367769592). + +## Measurement + +The bar was fixed before rendering: width at least 492, all 41 samples at +12 fps, exact delay fractions and repeat metadata, and an explicit endpoint +and wrap measurement. HQ adopted the measured-wrap acceptance criterion: wrap RGB mean absolute +difference no larger than the largest adjacent RGB step. RGB ignores alpha; +the renderer's background is opaque `(232,232,232,255)`. Equal frames are the +zero control; a black-to-white pixel gives max/mean difference 255 and one +pixel over 40. Zero motion is handled without dividing by a zero step. + +| Measured | Result | Bar / interpretation | +| --- | --- | --- | +| First producer, max 1280 | 488×1280 | FAIL width ≥492 | +| Final producer, max 1290 | 492×1290 | PASS width ≥492 | +| Renderer samples | 41 at 12 fps, t=0…40/12 s | All retained | +| APNG decoded frames | 41, all RGBA pixels equal to source | PASS lossless | +| APNG delays | 41 × exactly 1/12 s | PASS; no centisecond rounding | +| Encoded duration | 41/12 s = 3.416666… s | Includes every sample, including endpoint | +| APNG `acTL.num_plays` | 0 | Infinite repeat metadata only | +| First vs last max RGB difference | 212; 1,917 pixels over 40 | Observation only; exact endpoint equality is **not a bar** | +| Wrap mean absolute RGB difference | 0.44359887397323583 | ≤0.7597497951723703 maximum adjacent step: PASS smooth wrap | +| Physics stepped during render | 23 constraints at 12 fps | Not a physics-disabled keyed-loop check | +| Selected APNG size | 11,351,964 bytes | Predictor-optimized; lossless RGBA, error 0 | + +The issue's expected 492×1280 is contradicted by the actual renderer. Its +unchanged world viewport rounds to width 488 at max 1280. Applying the real +renderer's `round(worldWidth * maxSide / worldHeight)` formula gives 1290 +as the smallest integer longest side reaching width 492; only scale changes. + +Measured wrap, **not a native renderer verdict**: + +```text +measured wrap: idle 41 frame(s) at 12 fps, f0000 vs f0040: max |d| 212; wrap MAE 0.44359887397323583 vs max adjacent 0.7597497951723703 — PASS smooth wrap; different endpoints +``` + +The authored animation lasts 40/12 s; displaying every one of its 41 sampled +frames gives a 41/12 s file. No last frame was dropped, replaced or blended. +Infinite repeat metadata is separate from seam quality and endpoint identity. + +| Retained output / deleted frame series | SHA-256 | +| --- | --- | +| `idle-2x.apng` | `bd31ad5b7d952733417a8ac830c40e73719f95ef0cdb8569e092c2bde3161f0e` | +| Final 41 PNGs concatenated in filename order | `85e0d85756b3efc384a01a235e38a2e9dc3f178dae626937cd3f511c407ec8d9` | +| Rejected 488-wide 41 PNGs concatenated | `2fa8b2b905dd788a3653aec38edff10bda4bddc3740439da7803d5e849459dd3` | + +The sheet shows all 41 labelled samples with the figure within the viewport; +that small-sheet verdict does not establish endpoint identity. Both producer +frame sets (82 PNGs total) were hash-verified and deleted after this verdict. + +### Native renderer boundary + +**rigc emits no closure verdict; this is the measured wrap** + +The original max1290 producer log has no `loop:` line. The 2.68.0 +`rigc render --help` has no loop/closure check flag. Its implementation, +`src/cli/core_commands.ts:1030–1092`, prints samples, physics and the sidecar +path; `physicsLine` at 1117–1130 counts stepped constraints, not closure. +The actual printed line is: + +```text + .. physics: 23 constraint(s) stepped at 12 fps +``` + +Native closure is **UNAVAILABLE**, not a native PASS or FAIL. A separate +`rig-parts check` loop definition excludes physics; it is not this renderer's +output. The first/last max difference of 212 is retained as an observation, +not promoted into an exact-endpoint acceptance bar. + +### Measured optimization comparison + +All candidates were encoded on Nova from the original compressed APNG, after +actual APNG/filter/codec reader preflight and zero/255 error controls. No +rerender was performed. Independent FFmpeg decode compared every candidate's +41 RGBA frames with the original 41 decoded renderer frames. Maximum error +below is the largest absolute difference over all RGBA bytes, on 0–255. + +| Encoding | Bytes | Decoded frames / max RGBA error | Actual delays | Decision | +| --- | --- | --- | --- | --- | +| Original full-frame APNG | 17,453,151 | 41 / 0 | 41 × 1/12 s | Reference retained remotely | +| Predictor lossless APNG | **11,351,964** | **41 / 0; every byte identical** | **41 × 1/12 s** | **Selected** | +| Palette APNG, no dithering | 2,638,904 | 41 / 66 | 41 × 1/12 s | Rejected: not lossless | +| Palette GIF, no dithering | 2,253,537 | 41 / 66 | 8/100 and 9/100 s | Rejected: error and exact-timing failure | + +All three measured candidates are 492×1290 with infinite-repeat metadata. +Predictor APNG saves 6,101,187 bytes (about 35%) while preserving every RGBA +byte. The smaller palette candidates are not labelled lossless or silently +accepted as near-lossless; no nonzero error budget was assumed. GIF's +centisecond clock cannot express an exact 1/12-second frame delay. The +receipt retains each candidate's SHA-256, all actual delay fractions and the +measured FFmpeg commands. + +## Verdict + +**PASS under HQ's owner-adopted measured-wrap bar** — wrap MAE +0.44359887397323583 ≤ maximum adjacent step 0.7597497951723703, with width, +all 41 samples, exact 12-fps delays, infinite-repeat metadata and lossless +decoded pixels preserved in the selected predictor APNG. Exact endpoint +equality is not a bar. **rigc emits no closure verdict; this is the measured wrap**. + +## Rejected alternatives + +- Max 1280: actual width 488 misses the size bar. +- Upscaling the old recording: would not rerender the accepted build. +- Dropping frame 40: violates the requested 41-frame preservation. +- Replacing/blending endpoints, changing motion or suppressing physics: + would alter what this accepted build actually renders. +- GIF: centisecond timing cannot express an exact 1/12-second delay. + +## Limits + +One accepted office idle only. This proves no new rig, artwork, hand motion, +retarget, physics settlement or arbitrary-force quality. The rendering includes +physics; the stored parts-check loop with physics excluded is not a substitute +for these measured pixels. The APNG keeps the renderer's opaque +background and every source frame's decoded RGBA values, using lossless predictor +compression. It remains larger than the rejected quantized recordings. No browser playback or new full toolchain suite was run. diff --git a/trials/2026-10-25-character-1-idle-2x/encode.mjs b/trials/2026-10-25-character-1-idle-2x/encode.mjs new file mode 100644 index 00000000..69b7c6ae --- /dev/null +++ b/trials/2026-10-25-character-1-idle-2x/encode.mjs @@ -0,0 +1,58 @@ +// Lossless full-canvas APNG: keep every renderer sample, including the endpoint. +import {readFileSync,writeFileSync,readdirSync} from 'node:fs'; +import {resolve,join} from 'node:path'; +import {pathToFileURL} from 'node:url'; +import {createHash} from 'node:crypto'; +import {execFileSync} from 'node:child_process'; +const [mode,framesArg,toolsArg,outArg]=process.argv.slice(2); +if(!['preflight','encode','compress'].includes(mode)||!framesArg||!toolsArg)throw Error('usage: bun encode.mjs preflight|encode|compress [out.apng]'); +const frames=resolve(framesArg),tools=resolve(toolsArg); +const {readPlate,pngChunk}=await import(pathToFileURL(join(tools,'node_modules/rig-c/tools/plate.ts')).href); +const version=JSON.parse(readFileSync(join(tools,'node_modules/rig-c/package.json'))).version; +if(version!=='2.68.0')throw Error('rig-c 2.68.0 required'); +const hash=b=>createHash('sha256').update(b).digest('hex'); +function compare(a,b){if(a.length!==b.length||a.length%4)throw Error('RGBA lengths');let max=0,sum=0,over40=0;for(let p=0;p40)over40++;}return {max,mae:sum/(a.length/4*3),pixelsOver40:over40};} +const black=Uint8Array.from([0,0,0,255]),white=Uint8Array.from([255,255,255,255]); +if(compare(black,black).max!==0||compare(black,white).max!==255||compare(black,white).mae!==255||compare(black,white).pixelsOver40!==1)throw Error('metric controls failed'); +function chunks(buffer){const out=[];for(let at=8;atc.type==='IHDR').body,actl=list.find(c=>c.type==='acTL').body;return {width:header.readUInt32BE(0),height:header.readUInt32BE(4),frames:actl.readUInt32BE(0),plays:actl.readUInt32BE(4),delays:list.filter(c=>c.type==='fcTL').map(c=>[c.body.readUInt16BE(20),c.body.readUInt16BE(22)||100])};} +function verifyAnimation(meta){if(meta.width!==492||meta.height!==1290||meta.frames!==41||meta.plays!==0||meta.delays.length!==41||!meta.delays.every(([n,d])=>n*12===d))throw Error('requires 492x1290, 41 frames, exact 1/12-second delays, infinite APNG');} +if(mode==='compress'){ + if(!outArg)throw Error('out.apng required'); + const output=resolve(outArg);if(output===frames)throw Error('source and output must differ'); + const source=readFileSync(frames);verifyAnimation(apngMetadata(source)); + execFileSync('ffmpeg',predictorArgs(frames,output),{maxBuffer:1024*1024}); + const encoded=readFileSync(output),meta=apngMetadata(encoded);verifyAnimation(meta); + const decode=path=>execFileSync('ffmpeg',['-v','error','-threads','1','-ignore_loop','1','-i',path,'-frames:v','41','-pix_fmt','rgba','-fps_mode','passthrough','-f','rawvideo','-'],{maxBuffer:512*1024*1024}); + const original=decode(frames),optimized=decode(output); + if(original.length!==492*1290*4*41||!original.equals(optimized))throw Error('optimized decoded RGBA differs'); + const receipt={mode:'predictor-lossless',sourceSHA256:hash(source),encodedSHA256:hash(encoded),bytes:encoded.length,...meta,decodedRGBAEqual:true,maxRGBAError:0,nativeRendererLoopLine:null,closureDefinition:'rigc emits no closure verdict; this is the measured wrap'}; + writeFileSync(output+'.json',JSON.stringify(receipt,null,2)+'\n');console.log(JSON.stringify(receipt));process.exit(0); +} +const metadata=JSON.parse(readFileSync(join(frames,'frames.json')));const set=metadata.sets[0]; +if(metadata.sets.length!==1||set.animation!=='idle'||set.fps!==12||set.written!==41||set.sampled!==41)throw Error('expected idle, 41 samples, 12 fps'); +const files=readdirSync(join(frames,set.dir)).filter(n=>/^f\d{4}\.png$/.test(n)).sort();if(files.length!==41)throw Error('missing frames'); +const digest=createHash('sha256');let width,height; +for(const file of files){const path=join(frames,set.dir,file),bytes=readFileSync(path),image=readPlate(path);digest.update(bytes);if(width===undefined){width=image.width;height=image.height;}if(image.width!==width||image.height!==height)throw Error('frame dimensions differ');const header=chunks(bytes).find(c=>c.type==='IHDR').body;if(header[8]!==8||header[9]!==6||header[12]!==0)throw Error('requires renderer non-interlaced 8-bit RGBA PNGs');} +if(width<492)throw Error('width below 492'); +const inputSHA256=digest.digest('hex'); +// This executes the decoder/demuxer on the actual checkpoint, without rendering or encoding. +execFileSync('ffmpeg',['-v','error','-threads','1','-framerate','12','-i',join(frames,set.dir,'f%04d.png'),'-frames:v','41','-f','null','-'],{maxBuffer:1024*1024}); +if(mode==='preflight'){console.log(JSON.stringify({pass:true,stage:'all PNG/APNG input readers and metric controls',frames:41,width,height,fps:12,concatPNGBytesSHA256:inputSHA256,encoder:'full canvas RGBA PNG chunks; CRC generated by rig-c pngChunk',decoder:'ffmpeg image2 -> null, 41 frames'}));process.exit(0);} +if(!outArg)throw Error('out.apng required'); +const output=resolve(outArg),pieces=[readFileSync(join(frames,set.dir,files[0])).subarray(0,8)]; +const first=chunks(readFileSync(join(frames,set.dir,files[0]))).find(c=>c.type==='IHDR').body;pieces.push(pngChunk('IHDR',first)); +const actl=Buffer.alloc(8);actl.writeUInt32BE(41,0);actl.writeUInt32BE(0,4);pieces.push(pngChunk('acTL',actl));let sequence=0; +for(let i=0;ic.type==='fcTL').map(c=>({numerator:c.body.readUInt16BE(20),denominator:c.body.readUInt16BE(22)})); +if(controls.length!==41||!controls.every(c=>c.numerator===1&&c.denominator===12))throw Error('APNG timing'); +const receipt={sourceCommit:'40db33d91e4aacbe4cd31ac9f3cc557973b4a1d4',source:'family/roa/out/check/build',rigc:version,bun:Bun.version,ffmpeg:execFileSync('ffmpeg',['-version'],{encoding:'utf8'}).split('\n')[0],frames:41,sampledFPS:12,dimensions:{width,height},animationDurationSeconds:set.duration,encodedDurationSeconds:41/12,frameDelays:controls,plays:0,pixelsLossless:true,concatPNGBytesSHA256:inputSHA256,encodedSHA256:hash(encoded),bytes:encoded.length,endpoint,maxAdjacentMAE:maxAdjacent,wrapMAE:endpoint.mae,smoothWrap,exactEndpoints:endpoint.max===0,physics:set.physics??null,nativeRendererLoopLine:null,closureDefinition:'rigc emits no closure verdict; this is the measured wrap',supportingSeamLine:`supporting seam: idle 41 frame(s) at 12 fps, f0000 vs f0040: max |d| ${endpoint.max}; wrap MAE ${endpoint.mae} vs max adjacent ${maxAdjacent} — ${smoothWrap?'PASS':'FAIL'} smooth wrap; ${endpoint.max===0?'identical':'different'} endpoints`}; +writeFileSync(output+'.json',JSON.stringify(receipt,null,2)+'\n');console.log(receipt.supportingSeamLine);console.log(JSON.stringify(receipt)); diff --git a/trials/2026-10-25-character-1-idle-2x/idle-2x.apng b/trials/2026-10-25-character-1-idle-2x/idle-2x.apng new file mode 100644 index 00000000..8235f946 Binary files /dev/null and b/trials/2026-10-25-character-1-idle-2x/idle-2x.apng differ diff --git a/trials/2026-10-25-character-1-idle-2x/receipt.json b/trials/2026-10-25-character-1-idle-2x/receipt.json new file mode 100644 index 00000000..d71403a1 --- /dev/null +++ b/trials/2026-10-25-character-1-idle-2x/receipt.json @@ -0,0 +1,1172 @@ +{ + "sourceCommit": "40db33d91e4aacbe4cd31ac9f3cc557973b4a1d4", + "source": "family/roa/out/check/build", + "rigc": "2.68.0", + "bun": "1.4.2", + "ffmpeg": "ffmpeg version 7.0.2-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2024 the FFmpeg developers", + "frames": 41, + "sampledFPS": 12, + "dimensions": { + "width": 492, + "height": 1290 + }, + "animationDurationSeconds": 3.333333333333333, + "encodedDurationSeconds": 3.4166666666666665, + "frameDelays": [ + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + }, + { + "numerator": 1, + "denominator": 12 + } + ], + "plays": 0, + "pixelsLossless": true, + "concatPNGBytesSHA256": "85e0d85756b3efc384a01a235e38a2e9dc3f178dae626937cd3f511c407ec8d9", + "encodedSHA256": "bd31ad5b7d952733417a8ac830c40e73719f95ef0cdb8569e092c2bde3161f0e", + "bytes": 11351964, + "endpoint": { + "max": 212, + "mae": 0.44359887397323583, + "pixelsOver40": 1917 + }, + "maxAdjacentMAE": 0.7597497951723703, + "wrapMAE": 0.44359887397323583, + "smoothWrap": true, + "exactEndpoints": false, + "physics": { + "stepped": 23, + "fps": 12 + }, + "verdict": "PASS under HQ owner-adopted measured-wrap bar: wrapMAE 0.44359887397323583 <= max adjacent 0.7597497951723703; all size/frame/timing/infinite/lossless bars pass. Exact endpoint equality is not a bar.", + "sourceInputHashes": { + "skeleton.json": { + "sha256": "be888884043311d976152070ae63e6dd4063b0cf8d3322f953be106b24b41c92", + "bytes": 623412 + }, + "skeleton.model.json": { + "sha256": "b9b61d55614805e468b27f9fb2dbaba65a50a0ce56b32ffef04f8cacd2570632", + "bytes": 990791 + }, + "skeleton.atlas": { + "sha256": "fc4cb05f341664e05a181847f6714321ee6ea3826f7a4f9cf3c85fc669014461", + "bytes": 1183 + }, + "skeleton.png": { + "sha256": "84cbd1b50458090896ad1cc55537df3565e3c1296b47996572efc77a9d8eb6e0", + "bytes": 1422720 + } + }, + "sourceManifestSHA256": "89aa3714b3f4a0442b729c3f1643bd07c8e24dbf9565c21150f88daf47da623c", + "renderCommand": [ + "rigc", + "render", + "--candidate", + "family/roa/out/check/build", + "--animation", + "idle", + "--fps", + "12", + "--max", + "1290", + "--poser", + "core", + "--out", + "" + ], + "encodeCommand": [ + "bun", + "encode.mjs", + "encode", + "", + "", + "" + ], + "encoderScriptSHA256": "fae6a49d24aa45d3ccdb561e9720b8edc933e753ebcf2d9429ba74a35538e686", + "controls": { + "constantFrame": { + "max": 0, + "mae": 0 + }, + "blackToWhiteDefect": { + "max": 255, + "mae": 255, + "pixelsOver40": 1 + }, + "zeroMotionEdge": "zero endpoint MAE <= zero max adjacent MAE is true; no division by a zero step" + }, + "barFixedBeforeMeasurement": true, + "sheet": { + "file": "sheet.png", + "sha256": "e3fed75798dc68b71e6064092904defdabab923dd77635d05e8227a8ee930ed7", + "verdict": "All 41 labelled tiles visible with the figure contained in the viewport; no missing tile observed. Small-sheet inspection does not prove exact endpoints." + }, + "rejected1280Producer": { + "dimensions": { + "width": 488, + "height": 1280 + }, + "concatPNGBytesSHA256": "2fa8b2b905dd788a3653aec38edff10bda4bddc3740439da7803d5e849459dd3", + "reason": "width below the predeclared 492 minimum" + }, + "toolchain": { + "packageJSON": { + "name": "examples-idle-2x-tools", + "private": true, + "type": "module", + "dependencies": { + "rig-c": "2.68.0", + "@esotericsoftware/spine-core": "4.3.13" + } + }, + "bunLock": "{\n \"lockfileVersion\": 2,\n \"configVersion\": 1,\n \"workspaces\": {\n \"\": {\n \"name\": \"examples-idle-2x-tools\",\n \"dependencies\": {\n \"@esotericsoftware/spine-core\": \"4.3.13\",\n \"rig-c\": \"2.68.0\",\n },\n },\n },\n \"packages\": {\n \"@esotericsoftware/spine-core\": [\"@esotericsoftware/spine-core@4.3.13\", \"\", {}, \"sha512-iGs/40qOWSQYicABebDzFSwFOS1Nsm0dCCS8sGqbpVuThRKdLYeI1cVJpmATSpEmtcetEABTmh97NoBnt7XLDw==\"],\n\n \"rig-c\": [\"rig-c@2.68.0\", \"\", { \"bin\": { \"rigc\": \"bin/rigc.cjs\" } }, \"sha512-8AIFb38iU4oJK43R2X1iWQo+MiT9/55a5SiRbqAhP4YCw7pjDfYOcN9LEe6e7DQW+DKxhG6+3QDYKBnStxw00A==\"],\n }\n}\n", + "bunLockSHA256": "0b81a866c218fec1728e56e8dff649c5679837722bdc8455fc090d5c03c0d465" + }, + "novaJobs": [ + "20261024T163053Z-a93effd31f28457baddfd45a19251c95", + "20261024T163209Z-c2f66be166854201a3aa38aa82035b50", + "20261024T163345Z-ec3a19af810f4d49a0b7749114700ef5", + "20261024T163538Z-ee335480381240dab28554ff2ee84045" + ], + "rigModelMotionArtworkChanged": false, + "framesRetainedInRepository": false, + "frameCleanup": { + "totalFramesDeleted": 82, + "bothConcatenatedDigestsVerifiedBeforeDeletion": true, + "originalCorpusTouched": false, + "producerFramesRetained": false + }, + "unoptimizedReference": { + "file": "full-frame-reference.apng", + "bytes": 17453151, + "sha256": "462044902d2c1ac080d964238d3d05dab7eddafbe2c0940657cba8bf460c4ff7" + }, + "closureDefinition": "rigc emits no closure verdict; this is the measured wrap", + "nativeRendererLoopLine": null, + "nativeClosure": "UNAVAILABLE: rigc render 2.68.0 emits no closure verdict", + "hqDisposition": { + "measuredWrapBarAdopted": true, + "criterion": "wrapMAE <= maxAdjacentMAE", + "result": "PASS", + "exactEndpointEqualityIsBar": false, + "smoothWrapAsNativeVerdict": false + }, + "supportingSeamLine": "measured wrap: idle 41 frame(s) at 12 fps, f0000 vs f0040: max |d| 212; wrap MAE 0.44359887397323583 vs max adjacent 0.7597497951723703 \u2014 PASS smooth wrap; different endpoints", + "nativeRendererEvidence": { + "rigc": "2.68.0", + "actualPrintedPhysicsLine": " .. physics: 23 constraint(s) stepped at 12 fps", + "loopColonOccurrencesInProducerLog": 0, + "producerLogSHA256": "9969117f1081a8b7c2bbc64dd5db7a9804d45030aa4f8da7a3ea675bff1caad3", + "source": "rig-c@2.68.0/src/cli/core_commands.ts:1030-1092,1117-1130", + "sourceSHA256": "15f34cbf335e81a4c704c660896701dc989892444bb53d431f4687ca466739b7", + "definition": "render prints frame count/duration, physics stepped and frames.json; no closure metric or loop check flag" + }, + "optimization": { + "reference": { + "file": "full-frame-reference.apng", + "bytes": 17453151, + "sha256": "462044902d2c1ac080d964238d3d05dab7eddafbe2c0940657cba8bf460c4ff7" + }, + "preflightJob": "20261024T164354Z-ca8fa850a810460797563a2952311be5", + "measurementJob": "20261024T164502Z-48352c7770aa49189a0445bc3f2d7819", + "candidates": [ + { + "file": "predictor-lossless.apng", + "bytes": 11351964, + "sha256": "bd31ad5b7d952733417a8ac830c40e73719f95ef0cdb8569e092c2bde3161f0e", + "format": "APNG", + "dimensions": [ + 492, + 1290 + ], + "frames": 41, + "delays": [ + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ] + ], + "infinite": true, + "plays": 0, + "decodedRGBAEqual": true, + "maxRGBAError": 0, + "decodedFrames": 41, + "exact12fpsDelay": true, + "seconds": 55.742, + "eligible": true + }, + { + "file": "palette.apng", + "bytes": 2638904, + "sha256": "99cd3bfec63a1ea622ead43a545ef3ffa156a802ebc36a259e11ee370f53fff7", + "format": "APNG", + "dimensions": [ + 492, + 1290 + ], + "frames": 41, + "delays": [ + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ], + [ + 1, + 12 + ] + ], + "infinite": true, + "plays": 0, + "decodedRGBAEqual": false, + "maxRGBAError": 66, + "decodedFrames": 41, + "exact12fpsDelay": true, + "seconds": 17.899, + "eligible": false + }, + { + "file": "palette.gif", + "bytes": 2253537, + "sha256": "7b67f6eb610533eb17cde80b65ec6823a013a22a3b6b7557d21ed3d3336a5730", + "format": "GIF", + "dimensions": [ + 492, + 1290 + ], + "frames": 41, + "delays": [ + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 9, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ], + [ + 8, + 100 + ] + ], + "infinite": true, + "plays": 0, + "decodedRGBAEqual": false, + "maxRGBAError": 66, + "decodedFrames": 41, + "exact12fpsDelay": false, + "seconds": 3.027, + "eligible": false + } + ], + "selected": "predictor-lossless.apng", + "selection": "Smallest measured candidate with max RGBA error 0 and the same 41-frame, exact-delay, 492x1290, infinite-play bars. Palette error 66 rejected; GIF also fails exact timing.", + "measuredCommands": [ + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-filter_threads", + "1", + "-filter_complex_threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-f", + "null", + "-" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-filter_threads", + "1", + "-filter_complex_threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-filter_complex", + "[0:v]split[a][b];[a]palettegen=stats_mode=full[p];[b][p]paletteuse=dither=none", + "-frames:v", + "41", + "-f", + "null", + "-" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-h", + "encoder=apng" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-h", + "encoder=gif" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-filter_threads", + "1", + "-filter_complex_threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-pix_fmt", + "rgba", + "-fps_mode", + "passthrough", + "-f", + "rawvideo", + "-" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-filter_threads", + "1", + "-filter_complex_threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-fps_mode", + "passthrough", + "-pix_fmt", + "rgba", + "-plays", + "0", + "-compression_level", + "9", + "-pred", + "mixed", + "-f", + "apng", + "" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-pix_fmt", + "rgba", + "-fps_mode", + "passthrough", + "-f", + "rawvideo", + "-" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-filter_threads", + "1", + "-filter_complex_threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-fps_mode", + "passthrough", + "-filter_complex", + "[0:v]split[a][b];[a]palettegen=stats_mode=full[p];[b][p]paletteuse=dither=none", + "-plays", + "0", + "-compression_level", + "9", + "-pred", + "mixed", + "-f", + "apng", + "" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-pix_fmt", + "rgba", + "-fps_mode", + "passthrough", + "-f", + "rawvideo", + "-" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-filter_threads", + "1", + "-filter_complex_threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-fps_mode", + "passthrough", + "-filter_complex", + "[0:v]split[a][b];[a]palettegen=stats_mode=full[p];[b][p]paletteuse=dither=none", + "-loop", + "0", + "-f", + "gif", + "" + ], + "exit": 0 + }, + { + "command": [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-ignore_loop", + "1", + "-i", + "", + "-frames:v", + "41", + "-pix_fmt", + "rgba", + "-fps_mode", + "passthrough", + "-f", + "rawvideo", + "-" + ], + "exit": 0 + } + ], + "referencePreservedRemotely": true, + "temporaryDecodedFrameFilesCreated": false, + "temporaryWorkspaceCleaned": true, + "rerendered": false + }, + "aiReviewFix": { + "finding": "https://github.com/rig-c/examples/pull/64#discussion_r4367769592", + "headReviewed": "b8422d447d7ae91586a61e849fc9b76d9dd1ee4f", + "fix": "Absolute TRIAL from current trial-containing checkout; separate INPUT_ROOT verified against four source hashes. Do not check out the historical input commit alone.", + "extractedReadmeSetupExecuted": true, + "frozenInstallExit": 0, + "actualRendererLoaderExit": 0, + "rerendered": false + }, + "compressionCommand": [ + "bun", + "encode.mjs", + "compress", + "", + "", + "" + ] +} diff --git a/trials/2026-10-25-character-1-idle-2x/sheet.png b/trials/2026-10-25-character-1-idle-2x/sheet.png new file mode 100644 index 00000000..67c42eee Binary files /dev/null and b/trials/2026-10-25-character-1-idle-2x/sheet.png differ