@bitefoundry/retrieviewer 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/EMBEDDING.md CHANGED
@@ -52,10 +52,16 @@ Redirected stdout prints one plain frame and exits.
52
52
  ## Assets / original option
53
53
 
54
54
  Default files resolve against the module URL, never cwd (lazy Node fs or browser
55
- HTTP fetch). getPetAssetFiles(profile?) returns exactly18 filenames:8 compressed
56
- bundles plus metadata and animal. Copy these without transforming bytes for
57
- bundlers. Loader returns ArrayBuffer/typed bytes; .gz must remain compressed,
58
- not transparently decompressed by HTTP Content-Encoding.
55
+ HTTP fetch). getPetAssetFiles(profile?) returns 19 filenames: nine art archives
56
+ plus ten metadata files. Copy these unchanged for bundlers. The default Q5 files
57
+ use `.rvz`, an index followed by individually gzip-compressed crops. A custom
58
+ loader returns the original ArrayBuffer/typed bytes, without unpacking the archive.
59
+
60
+ Local Node hosts using the default loader read only the indices at creation and
61
+ read/decode requested crops synchronously into a two-frame cache per family.
62
+ There is no background loader or extra process. The final pet's `dispose()` closes
63
+ the shared files. Custom loaders and browsers use the portable memory-backed
64
+ store; they do not receive the local file-backed memory reduction.
59
65
 
60
66
  ```js
61
67
  import {createPet,getPetAssetFiles} from '@bitefoundry/retrieviewer';
@@ -74,11 +80,11 @@ They are **not bundled** in the small distribution. For external original art,
74
80
  copy getPetAssetFiles('original') from the workspace and supply loadAsset. Missing
75
81
  originals fail clearly, never silently fall back.
76
82
 
77
- Q5 is lossy quantization (max source-pixel error4/255), not lossless compression.
78
- Art gzip18.7MB versus54.2MB original; resident packed pixels66.5MB.
79
- One Windows Node sample:172MB RSS after rendering, startup737ms, mean frame2.61ms
80
- at160×50. Samples, not guarantees. Node/Three remain runtime costs.
81
- See [PUPPY-Q5-COMPARISON.md](./PUPPY-Q5-COMPARISON.md).
83
+ Q5 is lossy quantization (max original-source pixel error 4/255). The indexed
84
+ repack is lossless relative to the approved Q5 art: all 2,401 logical frames and
85
+ their timing remain unchanged. The nine indexed archives total 19,326,068 bytes;
86
+ their indices total 54,040 bytes. No Three.js dependency runs in the pet core.
87
+ See [PUPPY-MEMORY.md](./PUPPY-MEMORY.md) for measured costs and host limitations.
82
88
 
83
89
  ## JSONL bridge (Python/Rust/other process hosts)
84
90
 
package/PUPPY-MEMORY.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Runtime weight and performance
2
2
 
3
+ ## Indexed local art (local candidate, 2026-10-25)
4
+
5
+ The default local Node loader opens nine RVZ5 files and reads only 54,040 bytes
6
+ of indices (plus 20-byte header probes). Each file contains the exact approved
7
+ RVQ5 filtered rows, individually gzip-compressed offline at level 9. Frame access
8
+ reads and inflates one requested crop into the existing two-frame-per-family
9
+ cache. No artwork family is decompressed/recompressed during startup; no loader
10
+ thread, process, dependency, polling timer or persistent user cache is added.
11
+ Shared pets hold one set of file descriptors. Last disposal closes all nine;
12
+ partial startup failure closes previously opened files. Repeated disposal is safe.
13
+
14
+ All 2,401 logical source frames are pixel-identical, including duplicates and
15
+ random seeks. The indexed repack round-trips to byte-identical RVQ5 archives.
16
+ The 450-frame fetch replay retains the same trajectory and visible SHA256
17
+ `76d5ec6c58622f4330a277852e0d6cb16f4bf2f9fd118688d9ec97b35f80f6a8`.
18
+ Timing, contacts, physics, palettes, art resolution and behavior are unchanged.
19
+
20
+ Fresh-process Windows Node 24 samples, seed 731, 480x135 cells, the existing
21
+ `tools/benchmark-pet.mjs` workload. ArrayBuffers/RSS samples below use diagnostic
22
+ GC after native zlib-close callbacks; these are not desktop memory guarantees.
23
+
24
+ | Metric | Previous | Indexed |
25
+ |---|---:|---:|
26
+ | Initial load | 1,481ms | 17ms |
27
+ | Loaded ArrayBuffers | 21,122,810B | 206,439B |
28
+ | Rendered ArrayBuffers | 27,658,258B | 6,741,887B |
29
+ | Loaded process RSS | 92,504,064B | 66,420,736B |
30
+ | Rendered process RSS | 166,457,344B | 148,267,008B |
31
+ | Peak benchmark process RSS | 207,421,440B | 158,302,208B |
32
+ | Mean frame time | 4.04ms | 4.09ms |
33
+ | P95 frame time | 6.44ms | 6.80ms |
34
+
35
+ This is a RAM/startup tradeoff, not a smaller download: art bytes increase from
36
+ 19,095,012 to 19,326,068 (about 1.2%) to allow independent crop reads. The npm
37
+ candidate is about 19.5MB versus 19.3MB; it contains only indexed Q5 art, not
38
+ duplicate legacy archives. Legacy source archives remain for authoring and exact
39
+ regression comparisons. `pet-indexed-manifest.json` describes the shipped format;
40
+ the older `pet-q5-manifest.json` describes its legacy source inputs.
41
+
42
+ Random-access storage adds synchronous small reads during rendering. The local
43
+ sample used OS-cached files; cold disks, network filesystems, antivirus and other
44
+ platforms can have different latency. Frame access validates gzip length/CRC and
45
+ filtered pixels on demand; headers, maps, crop bounds and offsets are validated
46
+ at opening. This differs from the old eager whole-archive pixel validation.
47
+
48
+ Browser/custom-loader compatibility is retained by restoring each indexed family
49
+ into the existing portable store during async creation. Those hosts do not get
50
+ the local file-backed RAM reduction. Actual Aside browser loading, Braille output
51
+ and a ball interaction were checked; no Node file module was requested there.
52
+ An isolated npm-installed Windows native check painted real frames, accepted the
53
+ native ball button and exited worker/painter cleanly. Its short Node sample was
54
+ 73MiB RSS, with native-helper memory separate; it is not a peak or long-run budget.
55
+ No live Linux/macOS check was performed for this storage change.
56
+
57
+ Evidence: local `dist/indexed-art-{before,after}.json`,
58
+ `dist/indexed-art-tests.txt`, `dist/package-check-*/package-check.json` and
59
+ `dist/windows-overlay-qa/`. Rebuild/check deterministic archives with
60
+ `npm run bake:art-index` / `npm run check:art-index` in the source checkout.
61
+
3
62
  ## Held-pose invalidation (local candidate, 2026-10-24)
4
63
 
5
64
  The desktop pump now asks `pet.needsFrame(viewport)` before rasterization. Only
package/README.ko.md CHANGED
@@ -108,6 +108,9 @@ npm uninstall -g @bitefoundry/retrieviewer
108
108
 
109
109
  ## StackBite 알림
110
110
 
111
+ 0.0.17은 기존 그림과 움직임을 유지하고, 기본 로컬 Node 실행에서 필요한 그림만
112
+ 파일에서 읽어요. [메모리 측정 기록](PUPPY-MEMORY.md)을 참고하세요.
113
+
111
114
  0.0.16은 앞발로 살살 부르기와 두 발로 서서 벽 박박 긁기를 번갈아 보여줘요.
112
115
  홈페이지에서도 두 동작을 눌러볼 수 있어요. Windows의 npm 설치형 StackBite도
113
116
  감시할 수 있게 했어요. 작업 완료 체크표시는 아직 알림 대상이 아니에요.
package/README.md CHANGED
@@ -141,6 +141,9 @@ Version 0.0.15 alerts without waiting for the model, shows the Seat name and
141
141
  pending kind locally on Windows, and lets the puppy approach before looking
142
142
  toward the sidebar. It reuses existing artwork; names never enter model input.
143
143
 
144
+ Version 0.0.17 keeps the same artwork and motion while loading only requested
145
+ frames from disk in the default local Node host. See [memory measurements](PUPPY-MEMORY.md).
146
+
144
147
  Version 0.0.16 adds two approved attention gestures: a gentle seated paw and a
145
148
  standing two-paw glass scrabble. They alternate for new alerts; both are playable
146
149
  in the website demo. Windows watching also supports npm's PowerShell launcher.
package/RELEASING.md CHANGED
@@ -1,4 +1,20 @@
1
- # Retrieviewer 0.0.16 release process
1
+ # Retrieviewer 0.0.17 release process
2
+
3
+ Current target: `@bitefoundry/retrieviewer@0.0.17`, public `latest`.
4
+ On 2026-10-25 the owner authorized publication of the indexed local-art update
5
+ and measurement of actual operating memory. Local Node hosts read only the art
6
+ indices at startup, then reconstruct requested crops from file-backed archives.
7
+ All 2,401 approved Q5 frames, timing and behavior remain unchanged. The browser
8
+ and custom-loader paths retain the compatible memory-backed representation.
9
+ No new dependency, background loader, process or persistent user cache is added.
10
+
11
+ Publish one fresh installed/native-tested tarball after Windows/Linux CI and
12
+ verify its registry integrity and a fresh registry consumer. Keep existing pets
13
+ running; measure only owned diagnostic windows and report Node and native-helper
14
+ working sets separately. No real approval, paid inference or user-window change
15
+ is needed for the measurement. Preserve all existing registry releases.
16
+
17
+ ## Previous 0.0.16 scope (historical)
2
18
 
3
19
  Current target: `@bitefoundry/retrieviewer@0.0.16`, public `latest`.
4
20
  On2026-10-24 the owner accepted the gentle-paw and standing-wall gestures and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitefoundry/retrieviewer",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "A fluffy Braille desktop pet for Windows and Linux X11, with experimental macOS support and an embeddable TUI engine.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://bitefoundry.github.io/retrieviewer/",
@@ -142,26 +142,29 @@
142
142
  "puppy-continuous.mjs",
143
143
  "puppy-zoomies.mjs",
144
144
  "puppy-zoomies30.mjs",
145
- "puppy-stride-tweens-q5.bin.gz",
145
+ "puppy-stride-tweens-q5.rvz",
146
146
  "puppy-stride-meta.json",
147
147
  "puppy-stable-meta.json",
148
- "puppy-paper-rear-gallop-q5.bin.gz",
148
+ "puppy-paper-rear-gallop-q5.rvz",
149
149
  "puppy-paper-rear-gallop.json",
150
- "puppy-paper-approach-q5.bin.gz",
150
+ "puppy-paper-approach-q5.rvz",
151
151
  "puppy-paper-approach.json",
152
- "puppy-paper-seat-q5.bin.gz",
152
+ "puppy-paper-seat-q5.rvz",
153
153
  "puppy-paper-seat.json",
154
- "puppy-paper-arrival-seat-q5.bin.gz",
154
+ "puppy-paper-arrival-seat-q5.rvz",
155
155
  "puppy-paper-arrival-seat.json",
156
- "puppy-paper-behaviors-q5.bin.gz",
156
+ "puppy-paper-behaviors-q5.rvz",
157
157
  "puppy-paper-behaviors.json",
158
- "puppy-paper-actions-v9-q5.bin.gz",
158
+ "puppy-paper-actions-v9-q5.rvz",
159
159
  "puppy-paper-actions-v9.json",
160
- "puppy-companion-interactions-v3-q5.bin.gz",
160
+ "puppy-companion-interactions-v3-q5.rvz",
161
161
  "puppy-companion-interactions-v3.json",
162
- "puppy-companion-comfort-q5.bin.gz",
162
+ "puppy-companion-comfort-q5.rvz",
163
163
  "puppy-companion-comfort.json",
164
- "vendor/procedural-animals/LICENSE"
164
+ "vendor/procedural-animals/LICENSE",
165
+ "puppy-frame-indexed.mjs",
166
+ "puppy-frame-file.mjs",
167
+ "pet-indexed-manifest.json"
165
168
  ],
166
169
  "scripts": {
167
170
  "bake:motion": "node tools/bake-pet-motion.mjs",
@@ -173,7 +176,9 @@
173
176
  "test": "node --test",
174
177
  "test:package": "node check-pet-package.mjs",
175
178
  "release:check": "npm run check:motion && node tools/check-release.mjs",
176
- "prepublishOnly": "npm run release:check"
179
+ "prepublishOnly": "npm run release:check",
180
+ "bake:art-index": "node tools/index-pet-art.mjs",
181
+ "check:art-index": "node tools/index-pet-art.mjs --check"
177
182
  },
178
183
  "dependencies": {
179
184
  "jsonc-parser": "3.3.1"
@@ -0,0 +1,106 @@
1
+ {
2
+ "version": 1,
3
+ "format": "RVZ5",
4
+ "pixels": "Exact approved RVQ5 rows; no quantization, frame removal or resampling",
5
+ "entries": [
6
+ {
7
+ "file": "puppy-stride-tweens-q5.rvz",
8
+ "bytes": 7643027,
9
+ "indexBytes": 19608,
10
+ "frames": 799,
11
+ "uniqueFrames": 683,
12
+ "sha256": "e049a3e5dbf2332620d881b8b484b1828dcc7aa3c9e636bf14358b3146458fd9",
13
+ "source": "puppy-stride-tweens-q5.bin.gz",
14
+ "sourceSha256": "abcc6f1825f09655f23e977c56e6790d27edd054c4c24be852c097fb575d6a4f",
15
+ "restoredRvq5Sha256": "eecf92fd402843bef43aa4b154db6eba999959dd16cd8e34c54e42c88eaade10"
16
+ },
17
+ {
18
+ "file": "puppy-paper-rear-gallop-q5.rvz",
19
+ "bytes": 1481295,
20
+ "indexBytes": 4100,
21
+ "frames": 162,
22
+ "uniqueFrames": 143,
23
+ "sha256": "fefa5949317e5afde1f5872f8ac43da95206a844cec1d90b27b2f31f32655ddd",
24
+ "source": "puppy-paper-rear-gallop-q5.bin.gz",
25
+ "sourceSha256": "ca731e1979a37ec2e005a80365e3b58358f640b8b26e7036b0e931940a7b1264",
26
+ "restoredRvq5Sha256": "fe96e31d30348cc7d1fc674f40778d2312c9d1b40c57e634dcc397a35cc5ccf4"
27
+ },
28
+ {
29
+ "file": "puppy-paper-approach-q5.rvz",
30
+ "bytes": 1462216,
31
+ "indexBytes": 4004,
32
+ "frames": 162,
33
+ "uniqueFrames": 139,
34
+ "sha256": "5f14d0c8f4b2a5a6f59be653cd6a9f3c0450260e0ae4e79074a0da5dc9f66821",
35
+ "source": "puppy-paper-approach-q5.bin.gz",
36
+ "sourceSha256": "17179d3ace7748f66ed3c5a61b5246d24ecd3570b350535bd2edc8cbf58304e6",
37
+ "restoredRvq5Sha256": "6f57a5f506eabea41c00f02738c8124a4ae8e05471bdcd76e0a6e9ed739ca1a3"
38
+ },
39
+ {
40
+ "file": "puppy-paper-seat-q5.rvz",
41
+ "bytes": 481353,
42
+ "indexBytes": 1400,
43
+ "frames": 51,
44
+ "uniqueFrames": 49,
45
+ "sha256": "569e943321eaad920f6915c4bd7c60f70571737c57a9407f3268ae7283565c4f",
46
+ "source": "puppy-paper-seat-q5.bin.gz",
47
+ "sourceSha256": "5480c994de0d034d70eff6ea86095f3722cf87bde72f364dd369b94b1ad66f84",
48
+ "restoredRvq5Sha256": "b21aa11039b314ed3dec96818e571d54b6102bd0bc1f76e0a99ceb6d2b5ae097"
49
+ },
50
+ {
51
+ "file": "puppy-paper-arrival-seat-q5.rvz",
52
+ "bytes": 492675,
53
+ "indexBytes": 1400,
54
+ "frames": 51,
55
+ "uniqueFrames": 49,
56
+ "sha256": "d69c7d59c99b6f12cebab4e92ec2aa137ba68efb56bb5d37cb52ea1b91d217a2",
57
+ "source": "puppy-paper-arrival-seat-q5.bin.gz",
58
+ "sourceSha256": "7effa0a2150e8ef9ed37d5b6cb10089672e0ce32599c06db21ad4769144bf246",
59
+ "restoredRvq5Sha256": "3b2ef9e322e625340c42a549f17165492cc14f724f4593bf298f67195677bb6e"
60
+ },
61
+ {
62
+ "file": "puppy-paper-behaviors-q5.rvz",
63
+ "bytes": 1288500,
64
+ "indexBytes": 3692,
65
+ "frames": 162,
66
+ "uniqueFrames": 126,
67
+ "sha256": "fac739a813a00bfd63294b3926b2463b60a2ab1a1b063b4ae02a053d2f925e8d",
68
+ "source": "puppy-paper-behaviors-q5.bin.gz",
69
+ "sourceSha256": "e4b79387aca974ceec74a02dc3f02780eb53d38af6ec9ebd69994de744b39716",
70
+ "restoredRvq5Sha256": "cb045558ca490436c5d6703f6468daba26f20ed76bc886e829787889253ccbb9"
71
+ },
72
+ {
73
+ "file": "puppy-paper-actions-v9-q5.rvz",
74
+ "bytes": 4691230,
75
+ "indexBytes": 13772,
76
+ "frames": 594,
77
+ "uniqueFrames": 474,
78
+ "sha256": "80621dbbdbd5272e9f4a40ac237f1d1f784a01456cdef48f2c10fdd45095f600",
79
+ "source": "puppy-paper-actions-v9-q5.bin.gz",
80
+ "sourceSha256": "abb4677468206c62937c0247dc4c5823e0a7c4a3a197da40194a08dd594aeef9",
81
+ "restoredRvq5Sha256": "615fb04961a38e592a661a20ad50fad45e1e4205e34198fa87a155ace966ea77"
82
+ },
83
+ {
84
+ "file": "puppy-companion-interactions-v3-q5.rvz",
85
+ "bytes": 1160887,
86
+ "indexBytes": 4436,
87
+ "frames": 360,
88
+ "uniqueFrames": 124,
89
+ "sha256": "237089d60d0350f08c4570c3379d44a6947d610b59c50856ed5f050d1debf420",
90
+ "source": "puppy-companion-interactions-v3-q5.bin.gz",
91
+ "sourceSha256": "8c7d24254f3f1e9fb85bff5ed1c7904fb4f111b0e47b57eee22dc094c18f3bf3",
92
+ "restoredRvq5Sha256": "5ebfc58c9cfeaf349d30bc65be2e4663f7a3b8be9f35ca5d6da323172624de7a"
93
+ },
94
+ {
95
+ "file": "puppy-companion-comfort-q5.rvz",
96
+ "bytes": 624885,
97
+ "indexBytes": 1628,
98
+ "frames": 60,
99
+ "uniqueFrames": 57,
100
+ "sha256": "9df13e0e4c5cd854e42aee595ee675eea716293e392d6e587145ffbaa74c5870",
101
+ "source": "puppy-companion-comfort-q5.bin.gz",
102
+ "sourceSha256": "57942c55e9c467d973a08f1e23804b0f756eaed46dddfe3aba7e7c3621996236",
103
+ "restoredRvq5Sha256": "5f598c2ba15212a73e88c02f0d189b20a74e46f2346f7dfbc3bca8c2d26fc66b"
104
+ }
105
+ ]
106
+ }
Binary file
@@ -0,0 +1,15 @@
1
+ // Loaded only by the default local Node host; browsers never import node:fs.
2
+ import {openSync,readSync,fstatSync,closeSync} from 'node:fs';
3
+ import {gunzipSync} from 'node:zlib';
4
+ import {indexedIndexLength,readIndexedIndex,createIndexedFrameStore} from './puppy-frame-indexed.mjs';
5
+ import {registerFrameStore} from './puppy-frame-store.mjs';
6
+
7
+ export function openFrameFile(url,options){
8
+ const fd=openSync(url,'r');let owned=true;
9
+ try{
10
+ const stat=fstatSync(fd);if(!stat.isFile())throw Error('Invalid frame file');
11
+ const read=(offset,size)=>{const out=new Uint8Array(size);let at=0;while(at<size){const n=readSync(fd,out,at,size-at,offset+at);if(!n)throw Error('Truncated frame file');at+=n;}return out;};
12
+ const header=read(0,20),length=indexedIndexLength(header),index=readIndexedIndex(read(0,length),stat.size);
13
+ const store=registerFrameStore(createIndexedFrameStore(index,{...options,read,expand:(b,n)=>gunzipSync(b,{maxOutputLength:Math.max(1,n)}),close:()=>closeSync(fd)}));owned=false;return store;
14
+ }finally{if(owned)closeSync(fd);}
15
+ }
@@ -0,0 +1,78 @@
1
+ // RVZ5: the approved RVQ5 rows, independently gzip-compressed for random access.
2
+ // Header/map match RVQ5; each 24-byte crop record stores x,y,w,h,offset,gzipLength.
3
+ // Offsets are relative to the end of the index. No new quantization or resampling.
4
+ import {unfilterRow} from './puppy-frame-predictive.mjs';
5
+ const HEADER=20,RECORD=24,MAX_PIXELS=16_777_216,MAX_INDEX=64*1024*1024,MAX_EXPANDED=512*1024*1024;
6
+ const fail=why=>{throw Error('Invalid indexed frames: '+why);};
7
+ const viewOf=b=>new DataView(b.buffer,b.byteOffset,b.byteLength);
8
+ export function indexedIndexLength(header){
9
+ if(!(header instanceof Uint8Array)||header.length<HEADER||String.fromCharCode(...header.subarray(0,4))!=='RVZ5')fail('header');
10
+ const v=viewOf(header),width=v.getUint32(4,true),height=v.getUint32(8,true),frames=v.getUint32(12,true),unique=v.getUint32(16,true);
11
+ if(!width||!height||width>16384||height>16384||width*height>MAX_PIXELS||!frames||!unique||unique>frames)fail('shape');
12
+ const length=HEADER+4*frames+RECORD*unique;
13
+ if(length>MAX_INDEX)fail('index size');return length;
14
+ }
15
+ export function readIndexedIndex(bytes,fileSize=bytes.length){
16
+ const length=indexedIndexLength(bytes);if(bytes.length<length||!Number.isSafeInteger(fileSize)||fileSize<length)fail('truncated index');
17
+ const v=viewOf(bytes),width=v.getUint32(4,true),height=v.getUint32(8,true),frameCount=v.getUint32(12,true),uniqueCount=v.getUint32(16,true);
18
+ const map=new Uint32Array(frameCount),used=new Uint8Array(uniqueCount),rects=new Uint32Array(uniqueCount*6);let payload=0,expanded=0;
19
+ for(let i=0;i<frameCount;i++){const u=v.getUint32(HEADER+i*4,true);if(u>=uniqueCount)fail('map');map[i]=u;used[u]=1;}
20
+ if(used.includes(0))fail('unreferenced frame');
21
+ for(let u=0;u<uniqueCount;u++){
22
+ const at=HEADER+frameCount*4+u*RECORD,r=Array.from({length:6},(_,i)=>v.getUint32(at+i*4,true)),[x,y,w,h,offset,size]=r;
23
+ if((w===0)!==(h===0)||(!w&&(x||y))||x+w>width||y+h>height)fail('rectangle');
24
+ if(offset!==payload||size<20)fail('offset');payload+=size;expanded+=h*(w+1);
25
+ if(expanded>MAX_EXPANDED)fail('expanded size');rects.set(r,u*6);
26
+ }
27
+ if(length+payload!==fileSize)fail('file size');
28
+ return {width,height,frameCount,uniqueCount,map,rects,indexBytes:length,expandedBytes:expanded,fileSize};
29
+ }
30
+ const display=Uint8Array.from({length:32},(_,q)=>Math.round(q*255/31));
31
+
32
+ // The host provides bounded positional reads and a synchronous gzip decoder.
33
+ // Only two reconstructed frames survive; compressed art stays on disk.
34
+ export function createIndexedFrameStore(index,{read,expand,close=()=>{},frameIndices}={}){
35
+ if(typeof read!=='function'||typeof expand!=='function')fail('reader');
36
+ const {width,height,frameCount,uniqueCount,map,rects,indexBytes}=index,n=width*height;
37
+ const retained=frameIndices===undefined?null:new Uint8Array(frameCount);
38
+ if(retained){if(!Array.isArray(frameIndices)||!frameIndices.length)fail('retained frames');for(const i of frameIndices){if(!Number.isInteger(i)||i<0||i>=frameCount)fail('retained frame index');retained[i]=1;}}
39
+ const slots=[{unique:-1,frame:null},{unique:-1,frame:null}],cur=new Uint8Array(width),prev=new Uint8Array(width);
40
+ let closed=false,decodes=0,hits=0,diskBytes=0;
41
+ return Object.freeze({width,height,frameCount,uniqueCount,byteLength:n*frameCount,residentPixelBytes:0,
42
+ get residentBytes(){return closed?0:map.byteLength+rects.byteLength+cur.byteLength+prev.byteLength+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
43
+ stats(){return {decodes,hits,cachedFrames:slots.filter(s=>s.frame).length,diskReads:decodes,diskBytes,closed};},
44
+ getFrame(i){
45
+ if(closed)throw Error('Frame store is disposed');
46
+ if(!Number.isInteger(i)||i<0||i>=frameCount)fail('frame index');if(retained&&!retained[i])fail('frame was not retained');
47
+ const u=map[i];if(slots[0].unique===u){hits++;return slots[0].frame;}if(slots[1].unique===u){hits++;slots.reverse();return slots[0].frame;}
48
+ const k=u*6,x=rects[k],y=rects[k+1],w=rects[k+2],h=rects[k+3],offset=rects[k+4],size=rects[k+5],expected=h*(w+1);
49
+ const compressed=read(indexBytes+offset,size);if(!(compressed instanceof Uint8Array)||compressed.length!==size)fail('short read');
50
+ const rows=expand(compressed,expected);if(!(rows instanceof Uint8Array)||rows.length!==expected)fail('expanded length');
51
+ // Mark the evicted slot invalid before parsing: a failed decode is never a hit.
52
+ const slot=slots.pop();slots.unshift(slot);slot.unique=-1;const frame=slot.frame??=new Uint8Array(n);frame.fill(0);prev.fill(0);
53
+ for(let r=0,at=0;r<h;r++){
54
+ const type=rows[at++];if(type>4)fail('filter');
55
+ unfilterRow(type,rows,at,cur,prev,w);at+=w;
56
+ for(let c=0;c<w;c++){
57
+ const q=cur[c];if(q>31)fail('quantized value');frame[(y+r)*width+x+c]=display[q];
58
+ }
59
+ prev.set(cur.subarray(0,w));
60
+ }
61
+ slot.unique=u;decodes++;diskBytes+=size;return frame;
62
+ },
63
+ dispose(){if(closed)return;closed=true;for(const slot of slots){slot.frame=null;slot.unique=-1;}close();},
64
+ });
65
+ }
66
+
67
+ // Portable/custom loaders retain the existing in-memory Q5 representation.
68
+ // The default local Node loader instead reads only the index and requested crops.
69
+ export async function expandIndexedArchive(bytes,expand){
70
+ const index=readIndexedIndex(bytes),{frameCount,uniqueCount,rects,indexBytes,expandedBytes}=index;
71
+ const out=new Uint8Array(indexBytes+expandedBytes);out.set(bytes.subarray(0,indexBytes));out.set([82,86,81,53]);const v=viewOf(out);let offset=0;
72
+ for(let u=0;u<uniqueCount;u++){
73
+ const k=u*6,size=rects[k+3]*(rects[k+2]+1),start=indexBytes+rects[k+4],rows=await expand(bytes.subarray(start,start+rects[k+5]),size);
74
+ if(!(rows instanceof Uint8Array)||rows.length!==size)fail('expanded length');
75
+ const at=HEADER+frameCount*4+u*RECORD;v.setUint32(at+16,offset,true);v.setUint32(at+20,size,true);out.set(rows,indexBytes+offset);offset+=size;
76
+ }
77
+ return out;
78
+ }
@@ -68,7 +68,7 @@ export function encodePredictiveFrames(rvf1,{filter}={}){
68
68
  }
69
69
 
70
70
  // Exact PNG inverse filter of one row into `cur` (bpp 1, integer arithmetic).
71
- function unfilterRow(type,src,at,cur,prev,w){
71
+ export function unfilterRow(type,src,at,cur,prev,w){
72
72
  switch(type){
73
73
  case 0:for(let x=0;x<w;x++)cur[x]=src[at+x];return;
74
74
  case 1:for(let x=0;x<w;x++)cur[x]=(src[at+x]+(x?cur[x-1]:0))&255;return;
@@ -11,6 +11,12 @@ import {isPredictiveFrames,readPredictiveFrames} from './puppy-frame-predictive.
11
11
  const MAGIC=[0x52,0x56,0x46,0x31],HEADER=20,RECORD=24,MAX_SIDE=16384;
12
12
  const stores=new WeakSet();
13
13
 
14
+ // Indexed local files obey the same borrowed two-frame contract as memory stores.
15
+ export function registerFrameStore(store){
16
+ if(!store||typeof store.getFrame!=='function'||typeof store.stats!=='function'||!Number.isInteger(store.width)||!Number.isInteger(store.height)||!Number.isInteger(store.frameCount))throw Error('Invalid frame store');
17
+ stores.add(store);return store;
18
+ }
19
+
14
20
  export function isPackedFrames(value){return stores.has(value);}
15
21
  export function isGrayFrameSource(value){return value instanceof Uint8Array||stores.has(value);}
16
22
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,7 +1,7 @@
1
1
  /** A headless pet: the host owns input, time and terminal rendering. */
2
2
  export type FetchReturnStyle = 'auto' | 'brake' | 'trot' | 'run-trot';
3
3
  export interface PetOptions {
4
- /** Asset names are relative; return original file bytes (keep .gz files compressed). */
4
+ /** Relative names; return unchanged .rvz/.gz file bytes. Custom loaders use memory-backed art. */
5
5
  loadAsset?: (name: string) => Promise<ArrayBuffer | ArrayBufferView>;
6
6
  seed?: number;
7
7
  energy?: number;
package/retrieviewer.mjs CHANGED
@@ -5,6 +5,7 @@ import {createSnappyTrickTimings} from './puppy-paper-trick-timing.mjs';
5
5
  import {createCompanionLife,FETCH_RETURN_STYLES} from './puppy-companion-life.mjs';
6
6
  import {paintCompanionObjects} from './puppy-companion-objects.mjs';
7
7
  import {readPackedFrames} from './puppy-frame-store.mjs';
8
+ import {expandIndexedArchive} from './puppy-frame-indexed.mjs';
8
9
  import {BAKED_MOTION} from './puppy-motion-tables.generated.mjs';
9
10
 
10
11
  // Embeddable autonomous puppy. Importing or creating has no DOM, timer, stdin/stdout
@@ -19,12 +20,12 @@ export const ART_PROFILES=Object.freeze(['original','q5']);
19
20
  export const PET_ASSETS=Object.freeze({
20
21
  // Kept as a legacy identifier only; createPet/getPetAssetFiles no longer load it.
21
22
  animal:'locomotion-puppy.animal',
22
- stride:'puppy-stride-tweens-q5.bin.gz',strideInfo:'puppy-stride-meta.json',stableInfo:'puppy-stable-meta.json',
23
+ stride:'puppy-stride-tweens-q5.rvz',strideInfo:'puppy-stride-meta.json',stableInfo:'puppy-stable-meta.json',
23
24
  rear:'puppy-paper-rear-gallop',approach:'puppy-paper-approach',seat:'puppy-paper-seat',arrivalSeat:'puppy-paper-arrival-seat',
24
25
  behaviors:'puppy-paper-behaviors',actions:'puppy-paper-actions-v9',interactions:'puppy-companion-interactions-v3',comfort:'puppy-companion-comfort',
25
26
  });
26
27
  const BUNDLES=['rear','approach','seat','arrivalSeat','behaviors','actions','interactions','comfort'];
27
- const frameFile=(base,profile)=>base+(profile==='q5'?'-q5.bin.gz':'-compact.bin.gz');
28
+ const frameFile=(base,profile)=>base+(profile==='q5'?'-q5.rvz':'-compact.bin.gz');
28
29
  /** Exact files a host/bundler must copy, including metadata. Never loads assets. */
29
30
  export function getPetAssetFiles(artProfile=DEFAULT_ART_PROFILE){
30
31
  if(!ART_PROFILES.includes(artProfile))throw RangeError('artProfile must be original or q5');
@@ -50,9 +51,9 @@ const bytesOf=(name,value)=>{
50
51
  if(ArrayBuffer.isView(value))return new Uint8Array(value.buffer,value.byteOffset,value.byteLength);
51
52
  throw TypeError(`loadAsset(${name}) must return an ArrayBuffer or typed bytes`);
52
53
  };
53
- async function gunzip(bytes){
54
+ async function gunzip(bytes,size){
54
55
  // Native Node gzip avoids Blob/Response/stream copies of every large cache.
55
- if(typeof process!=='undefined'&&process.versions?.node){const {gunzipSync}=await import('node:zlib');return gunzipSync(bytes);}
56
+ if(typeof process!=='undefined'&&process.versions?.node){const {gunzipSync}=await import('node:zlib');return gunzipSync(bytes,size===undefined?undefined:{maxOutputLength:Math.max(1,size)});}
56
57
  if(typeof DecompressionStream!=='undefined')return new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer());
57
58
  throw Error('This browser needs DecompressionStream for pet assets');
58
59
  }
@@ -75,8 +76,8 @@ function heldFrameKey(state,report,{columns,rows,cellAspect=.5,camera,poseFps=24
75
76
  report.ball,report.treat]);
76
77
  }
77
78
  async function loadPetAssets(loadAsset,artProfile){
78
- // Node keeps filtered crops compressed; only the existing two-frame LRU is
79
- // expanded. Browser bundles retain the portable bit-packed implementation.
79
+ // Default local Node hosts keep art on disk and reconstruct two frames per
80
+ // family. Custom/browser loaders retain the compatible memory-backed store.
80
81
  let codec;
81
82
  if(typeof process!=='undefined'&&process.versions?.node){
82
83
  const {deflateRawSync,inflateRawSync}=await import('node:zlib');
@@ -86,9 +87,18 @@ async function loadPetAssets(loadAsset,artProfile){
86
87
  }
87
88
  const load=async name=>bytesOf(name,await loadAsset(name));
88
89
  const json=async name=>JSON.parse(new TextDecoder().decode(await load(name)));
89
- const frames=async (name,options)=>readPackedFrames(await gunzip(await load(name)),{...options,codec});
90
- // One family at a time: never inflate all eight legacy-sized buffers together.
90
+ let openFrameFile;
91
+ if(artProfile==='q5'&&loadAsset===defaultLoadAsset&&new URL('.',import.meta.url).protocol==='file:'&&typeof process!=='undefined'&&process.versions?.node)({openFrameFile}=await import('./puppy-frame-file.mjs'));
92
+ const frames=async (name,options)=>{
93
+ if(name.endsWith('.rvz')){
94
+ if(openFrameFile)return openFrameFile(new URL(name,import.meta.url),options);
95
+ return readPackedFrames(await expandIndexedArchive(await load(name),gunzip),{...options,codec});
96
+ }
97
+ return readPackedFrames(await gunzip(await load(name)),{...options,codec});
98
+ };
99
+ // Read small indices locally; expand portable archives one family at a time.
91
100
  const data={assets:{}};
101
+ try{
92
102
  data.info=await json(PET_ASSETS.strideInfo);data.meta=await json(PET_ASSETS.stableInfo);
93
103
  data.cache=await frames(frameFile('puppy-stride-tweens',artProfile));
94
104
  for(const key of BUNDLES){
@@ -100,15 +110,17 @@ async function loadPetAssets(loadAsset,artProfile){
100
110
  data.assets[key]={info,bytes:await frames(frameFile(base,artProfile),options)};
101
111
  }
102
112
  return data;
113
+ }catch(error){disposeAssets(data);throw error;}
103
114
  }
115
+ function disposeAssets(data){data?.cache?.dispose?.();for(const family of Object.values(data?.assets??{}))family.bytes?.dispose?.();}
104
116
  async function acquireAssets(loadAsset,artProfile){
105
117
  let entry;
106
118
  if(loadAsset===defaultLoadAsset){entry=sharedAssets.get(artProfile);if(!entry){entry={users:0,promise:loadPetAssets(loadAsset,artProfile)};sharedAssets.set(artProfile,entry);}}
107
119
  else entry={users:0,promise:loadPetAssets(loadAsset,artProfile)};
108
120
  entry.users++;
109
121
  let released=false;
110
- const release=()=>{if(released)return;released=true;const previous=entry;entry=null;if(--previous.users===0&&sharedAssets.get(artProfile)===previous)sharedAssets.delete(artProfile);};
111
- try{return{data:await entry.promise,release};}catch(error){release();throw error;}
122
+ const release=()=>{if(released)return;released=true;const previous=entry;entry=null;if(--previous.users===0){if(sharedAssets.get(artProfile)===previous)sharedAssets.delete(artProfile);disposeAssets(previous.data);}};
123
+ try{const data=await entry.promise;entry.data=data;return{data,release};}catch(error){release();throw error;}
112
124
  }
113
125
 
114
126
  export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file