@bitefoundry/retrieviewer 0.0.18 → 0.0.20

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/NATIVE-RUNTIME.md CHANGED
@@ -1,4 +1,4 @@
1
- # Lightweight desktop runtime · 0.0.18
1
+ # Lightweight desktop runtime · 0.0.20
2
2
 
3
3
  Windows x64 basic desktop mode runs the existing JavaScript pet behavior in
4
4
  QuickJS-NG, inside the same native process that paints the transparent window.
@@ -41,15 +41,14 @@ play/treat/attention or tower/wheel actions. No forced GC or working-set trimmin
41
41
 
42
42
  | Pet | Median RSS | Sampled maximum | OS lifetime peak |
43
43
  |---|---:|---:|---:|
44
- | Retriever |29.4MiB|39.3MiB|39.9MiB|
45
- | Ginger cat |44.2MiB|61.8MiB|65.3MiB|
44
+ | Retriever |22.9MiB|37.9MiB|38.7MiB|
45
+ | Ginger cat with tower/wheel |44.4MiB|64.6MiB|65.5MiB|
46
46
 
47
- The cat's scene also includes the tower and wheel. It is typically below50MiB
48
- in this measurement, but its peaks exceed50MiB; this is not a strict50MiB cap.
49
- The preceding Node/CLR dog had a sampled maximum170.7MiB on the same host.
50
- Raw evidence: dist/native-product-memory/{dog,cat,report}.json (local QA output).
51
- Reproduce with the measurement command below. Package/website figures are
52
- rounded; memory remains dependent on scene, window dimensions and OS behavior.
47
+ Version 0.0.20 uses one cropped layered window for the complete scene. This
48
+ fixes flicker from the 0.0.19 multi-window painter; its lower memory figures are
49
+ superseded. Buffer shrinking and hidden-state cleanup remain. See the
50
+ [presentation fix](SPARSE-PAINT.md). These finite measurements do not establish
51
+ a universal 50 MiB memory cap. Raw evidence: dist/coherent-paint-memory/.
53
52
 
54
53
  ## Scope
55
54
 
@@ -87,3 +86,10 @@ off-screen diagnostic windows, preserving an already-running user companion.
87
86
  The executable embeds the Windows UTF-8 process code-page manifest (Windows10
88
87
  1903+ /11) so QuickJS stdio also reads Korean/non-ASCII installation paths.
89
88
  A regression test launches both species from Korean bundle, art and state paths.
89
+
90
+ ## Coherent scene presentation
91
+
92
+ [One complete-scene upload](SPARSE-PAINT.md) keeps the final pet, props and
93
+ furniture in the same HWND. That window stays click-through and non-activating.
94
+ The 128-frame presentation regression checks window identity, full-frame pixel
95
+ parity and hidden-resource release separately from pet-core pixel parity.
package/PUPPY-MEMORY.md CHANGED
@@ -1,3 +1,28 @@
1
+ # 0.0.20 coherent scene painter
2
+
3
+ The complete scene now updates through one native layered window to fix the
4
+ 0.0.19 flicker regression. Buffer shrinking, hidden-state release, stamp atlas
5
+ and indexed art loading remain; no art or pet behavior changed. Distant cat
6
+ furniture can enlarge the one bounding surface. The 0.0.19 figures below are
7
+ historical and must not describe the current version. See
8
+ [presentation evidence](SPARSE-PAINT.md) and [current measurements](NATIVE-RUNTIME.md).
9
+
10
+ Real 4K 150s measurements: dog median22.9MiB / OS peak38.7MiB; cat with
11
+ tower/wheel44.4 /65.5MiB. Watch/brain off, no forced GC/trim; not a hard cap.
12
+
13
+ ## Superseded 0.0.19 result (presentation regression)
14
+
15
+ # 0.0.19 sparse painter result
16
+
17
+ Windows x64 basic mode retains one native process and splits sparse content
18
+ into bounded cropped surfaces. In the same real4K150s schedule, dog median RSS
19
+ was22.6MiB (OS peak36.5MiB); cat with tower/wheel29.5MiB (peak39.1MiB).
20
+ Final masks and behavior are unchanged. No forced GC/trim; optional watching,
21
+ other OS hosts and browser tabs are outside this memory claim. See
22
+ [sparse paint evidence](SPARSE-PAINT.md) and [runtime scope](NATIVE-RUNTIME.md).
23
+
24
+ ## Previous0.0.18 native result (historical)
25
+
1
26
  # 0.0.18 native desktop result
2
27
 
3
28
  Windows x64 basic mode now uses a single QuickJS/Win32 process. In real4K
package/README.ko.md CHANGED
@@ -148,6 +148,19 @@ JS 플러그인과 JSONL 브리지를 제공해요. 화면·입력·시간 제
148
148
  AI로 생성한 그림에 자세 정렬·접지·프레임 처리를 적용한 애니메이션입니다.
149
149
  실제 모션 캡처나 자의식이 있는 강아지라고 주장하지 않아요.
150
150
 
151
+ ## 0.0.20
152
+
153
+ Windows에서 화면 일부가 깜빡이던 문제를 고쳤어요. 장면 전체를 창 하나로
154
+ 갱신하고, 버퍼 축소와 숨김 상태 정리는 유지합니다. 그림과 동작은 그대로예요.
155
+ [수정과 측정 기록](SPARSE-PAINT.md)을 참고하세요.
156
+
157
+ ## 0.0.19 (이전 버전 · 깜빡임 수정으로 대체)
158
+
159
+ Windows 기본 실행에서 표시 영역을 나누고 불필요하게 커진 버퍼를 회수해요.
160
+ 그림과 동작은 그대로예요. 실제4K150초 측정에서 강아지는 중앙값22.6MiB·최고36.5MiB,
161
+ 가구가 있는 고양이는29.5MiB·39.1MiB였어요. 선택형 감시·다른OS·브라우저는
162
+ 별도 범위예요. [측정과 검증 기록](SPARSE-PAINT.md)을 참고하세요.
163
+
151
164
  ## 0.0.18
152
165
 
153
166
  Windows x64 기본 실행은 네이티브 프로세스 하나만 남고 Node는 종료돼요.
package/README.md CHANGED
@@ -189,6 +189,21 @@ The puppy artwork was developed with AI-generated imagery and programmatic pose,
189
189
  grounding and frame processing. It is authored animation, not motion capture or
190
190
  a claim of sentience.
191
191
 
192
+ ## 0.0.20
193
+
194
+ Fixes Windows flicker introduced by the split-window painter. The whole scene
195
+ updates through one stable transparent window again; buffer shrinking and
196
+ hidden-state cleanup remain. Artwork and behavior are unchanged.
197
+ See [presentation fix and measurements](SPARSE-PAINT.md).
198
+
199
+ ## 0.0.19 (historical; superseded by flicker fix)
200
+
201
+ Windows basic mode now splits distant content into small paint regions and
202
+ reclaims oversized/hidden surfaces. Same pets, artwork and behavior. In real4K
203
+ 150s tests: dog median22.6MiB / OS peak36.5MiB; cat with tower/wheel29.5 /39.1MiB.
204
+ Optional watching and other OS/browser hosts keep their separate memory scope.
205
+ See [measurements and verification](SPARSE-PAINT.md).
206
+
192
207
  ## 0.0.18
193
208
 
194
209
  Windows x64 basic mode now runs one native process; Node exits after launch.
package/RELEASING.md CHANGED
@@ -1,4 +1,37 @@
1
- # Retrieviewer 0.0.18 release process
1
+ # Retrieviewer 0.0.20 release process
2
+
3
+ Current target: `@bitefoundry/retrieviewer@0.0.20`, public `latest`.
4
+ Hotfix for the reported flicker in the previously authorized npm/site release.
5
+ Replace the dynamically repartitioned HWNDs with one complete-scene layered
6
+ window and one pixel/position/size upload. Keep stamp caching, indexed art,
7
+ buffer shrinking and hidden-state release. Artwork and pet behavior are unchanged.
8
+
9
+ Verify moving/disjoint masks keep one stable window, exact native paint pixels,
10
+ resize/hide/restore/switch/teardown, full suite and fresh installed artifact.
11
+ Publish that same tested tarball after CI, verify registry integrity and a fresh
12
+ consumer, then confirm the matching bilingual website. Memory figures must
13
+ come from this version, not the regressed sparse painter. Existing code/art
14
+ licenses, optional integrations, user pet and privacy setup are preserved.
15
+
16
+ ## Previous 0.0.19 scope (historical; presentation regression)
17
+
18
+ ### Retrieviewer 0.0.19 release process
19
+
20
+ Current target: `@bitefoundry/retrieviewer@0.0.19`, public `latest`.
21
+ The owner explicitly authorized npm and website deployment on2026-10-25.
22
+ Ship the verified sparse Windows painter: bounded disjoint final-mask regions,
23
+ reclaimable buffers, preserved artwork/behavior and existing compatibility modes.
24
+ Keep code MIT and both artworks CC BY-SA4.0; no new generation inputs or assets.
25
+
26
+ Existing evidence covers300 exact native paint frames,56 partition cases,
27
+ real4K150s operating memory and owned-window shrink/hide/restore/switch/teardown.
28
+ Run the suite/release gate, build and install a fresh exact tarball, exercise its
29
+ actual native resources/picker, then publish that same artifact after CI.
30
+ Verify registry integrity, fresh registry installation and production website.
31
+ Keep website figures scoped to Windows basic mode; update EN/KO copy and cache
32
+ keys together. Preserve old releases, the user's running pet and privacy setup.
33
+
34
+ ## Previous0.0.18 scope (historical)
2
35
 
3
36
  Current target: `@bitefoundry/retrieviewer@0.0.18`, public `latest`.
4
37
  The owner authorized immediate npm and website deployment of the lightweight
@@ -0,0 +1,119 @@
1
+ # Coherent native painting · 0.0.20
2
+
3
+ The 0.0.19 sparse-window optimization is superseded. Per-frame pixel comparison
4
+ passed, but a moving scene could change its partition and destroy/reposition
5
+ multiple HWNDs at different compositor moments. The owner reported visible
6
+ flicker. Preparing all DIBs first did not make those window updates atomic.
7
+
8
+ Version 0.0.20 paints the complete final mask into one cropped layered window.
9
+ `UpdateLayeredWindow` uploads all pixels, position and size together. The same
10
+ HWND survives movement and crop changes. Visibility is changed only when first
11
+ shown; there is no extra per-frame `SetWindowPos` after upload. No animal assets,
12
+ anatomy, motion, Braille masks or optional integrations changed.
13
+
14
+ The 256-mask stamp atlas and one-process QuickJS host remain. Capacity still
15
+ shrinks when it exceeds twice the rounded target area; hidden/minimized scenes
16
+ release their surface and window. The tradeoff is one bounding bitmap spanning
17
+ distant furniture: memory can exceed the regressed split-painter measurements.
18
+ Those old numbers below must not be presented as current measurements.
19
+
20
+ ## Verification
21
+
22
+ - Real Win32 presentation regression: 128 changing/disjoint scenes retain one
23
+ visible HWND, with exact full-scene BGRA pixels; hidden resources release.
24
+ - Native mask/capacity tests: 56 grids across seven cell sizes.
25
+ - Real moving dog/cream, cat/gold and cat/dark: 301 exact paint comparisons.
26
+ - Owned 4K -> small -> 4K, minimize/restore and same-process species switching.
27
+ DIB capacity shrank from 12.22 to 1.50 MiB and reached zero when minimized.
28
+
29
+ Reproduce with `node --test native-desktop.test.mjs`,
30
+ `node tools/check-native-paint.mjs` and `node tools/check-native-resources.mjs`.
31
+ This checks actual window lifetime and upload coherence as well as image bytes;
32
+ finite pixel/lifetime checks are not a claim that every display was observed.
33
+
34
+ ## Current memory measurement
35
+
36
+ Same Windows x64 host, owned off-screen 3840x2160 target, real native painter,
37
+ seed731 and 150s action schedule, 300 roughly half-second samples per pet
38
+ (157.5/156.9s wall time). Watch/brain off; no forced GC or working-set trim.
39
+ Harness and external DWM/GPU allocations are excluded.
40
+
41
+ | Pet | Median RSS | Sampled maximum | OS lifetime peak |
42
+ |---|---:|---:|---:|
43
+ | Retriever |22.9MiB|37.9MiB|38.7MiB|
44
+ | Cat with tower/wheel |44.4MiB|64.6MiB|65.5MiB|
45
+
46
+ Cat furniture spans a large bounding bitmap again. This restores coherent
47
+ presentation at a higher RAM cost than 0.0.19; it is not a universal 50MiB cap.
48
+ Both owned processes stopped and removed their sessions. Raw local evidence:
49
+ `dist/coherent-paint-memory/{dog,cat,report}.json`. Full suite:244 passed,
50
+ two macOS-only tests skipped on Windows. No artwork or motion change.
51
+
52
+ ## Superseded 0.0.19 experiment (historical)
53
+
54
+ # Sparse Windows paint surfaces · 0.0.19
55
+
56
+ Version0.0.19 builds on0.0.18. JavaScript behavior, Q5 art, world/contact math, colors and
57
+ Braille masks are unchanged; the native painter allocates less empty space.
58
+
59
+ ## Change
60
+
61
+ The old painter kept one bitmap spanning every nonempty cell. The cat, tower
62
+ and wheel therefore retained a large transparent rectangle between objects.
63
+ Its width and height only grew until species switch/exit. A held-pose ablation
64
+ at3840×2160 measured cat+furniture38.6MiB versus cat alone17.6MiB; the equivalent
65
+ single DIB capacities were12.22MiB and1.63MiB respectively.
66
+
67
+ The new painter partitions the FINAL cell mask into up to8 disjoint cropped
68
+ regions. Greedy splits account for64px allocation rounding and must save at
69
+ least256KiB of bitmap space. No glyph is resampled, dropped, drawn twice or
70
+ recolored; geometry and palette still share the original stamp atlas. A dense
71
+ scene can remain a single region. This is a bounded native composition change,
72
+ not extra animal simulations or one process per region.
73
+
74
+ Each surface reuses capacity while it is within2× its current rounded target
75
+ area. Oversized capacity is reclaimed on repaint; unused regions are destroyed.
76
+ Hidden/minimized/retargeted scenes release their surfaces and overlay windows.
77
+ Resume repaints from the current simulation state. Windows system/compositor
78
+ allocations remain separate from the reported `paintSurfaceBytes` DIB counter.
79
+
80
+ ## Measurement
81
+
82
+ Same Windows11 x64 host, real native painter and an owned off-screen physical
83
+ 3840×2160 scene. Seed731; the existing150s diagnostic action schedule and300
84
+ roughly0.5s samples per pet. No forced GC/working-set trimming. Watch/brain off;
85
+ harness and external DWM/GPU memory excluded. Baseline is the tested0.0.18
86
+ installed-artifact run. These finite runs do not establish a universal RAM cap.
87
+
88
+ | Scene |0.0.18 median RSS|0.0.19 median RSS|0.0.18 OS peak|0.0.19 OS peak|
89
+ |---|---:|---:|---:|---:|
90
+ |Dog|29.35MiB|22.63MiB|40.34MiB|36.45MiB|
91
+ |Cat with tower/wheel|44.22MiB|29.46MiB|65.45MiB|39.13MiB|
92
+
93
+ The short fixed-pose cat+furniture comparison fell from38.6 to27.3MiB. This is
94
+ a different workload from the moving150s runs above, not a replacement for them.
95
+ Actual resize testing reduced allocated DIBs from6.52 to1.66MiB. Minimizing
96
+ reported0 DIB bytes and0 overlay HWNDs; restore and dog/cat switching recovered.
97
+
98
+ A paired16s cat timing check had92 paints/340 rendered calls in both variants.
99
+ Mean render time changed3.06→3.94ms, P95 12.42→16.68ms. Partitioning trades some
100
+ CPU for memory; these timings are not a constant-FPS guarantee.
101
+
102
+ ## Verification
103
+
104
+ - Native self-test:56 grids across7 cell sizes. Occupied masks covered once,
105
+ all regions disjoint/in bounds, partition plans no larger than one crop,
106
+ at most8 regions, and real DIB shrink/release verified.
107
+ - Real painter:300 moving frames across dog/cream and cat/gold/dark compared
108
+ against the old monolithic GDI+ ellipse output, byte-for-byte including alpha.
109
+ This tests per-frame paint pixels, not atomicity of compositor presentation.
110
+ - Real owned windows: pick/reselect,4K→small→4K, minimize/restore, species
111
+ switching, bootstrap detach, startup EOF, stop and Korean paths passed.
112
+ - Local full suite:243 passed,2 macOS-only tests skipped on this Windows host.
113
+
114
+ Reproduce with `node --test native-desktop.test.mjs`,
115
+ `node tools/check-native-paint.mjs`, `node tools/check-native-resources.mjs`,
116
+ and `node tools/measure-native-pet.mjs 150` after `npm run build:native`.
117
+ Local evidence is under dist/sparse-paint-memory, dist/paint-parity-HlzSTl,
118
+ dist/paint-resource-m3dMB1 and dist/sparse-timing-REFNLe. QA output is not
119
+ packaged. Linux/macOS/browser and optional Node/CLR watching paths are unchanged.
@@ -25,7 +25,10 @@ export async function startNativeDesktop(options={}){
25
25
  if(process.platform!=='win32'||process.arch!=='x64')throw Error('Native desktop currently requires Windows x64');
26
26
  await verifyNativeArtifact();const paths=await prepareRoot(options);await noLink(paths.session);
27
27
  const pipe=controlAddress({uuid:randomUUID()}),token=randomBytes(32).toString('hex');
28
- const child=spawn(fileURLToPath(binary),[fileURLToPath(bundle),fileURLToPath(new URL('../../',root)),'--production'],{detached:true,windowsHide:true,stdio:['pipe','pipe','pipe'],env:{...process.env,OPENROUTER_API_KEY:undefined}});
28
+ const args=[fileURLToPath(bundle),fileURLToPath(new URL('../../',root)),'--production'];
29
+ if(options.nativeDiagnostic&&options.nativeVerifyPaint)args.push('--verify-regions');
30
+ if(options.nativeDiagnostic&&options.nativeProofPath)args.push('--proof='+options.nativeProofPath);
31
+ const child=spawn(fileURLToPath(binary),args,{detached:true,windowsHide:true,stdio:['pipe','pipe','pipe'],env:{...process.env,OPENROUTER_API_KEY:undefined}});
29
32
  let owns=false,complete=false,stderr='';const session={owner:'bitefoundry-retrieviewer-overlay-v1',pid:child.pid,pipe,token};
30
33
  child.stderr.on('data',b=>stderr=(stderr+b.toString('utf8')).slice(-2048));child.stdin.on('error',()=>{});
31
34
  try{
@@ -74,6 +74,8 @@ static JSValue blit(JSContext* c,JSValueConst,int argc,JSValueConst* a){try{
74
74
  }catch(const std::exception& e){return failure(c,e);}}
75
75
  int wmain(int argc,wchar_t** wideArgs){
76
76
  std::vector<std::string> argText;for(int i=0;i<argc;i++){int n=WideCharToMultiByte(CP_UTF8,0,wideArgs[i],-1,nullptr,0,nullptr,nullptr);std::string value(n,0);WideCharToMultiByte(CP_UTF8,0,wideArgs[i],-1,value.data(),n,nullptr,nullptr);value.pop_back();argText.push_back(value);}std::vector<char*> args;for(auto& text:argText)args.push_back(text.data());char** argv=args.data();
77
+ if(argc==2&&std::string(argv[1])=="--self-test-regions")try{return NativePet::selfTestPaintRegions();}catch(const std::exception& e){fprintf(stderr,"%s\n",e.what());return 1;}
78
+ if(argc==2&&std::string(argv[1])=="--self-test-presentation")try{return NativePet::selfTestPaintPresentation();}catch(const std::exception& e){fprintf(stderr,"%s\n",e.what());return 1;}
77
79
  bool production=false;for(int i=1;i<argc;i++)if(std::string(argv[i])=="--production")production=true;std::string configuration;
78
80
  if(production){char ch;while(std::cin.get(ch)&&ch!='\n'){if(configuration.size()>=8192)return 2;configuration+=ch;}if(configuration.empty())return 2;}
79
81
  if(argc<2)return 2;JSRuntime* rt=JS_NewRuntime();js_std_init_handlers(rt);JSContext* ctx=JS_NewContext(rt);JS_SetModuleLoaderFunc2(rt,nullptr,js_module_loader,js_module_check_attributes,nullptr);js_init_module_std(ctx,"qjs:std");js_init_module_os(ctx,"qjs:os");js_std_add_helpers(ctx,argc-1,argv+1);
@@ -7,8 +7,8 @@
7
7
  "sha256": "e9da942dbe57ac778c44ea8f300e2e868face5e73b00ffcd4399aeb7e94ee09c"
8
8
  },
9
9
  "bin/win32-x64/retrieviewer-native.exe": {
10
- "bytes": 1551872,
11
- "sha256": "317841c3a61f78ee57e56b9eb7f778d59d17ee6a3ab49e04eab37a40509799c1"
10
+ "bytes": 1562624,
11
+ "sha256": "0844d0e5303938e11d3e57acb457d7f6df2f138ee84246ecdf006395055f0fba"
12
12
  }
13
13
  }
14
14
  }
@@ -0,0 +1,116 @@
1
+ // Included inside NativePet. Shared final-mask stamps preserve original pixels.
2
+ struct Surface {
3
+ HDC dc=nullptr;HBITMAP bitmap=nullptr;HGDIOBJ old=nullptr;uint8_t* bits=nullptr;int width=0,height=0;
4
+ ~Surface(){release();}
5
+ uint64_t bytes() const {return (uint64_t)width*height*4;}
6
+ void release(){if(dc&&old)SelectObject(dc,old);if(bitmap)DeleteObject(bitmap);if(dc)DeleteDC(dc);dc=nullptr;bitmap=nullptr;old=nullptr;bits=nullptr;width=height=0;}
7
+ void ensure(int w,int h){
8
+ const int rw=PetPaint::rounded(w),rh=PetPaint::rounded(h);
9
+ if(w<=width&&h<=height&&(uint64_t)width*height<=(uint64_t)rw*rh*2)return;
10
+ int nw=std::max(width,rw),nh=std::max(height,rh);
11
+ if((uint64_t)nw*nh>(uint64_t)rw*rh*2){nw=rw;nh=rh;}
12
+ release();width=nw;height=nh;BITMAPINFO info{};info.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);info.bmiHeader.biWidth=nw;info.bmiHeader.biHeight=-nh;info.bmiHeader.biPlanes=1;info.bmiHeader.biBitCount=32;info.bmiHeader.biCompression=BI_RGB;
13
+ HDC screen=GetDC(nullptr);dc=CreateCompatibleDC(screen);bitmap=CreateDIBSection(screen,&info,DIB_RGB_COLORS,(void**)&bits,nullptr,0);ReleaseDC(nullptr,screen);
14
+ if(!bitmap||!dc)throw std::runtime_error("Native surface allocation");old=SelectObject(dc,bitmap);
15
+ }
16
+ };
17
+ struct Panel {
18
+ HWND window=nullptr;Surface surface;PetPaint::Rect crop;
19
+ void release(){if(window)DestroyWindow(window);window=nullptr;surface.release();crop={};}
20
+ };
21
+ static std::array<Panel,PetPaint::MaxRegions> panels;
22
+ static void releasePanels(){for(auto& p:panels)p.release();}
23
+ static void hidePanels(){releasePanels();}
24
+ static uint64_t paintBytes(){uint64_t total=0;for(const auto& p:panels)total+=p.surface.bytes();return total;}
25
+ static size_t paintCount(){size_t n=0;for(const auto& p:panels)if(p.window)n++;return n;}
26
+ struct DotAtlas {
27
+ int cell=0;uint32_t color=0;std::vector<uint8_t> pixels;
28
+ void ensure(int value){
29
+ if(cell==value&&color==ink().GetValue())return;cell=value;color=ink().GetValue();size_t length=(size_t)cell*cell*2*4;pixels.assign(length*256,0);
30
+ static const int dx[]={0,0,0,1,1,1,0,1},dy[]={0,1,2,0,1,2,3,3};float step=cell/2.f,diameter=std::max(1.1f,step*.48f);
31
+ for(int mask=0;mask<256;mask++){Gdiplus::Bitmap bitmap(cell,cell*2,cell*4,PixelFormat32bppPARGB,pixels.data()+mask*length);Gdiplus::Graphics g(&bitmap);g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);Gdiplus::SolidBrush brush(ink());for(int bit=0;bit<8;bit++)if(mask&(1<<bit)){float x=(dx[bit]+.5f)*step,y=(dy[bit]+.5f)*step;g.FillEllipse(&brush,x-diameter/2,y-diameter/2,diameter,diameter);}}
32
+ }
33
+ };
34
+ static DotAtlas atlas;
35
+ static uint64_t verifiedPaints=0;
36
+ static std::vector<uint8_t> assemble(const PetPaint::Rect& all,int cell){
37
+ const int width=all.width()*cell,height=all.height()*cell*2;std::vector<uint8_t> image((size_t)width*height*4,0);
38
+ for(const auto& p:panels)if(p.window){const auto& r=p.crop;const int w=r.width()*cell,h=r.height()*cell*2;
39
+ for(int y=0;y<h;y++)memcpy(image.data()+((size_t)((r.top-all.top)*cell*2+y)*width+(r.left-all.left)*cell)*4,p.surface.bits+(size_t)y*p.surface.width*4,(size_t)w*4);
40
+ }
41
+ return image;
42
+ }
43
+ static void verifyPaint(const std::vector<uint8_t>& masks,int columns,const PetPaint::Rect& all,int cell){
44
+ const int w=all.width()*cell,h=all.height()*cell*2;std::vector<uint8_t> reference((size_t)w*h*4,0);
45
+ {Gdiplus::Bitmap bitmap(w,h,w*4,PixelFormat32bppPARGB,reference.data());Gdiplus::Graphics g(&bitmap);g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);Gdiplus::SolidBrush brush(ink());
46
+ static const int dx[]={0,0,0,1,1,1,0,1},dy[]={0,1,2,0,1,2,3,3};float step=cell/2.f,diameter=std::max(1.1f,step*.48f);
47
+ for(int y=all.top;y<all.bottom;y++)for(int x=all.left;x<all.right;x++){int mask=masks[(size_t)y*columns+x];for(int bit=0;bit<8;bit++)if(mask&(1<<bit)){float px=(x-all.left)*cell+(dx[bit]+.5f)*step,py=(y-all.top)*cell*2+(dy[bit]+.5f)*step;g.FillEllipse(&brush,px-diameter/2,py-diameter/2,diameter,diameter);}}g.Flush(Gdiplus::FlushIntentionSync);
48
+ }
49
+ if(reference!=assemble(all,cell))throw std::runtime_error("Native dots changed pixels");verifiedPaints++;
50
+ if(!atlasVerified){printf("{\"stage\":\"dot-atlas-verified\",\"pixelExact\":true,\"regions\":%zu}\n",paintCount());fflush(stdout);}atlasVerified=true;
51
+ }
52
+ static void paint(const std::string& text,int columns,int rows,int cell,POINT canvas){
53
+ std::vector<uint8_t> masks((size_t)columns*rows);size_t at=0;
54
+ for(int y=0;y<rows;y++){if(y){if(at>=text.size()||text[at++]!='\n')throw std::runtime_error("Native row separator");}for(int x=0;x<columns;x++){if(at+3>text.size())throw std::runtime_error("Native text length");const auto* p=(const unsigned char*)text.data()+at;uint32_t ch=((p[0]&15)<<12)|((p[1]&63)<<6)|(p[2]&63);at+=3;if(ch<0x2800||ch>0x28ff)throw std::runtime_error("Native glyph");masks[(size_t)y*columns+x]=(uint8_t)(ch-0x2800);}}
55
+ if(at!=text.size())throw std::runtime_error("Native trailing text");
56
+ const auto regions=PetPaint::partition(masks,columns,rows,cell);if(regions.empty()){releasePanels();return;}
57
+ PetPaint::Rect all;for(const auto& r:regions)all.include(r);
58
+ if((int64_t)all.width()*cell*all.height()*cell*2>8000000){hidePanels();visible=false;return;}
59
+ atlas.ensure(cell);const size_t stampSize=(size_t)cell*cell*2*4;
60
+ for(size_t i=0;i<regions.size();i++){
61
+ auto& panel=panels[i];panel.crop=regions[i];const auto& r=panel.crop;auto& surface=panel.surface;
62
+ if(!panel.window){panel.window=CreateWindowEx(WS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOOLWINDOW|WS_EX_NOACTIVATE|WS_EX_TOPMOST,L"RetrieviewerNativeDots",L"",WS_POPUP,0,0,1,1,nullptr,nullptr,GetModuleHandle(nullptr),nullptr);if(!panel.window)throw std::runtime_error("Native overlay region");}
63
+ surface.ensure(r.width()*cell,r.height()*cell*2);std::fill(surface.bits,surface.bits+(size_t)surface.width*surface.height*4,0);
64
+ for(int y=r.top;y<r.bottom;y++)for(int x=r.left;x<r.right;x++){int mask=masks[(size_t)y*columns+x];if(!mask)continue;const uint8_t* stamp=atlas.pixels.data()+mask*stampSize;for(int sy=0;sy<cell*2;sy++)memcpy(surface.bits+((size_t)((y-r.top)*cell*2+sy)*surface.width+(x-r.left)*cell)*4,stamp+(size_t)sy*cell*4,cell*4);}
65
+ }
66
+ for(size_t i=regions.size();i<panels.size();i++)panels[i].release();
67
+ if(verifyAtlas&&(!atlasVerified||verifyEveryPaint))verifyPaint(masks,columns,all,cell);
68
+ if(saveProof&&!proofSaved){const int w=all.width()*cell,h=all.height()*cell*2;auto image=assemble(all,cell);Gdiplus::Bitmap source(w,h,w*4,PixelFormat32bppPARGB,image.data()),proof(w,h,PixelFormat32bppARGB);Gdiplus::Graphics pg(&proof);pg.Clear(Gdiplus::Color(255,0,0,0));pg.DrawImage(&source,Gdiplus::Rect(0,0,w,h),0,0,w,h,Gdiplus::UnitPixel);pg.Flush(Gdiplus::FlushIntentionSync);CLSID png={0x557cf406,0x1a04,0x11d3,{0x9a,0x73,0x00,0x00,0xf8,0x1e,0xf3,0x2e}};if(proof.Save(wide(proofPath).c_str(),&png,nullptr)!=Gdiplus::Ok)throw std::runtime_error("PNG proof failed");proofSaved=true;}
69
+ // The complete scene (pet, ball and furniture) has exactly one HWND.
70
+ // UpdateLayeredWindow changes its pixels, size and position in one call.
71
+ // Do not add another per-frame SetWindowPos/show: it exposes separate state.
72
+ HDC screen=GetDC(nullptr);BLENDFUNCTION blend={AC_SRC_OVER,0,255,AC_SRC_ALPHA};POINT origin={0,0};
73
+ for(size_t i=0;i<regions.size();i++){auto& p=panels[i];POINT destination={canvas.x+p.crop.left*cell,canvas.y+p.crop.top*cell*2};SIZE size={p.crop.width()*cell,p.crop.height()*cell*2};BOOL ok=UpdateLayeredWindow(p.window,screen,&destination,&size,p.surface.dc,&origin,0,&blend,ULW_ALPHA);if(!ok){ReleaseDC(nullptr,screen);throw std::runtime_error("Native layered update");}if(!IsWindowVisible(p.window))ShowWindow(p.window,SW_SHOWNOACTIVATE);}
74
+ ReleaseDC(nullptr,screen);painted++;
75
+ }
76
+ static int selfTestPaintRegions(){
77
+ uint32_t random=731;auto next=[&](){random^=random<<13;random^=random>>17;random^=random<<5;return random;};size_t checks=0;
78
+ for(int cell:{6,8,10,12,16,24,32})for(int type=0;type<8;type++){
79
+ const int cols=type==7?1000:240,rows=type==7?125:90;std::vector<uint8_t> masks((size_t)cols*rows);
80
+ for(int y=0;y<rows;y++)for(int x=0;x<cols;x++){bool on=type==1||type==2&&x%61==0||type==3&&y%31==0||type==4&&(x<20||x>cols-30)&&y>10||type==5&&(next()%100<4)||type==6&&((x-cols/2)*(x-cols/2)+(y-rows/2)*(y-rows/2)<900)||type==7&&x%97<3&&y%17<2;if(on)masks[(size_t)y*cols+x]=(uint8_t)(1+next()%255);}
81
+ const auto regions=PetPaint::partition(masks,cols,rows,cell);if(regions.size()>PetPaint::MaxRegions)throw std::runtime_error("Non-atomic scene partition");std::vector<uint8_t> seen(masks.size(),0);PetPaint::Rect all;uint64_t bytes=0;
82
+ for(const auto& r:regions){if(r.empty()||r.left<0||r.top<0||r.right>cols||r.bottom>rows)throw std::runtime_error("Region outside grid");all.include(r);bytes+=PetPaint::bytes(r,cell);for(int y=r.top;y<r.bottom;y++)for(int x=r.left;x<r.right;x++)if(++seen[(size_t)y*cols+x]!=1)throw std::runtime_error("Overlapping paint regions");}
83
+ for(size_t i=0;i<masks.size();i++)if(masks[i]&&seen[i]!=1)throw std::runtime_error("Missing mask cell");if(bytes>PetPaint::bytes(all,cell))throw std::runtime_error("Partition grew allocation");checks++;
84
+ }
85
+ Surface s;s.ensure(2048,1024);const auto big=s.bytes();s.ensure(1800,900);if(s.bytes()!=big)throw std::runtime_error("Surface churn on small size change");s.ensure(96,64);if(s.bytes()>(uint64_t)128*64*4*2)throw std::runtime_error("Surface did not shrink");s.release();if(s.bytes()!=0)throw std::runtime_error("Surface did not release");
86
+ printf("{\"passed\":true,\"partitionCases\":%zu,\"coverageAndDisjointness\":true,\"surfaceShrink\":true}\n",checks);return 0;
87
+ }
88
+
89
+ // Real Win32 regression: moving/disjoint masks must retain one display window.
90
+ // Byte equality alone did not catch the multi-HWND presentation regression.
91
+ static int selfTestPaintPresentation(){
92
+ Gdiplus::GdiplusStartupInput input;ULONG_PTR token=0;
93
+ if(Gdiplus::GdiplusStartup(&token,&input,nullptr)!=Gdiplus::Ok)throw std::runtime_error("Presentation GDI+ startup");
94
+ WNDCLASS wc{};wc.lpfnWndProc=DefWindowProc;wc.hInstance=GetModuleHandle(nullptr);wc.lpszClassName=L"RetrieviewerNativeDots";RegisterClass(&wc);
95
+ const int columns=240,rows=90,cell=8;HWND first=nullptr;size_t checked=0;
96
+ verifyAtlas=verifyEveryPaint=atlasVerified=true;
97
+ try{
98
+ for(int frame=0;frame<128;frame++){
99
+ std::string text;
100
+ for(int y=0;y<rows;y++){if(y)text+='\n';for(int x=0;x<columns;x++){
101
+ bool pet=x>=20+frame%81&&x<55+frame%81&&y>=20&&y<65;
102
+ bool object=frame%3!=0&&x>=200-frame%13&&x<210-frame%13&&y>=5&&y<30;
103
+ unsigned ch=0x2800+(pet||object?1+(x+y+frame)%255:0);
104
+ text+=(char)(0xe0|(ch>>12));text+=(char)(0x80|((ch>>6)&63));text+=(char)(0x80|(ch&63));
105
+ }}
106
+ paint(text,columns,rows,cell,POINT{-12000,-12000});
107
+ if(paintCount()!=1||!IsWindowVisible(panels[0].window))throw std::runtime_error("Scene lost coherent visible window");
108
+ if(!first)first=panels[0].window;
109
+ if(panels[0].window!=first)throw std::runtime_error("Moving scene recreated its display window");
110
+ checked++;
111
+ }
112
+ hidePanels();if(paintCount()!=0||paintBytes()!=0)throw std::runtime_error("Hidden scene retained paint resources");
113
+ }catch(...){releasePanels();atlas.pixels.clear();Gdiplus::GdiplusShutdown(token);throw;}
114
+ atlas.pixels.clear();Gdiplus::GdiplusShutdown(token);
115
+ printf("{\"passed\":true,\"frames\":%zu,\"singleWindow\":true,\"stableWindow\":true,\"pixelExact\":true,\"hiddenResourcesReleased\":true}\n",checked);return 0;
116
+ }
@@ -1,16 +1,19 @@
1
1
  #include <windows.h>
2
2
  #include <gdiplus.h>
3
3
  #include <memory>
4
+ #include <array>
5
+ #include "paint-regions.hpp"
4
6
  #include "native-control.hpp"
5
7
  namespace NativePet {
6
8
  static JSContext* context;
7
- static HWND target=nullptr,overlay=nullptr;
9
+ static HWND target=nullptr;
8
10
  static bool showing=false,paused=false,failed=false;
9
11
  static ULONGLONG startTime=0,lastTick=0,durationMs=0;
10
12
  static int stage=0,painted=0;
11
13
  static std::string previous;
12
14
  static std::vector<double> timings;
13
- static bool saveProof=false,verifyAtlas=false,proofSaved=false,atlasVerified=false;
15
+ static bool saveProof=false,verifyAtlas=false,verifyEveryPaint=false,proofSaved=false,atlasVerified=false;
16
+ static std::string proofPath="dist/native-memory/native-window-proof.png";
14
17
  static bool production=false,diagnostic=false,testPicker=false,visible=false,inkExplicit=false;
15
18
  static HWND followed=nullptr,picker=nullptr,windowList=nullptr;
16
19
  static DWORD followedPid=0;
@@ -27,39 +30,15 @@ static Gdiplus::Color ink(){return inkName=="dark"?Gdiplus::Color(245,67,48,24):
27
30
  static void updateStatus();static void chooseWindow();static void layoutControls();
28
31
  static JSValue call(const char* name,int argc=0,JSValue* args=nullptr){JSValue global=JS_GetGlobalObject(context),fn=JS_GetPropertyStr(context,global,name),out=JS_Call(context,fn,global,argc,args);JS_FreeValue(context,fn);JS_FreeValue(context,global);if(JS_IsException(out)){js_std_dump_error(context);failed=true;if(target)PostMessage(target,WM_CLOSE,0,0);}return out;}
29
32
  static void interact(const char* kind){JSValue arg=JS_NewString(context,kind),result=call("__desktopInteract",1,&arg);bool accepted=!JS_IsException(result)&&JS_ToBool(context,result);JS_FreeValue(context,result);JS_FreeValue(context,arg);lastKind=kind;lastAccepted=accepted;if(!production||diagnostic){printf("{\"interaction\":\"%s\",\"accepted\":%s}\n",kind,accepted?"true":"false");fflush(stdout);}previous.clear();updateStatus();}
30
- struct Surface {
31
- HDC dc=nullptr;HBITMAP bitmap=nullptr;HGDIOBJ old=nullptr;uint8_t* bits=nullptr;int width=0,height=0;
32
- ~Surface(){release();}
33
- void release(){if(dc){if(old)SelectObject(dc,old);if(bitmap)DeleteObject(bitmap);DeleteDC(dc);}dc=nullptr;bitmap=nullptr;old=nullptr;bits=nullptr;width=height=0;}
34
- void ensure(int w,int h){if(w<=width&&h<=height)return;w=std::max(width,(w+63)/64*64);h=std::max(height,(h+63)/64*64);release();width=w;height=h;BITMAPINFO info{};info.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);info.bmiHeader.biWidth=w;info.bmiHeader.biHeight=-h;info.bmiHeader.biPlanes=1;info.bmiHeader.biBitCount=32;info.bmiHeader.biCompression=BI_RGB;HDC screen=GetDC(nullptr);dc=CreateCompatibleDC(screen);bitmap=CreateDIBSection(screen,&info,DIB_RGB_COLORS,(void**)&bits,nullptr,0);ReleaseDC(nullptr,screen);if(!bitmap||!dc)throw std::runtime_error("Native surface allocation");old=SelectObject(dc,bitmap);}
35
- };
36
- static Surface surface;
37
- struct DotAtlas {
38
- int cell=0;uint32_t color=0;std::vector<uint8_t> pixels;
39
- void ensure(int value){if(cell==value&&color==ink().GetValue())return;cell=value;color=ink().GetValue();size_t length=(size_t)cell*cell*2*4;pixels.assign(length*256,0);static const int dx[]={0,0,0,1,1,1,0,1},dy[]={0,1,2,0,1,2,3,3};float step=cell/2.f,diameter=std::max(1.1f,step*.48f);
40
- for(int mask=0;mask<256;mask++){Gdiplus::Bitmap bitmap(cell,cell*2,cell*4,PixelFormat32bppPARGB,pixels.data()+mask*length);Gdiplus::Graphics g(&bitmap);g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);Gdiplus::SolidBrush brush(ink());for(int bit=0;bit<8;bit++)if(mask&(1<<bit)){float x=(dx[bit]+.5f)*step,y=(dy[bit]+.5f)*step;g.FillEllipse(&brush,x-diameter/2,y-diameter/2,diameter,diameter);}}
41
- }
42
- };
43
- static DotAtlas atlas;
44
- static void paint(const std::string& text,int columns,int rows,int cell,POINT canvas){
45
- std::vector<uint8_t> masks((size_t)columns*rows);size_t at=0;int left=columns,top=rows,right=-1,bottom=-1;
46
- for(int y=0;y<rows;y++){if(y){if(at>=text.size()||text[at++]!='\n')throw std::runtime_error("Native row separator");}for(int x=0;x<columns;x++){if(at+3>text.size())throw std::runtime_error("Native text length");const auto* p=(const unsigned char*)text.data()+at;uint32_t ch=((p[0]&15)<<12)|((p[1]&63)<<6)|(p[2]&63);at+=3;if(ch<0x2800||ch>0x28ff)throw std::runtime_error("Native glyph");uint8_t mask=(uint8_t)(ch-0x2800);masks[y*columns+x]=mask;if(mask){left=std::min(left,x);right=std::max(right,x);top=std::min(top,y);bottom=std::max(bottom,y);}}}
47
- if(at!=text.size())throw std::runtime_error("Native trailing text");if(right<left){ShowWindow(overlay,SW_HIDE);return;}
48
- int w=(right-left+1)*cell,h=(bottom-top+1)*cell*2;if((int64_t)w*h>8000000){ShowWindow(overlay,SW_HIDE);visible=false;return;}surface.ensure(w,h);std::fill(surface.bits,surface.bits+(size_t)surface.width*surface.height*4,0);
49
- atlas.ensure(cell);size_t stampSize=(size_t)cell*cell*2*4;
50
- for(int y=top;y<=bottom;y++)for(int x=left;x<=right;x++){int mask=masks[y*columns+x];if(!mask)continue;const uint8_t* stamp=atlas.pixels.data()+mask*stampSize;for(int sy=0;sy<cell*2;sy++)memcpy(surface.bits+((size_t)((y-top)*cell*2+sy)*surface.width+(x-left)*cell)*4,stamp+(size_t)sy*cell*4,cell*4);}
51
- if(verifyAtlas&&!atlasVerified){std::vector<uint8_t> reference((size_t)w*h*4,0);Gdiplus::Bitmap bitmap(w,h,w*4,PixelFormat32bppPARGB,reference.data());Gdiplus::Graphics g(&bitmap);g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);Gdiplus::SolidBrush ink(NativePet::ink());static const int dx[]={0,0,0,1,1,1,0,1},dy[]={0,1,2,0,1,2,3,3};float step=cell/2.f,diameter=std::max(1.1f,step*.48f);for(int y=top;y<=bottom;y++)for(int x=left;x<=right;x++){int mask=masks[y*columns+x];for(int bit=0;bit<8;bit++)if(mask&(1<<bit)){float px=(x-left)*cell+(dx[bit]+.5f)*step,py=(y-top)*cell*2+(dy[bit]+.5f)*step;g.FillEllipse(&ink,px-diameter/2,py-diameter/2,diameter,diameter);}}g.Flush(Gdiplus::FlushIntentionSync);for(int y=0;y<h;y++)if(memcmp(reference.data()+(size_t)y*w*4,surface.bits+(size_t)y*surface.width*4,w*4)!=0)throw std::runtime_error("Dot atlas changed pixels");atlasVerified=true;printf("{\"stage\":\"dot-atlas-verified\",\"pixelExact\":true}\n");fflush(stdout);}
52
- if(saveProof&&!proofSaved){Gdiplus::Bitmap source(surface.width,surface.height,surface.width*4,PixelFormat32bppPARGB,surface.bits),proof(w,h,PixelFormat32bppARGB);Gdiplus::Graphics pg(&proof);pg.Clear(Gdiplus::Color(255,0,0,0));pg.DrawImage(&source,Gdiplus::Rect(0,0,w,h),0,0,w,h,Gdiplus::UnitPixel);pg.Flush(Gdiplus::FlushIntentionSync);CLSID png={0x557cf406,0x1a04,0x11d3,{0x9a,0x73,0x00,0x00,0xf8,0x1e,0xf3,0x2e}};if(proof.Save(L"dist/native-memory/native-window-proof.png",&png,nullptr)!=Gdiplus::Ok)throw std::runtime_error("PNG proof failed");proofSaved=true;}
53
- POINT destination={canvas.x+left*cell,canvas.y+top*cell*2},origin={0,0};SIZE size={w,h};BLENDFUNCTION blend={AC_SRC_OVER,0,255,AC_SRC_ALPHA};HDC screen=GetDC(nullptr);BOOL result=UpdateLayeredWindow(overlay,screen,&destination,&size,surface.dc,&origin,0,&blend,ULW_ALPHA);ReleaseDC(nullptr,screen);if(!result)throw std::runtime_error("Native layered update");SetWindowPos(overlay,HWND_TOPMOST,destination.x,destination.y,w,h,SWP_NOACTIVATE|SWP_SHOWWINDOW);painted++;
54
- }
33
+ #include "native-paint.hpp"
55
34
  static void updateStatus(){
56
35
  if(!production)return;
57
36
  PROCESS_MEMORY_COUNTERS_EX memory{};GetProcessMemoryInfo(GetCurrentProcess(),(PROCESS_MEMORY_COUNTERS*)&memory,sizeof(memory));
58
- std::ostringstream out;out<<"{\"running\":true,\"reachable\":true,\"engine\":\"quickjs-native\",\"pet\":\""<<petKind<<"\",\"ink\":\""<<inkName<<"\",\"returnStyle\":\""<<returnStyle<<"\",\"selected\":"<<(followed?"true":"false")<<",\"visible\":"<<(visible?"true":"false")<<",\"pickerVisible\":"<<(picker?"true":"false")<<",\"paused\":"<<(paused?"true":"false")<<",\"frames\":"<<frames<<",\"paintedFrames\":"<<painted<<",\"pid\":"<<GetCurrentProcessId()<<",\"painterPid\":"<<GetCurrentProcessId()<<",\"rssBytes\":"<<memory.WorkingSetSize<<",\"peakWorkingSetBytes\":"<<memory.PeakWorkingSetSize<<",\"privateBytes\":"<<memory.PrivateUsage<<",\"watch\":{\"source\":\"off\",\"state\":\"off\",\"pendingCount\":0},\"brain\":{\"provider\":\"off\",\"state\":\"off\"},\"lastInteraction\":";
37
+ std::ostringstream out;out<<"{\"running\":true,\"reachable\":true,\"engine\":\"quickjs-native\",\"pet\":\""<<petKind<<"\",\"ink\":\""<<inkName<<"\",\"returnStyle\":\""<<returnStyle<<"\",\"selected\":"<<(followed?"true":"false")<<",\"visible\":"<<(visible?"true":"false")<<",\"pickerVisible\":"<<(picker?"true":"false")<<",\"paused\":"<<(paused?"true":"false")<<",\"frames\":"<<frames<<",\"paintedFrames\":"<<painted<<",\"pid\":"<<GetCurrentProcessId()<<",\"painterPid\":"<<GetCurrentProcessId()<<",\"rssBytes\":"<<memory.WorkingSetSize<<",\"peakWorkingSetBytes\":"<<memory.PeakWorkingSetSize<<",\"privateBytes\":"<<memory.PrivateUsage<<",\"paintSurfaceBytes\":"<<paintBytes()<<",\"paintRegionCount\":"<<paintCount()<<",\"paintVerificationCount\":"<<verifiedPaints<<",\"watch\":{\"source\":\"off\",\"state\":\"off\",\"pendingCount\":0},\"brain\":{\"provider\":\"off\",\"state\":\"off\"},\"lastInteraction\":";
59
38
  if(lastKind.empty())out<<"null";else out<<"{\"kind\":\""<<lastKind<<"\",\"accepted\":"<<(lastAccepted?"true":"false")<<"}";out<<"}";control.update(out.str());
60
39
  }
61
40
  static void switchPet(const std::string& next){
62
- if(next==petKind)return;JSValue arg=JS_NewString(context,next.c_str()),result=call("__desktopSwitchPet",1,&arg);bool ok=!JS_IsException(result)&&JS_ToBool(context,result);JS_FreeValue(context,arg);JS_FreeValue(context,result);if(!ok)return;petKind=next;if(!inkExplicit)inkName=next=="cat"?"gold":"cream";previous.clear();surface.release();lastCanvas={INT_MIN,INT_MIN};layoutControls();updateStatus();
41
+ if(next==petKind)return;JSValue arg=JS_NewString(context,next.c_str()),result=call("__desktopSwitchPet",1,&arg);bool ok=!JS_IsException(result)&&JS_ToBool(context,result);JS_FreeValue(context,arg);JS_FreeValue(context,result);if(!ok)return;petKind=next;if(!inkExplicit)inkName=next=="cat"?"gold":"cream";previous.clear();releasePanels();lastCanvas={INT_MIN,INT_MIN};layoutControls();updateStatus();
63
42
  }
64
43
  static BOOL CALLBACK enumerate(HWND window,LPARAM){
65
44
  if(!IsWindowVisible(window)||GetWindow(window,GW_OWNER))return TRUE;DWORD pid=0;GetWindowThreadProcessId(window,&pid);if(!pid||pid==GetCurrentProcessId())return TRUE;wchar_t title[512];if(!GetWindowText(window,title,512))return TRUE;RECT rect;if(!GetClientRect(window,&rect)||rect.right<160||rect.bottom<120)return TRUE;LRESULT index=SendMessage(windowList,LB_ADDSTRING,0,(LPARAM)title);if(index!=LB_ERR&&index!=LB_ERRSPACE){SendMessage(windowList,LB_SETITEMDATA,index,(LPARAM)choices.size());choices.emplace_back(window,pid);}return TRUE;
@@ -73,7 +52,7 @@ static LRESULT CALLBACK pickerProcedure(HWND window,UINT message,WPARAM w,LPARAM
73
52
  if(message==WM_DESTROY){if(picker==window)picker=nullptr;if(!followed&&target)PostMessage(target,WM_CLOSE,0,0);updateStatus();return 0;}return DefWindowProc(window,message,w,l);
74
53
  }
75
54
  static void chooseWindow(){
76
- if(!production||(diagnostic&&!testPicker))return;if(picker){SetForegroundWindow(picker);return;}visible=false;ShowWindow(overlay,SW_HIDE);ShowWindow(target,SW_HIDE);previous.clear();WNDCLASS wc{};wc.lpfnWndProc=pickerProcedure;wc.hInstance=GetModuleHandle(nullptr);wc.lpszClassName=L"RetrieviewerNativePicker";wc.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);RegisterClass(&wc);
55
+ if(!production||(diagnostic&&!testPicker))return;if(picker){SetForegroundWindow(picker);return;}visible=false;hidePanels();ShowWindow(target,SW_HIDE);previous.clear();WNDCLASS wc{};wc.lpfnWndProc=pickerProcedure;wc.hInstance=GetModuleHandle(nullptr);wc.lpszClassName=L"RetrieviewerNativePicker";wc.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);RegisterClass(&wc);
77
56
  picker=CreateWindowEx(WS_EX_TOOLWINDOW,wc.lpszClassName,L"Retrieviewer - choose an app window",WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU, std::max(0,(GetSystemMetrics(SM_CXSCREEN)-650)/2),std::max(0,(GetSystemMetrics(SM_CYSCREEN)-450)/2),650,450,nullptr,nullptr,wc.hInstance,nullptr);if(!picker)throw std::runtime_error("Cannot open window picker");
78
57
  addPetCombo(picker);CreateWindow(L"STATIC",L"Choose the app window your companion should follow.",WS_CHILD|WS_VISIBLE,136,12,490,24,picker,nullptr,wc.hInstance,nullptr);windowList=CreateWindowEx(WS_EX_CLIENTEDGE,L"LISTBOX",L"",WS_CHILD|WS_VISIBLE|WS_VSCROLL|LBS_NOTIFY,12,44,610,310,picker,(HMENU)100,wc.hInstance,nullptr);CreateWindow(L"BUTTON",L"Attach",WS_CHILD|WS_VISIBLE|BS_DEFPUSHBUTTON,434,367,90,30,picker,(HMENU)101,wc.hInstance,nullptr);CreateWindow(L"BUTTON",L"Refresh",WS_CHILD|WS_VISIBLE,532,367,90,30,picker,(HMENU)102,wc.hInstance,nullptr);choices.clear();EnumWindows(enumerate,0);ShowWindow(picker,SW_SHOW);SetWindowPos(picker,HWND_TOP,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW);SetForegroundWindow(picker);updateStatus();
79
58
  }
@@ -91,9 +70,9 @@ static void tick(){
91
70
  DWORD scenePid=0;if(scene)GetWindowThreadProcessId(scene,&scenePid);
92
71
  if(production&&(!diagnostic||testPicker)&&scene&&(!IsWindow(scene)||scenePid!=followedPid)){followed=nullptr;chooseWindow();scene=nullptr;}
93
72
  if(picker||!scene||IsIconic(scene)||(production&&(!diagnostic||testPicker)&&GetForegroundWindow()!=scene&&GetForegroundWindow()!=target)||(showing&&GetForegroundWindow()!=target)){
94
- visible=false;ShowWindow(overlay,SW_HIDE);if(production&&(!diagnostic||testPicker))ShowWindow(target,SW_HIDE);previous.clear();updateStatus();return;
73
+ visible=false;hidePanels();if(production&&(!diagnostic||testPicker))ShowWindow(target,SW_HIDE);previous.clear();updateStatus();return;
95
74
  }
96
- RECT rect;GetClientRect(scene,&rect);POINT canvas{0,0};ClientToScreen(scene,&canvas);int cell=8,columns=rect.right/cell,rows=rect.bottom/(cell*2);while(columns*rows>125000&&cell<32){cell+=2;columns=rect.right/cell;rows=rect.bottom/(cell*2);}if(columns<8||rows<4||columns*rows>125000){visible=false;ShowWindow(overlay,SW_HIDE);previous.clear();updateStatus();return;}
75
+ RECT rect;GetClientRect(scene,&rect);POINT canvas{0,0};ClientToScreen(scene,&canvas);int cell=8,columns=rect.right/cell,rows=rect.bottom/(cell*2);while(columns*rows>125000&&cell<32){cell+=2;columns=rect.right/cell;rows=rect.bottom/(cell*2);}if(columns<8||rows<4||columns*rows>125000){visible=false;hidePanels();previous.clear();updateStatus();return;}
97
76
  visible=true;if(canvas.x!=lastCanvas.x||canvas.y!=lastCanvas.y||rect.right!=lastWidth||rect.bottom!=lastHeight){previous.clear();lastCanvas=canvas;lastWidth=rect.right;lastHeight=rect.bottom;}
98
77
  if(production&&(!diagnostic||testPicker)){RECT bar;GetWindowRect(target,&bar);int x=canvas.x+std::max<LONG>(0,rect.right-(bar.right-bar.left)),y=canvas.y+8;if(bar.left!=x||bar.top!=y||!IsWindowVisible(target))SetWindowPos(target,HWND_TOPMOST,x,y,0,0,SWP_NOSIZE|SWP_NOACTIVATE|SWP_SHOWWINDOW);}
99
78
  if(paused){updateStatus();return;}
@@ -105,24 +84,24 @@ static LRESULT CALLBACK procedure(HWND window,UINT message,WPARAM w,LPARAM l){
105
84
  if(message==WM_APP+1){if(w==1||w==2)switchPet(w==2?"cat":"dog");return 0;}
106
85
  if(message==WM_COMMAND){int id=LOWORD(w);if(id==9&&HIWORD(w)==CBN_SELCHANGE)switchPet(SendMessage((HWND)l,CB_GETCURSEL,0,0)==1?"cat":"dog");else if(id==1)interact(petKind=="cat"?"greet":"ball");else if(id==2)interact("treat");else if(id==3)interact(petKind=="cat"?"company":"tug");else if(id==4){paused=!paused;previous.clear();layoutControls();updateStatus();}else if(id==5){if(production)chooseWindow();else interact("paw");}else if(id==6)interact(production?"tower":"wall");else if(id==8)interact("wheel");else if(id==7)DestroyWindow(window);return 0;}
107
86
  if(message==WM_SIZE||message==WM_MOVE){previous.clear();return 0;}
108
- if(message==WM_DESTROY){KillTimer(window,1);followed=nullptr;if(picker){HWND old=picker;picker=nullptr;SetWindowLongPtr(old,GWLP_WNDPROC,(LONG_PTR)DefWindowProc);DestroyWindow(old);}if(overlay)DestroyWindow(overlay);overlay=nullptr;PostQuitMessage(0);return 0;}
87
+ if(message==WM_DESTROY){KillTimer(window,1);followed=nullptr;if(picker){HWND old=picker;picker=nullptr;SetWindowLongPtr(old,GWLP_WNDPROC,(LONG_PTR)DefWindowProc);DestroyWindow(old);}releasePanels();PostQuitMessage(0);return 0;}
109
88
  return DefWindowProc(window,message,w,l);
110
89
  }
111
90
  static int run(JSContext* ctx,int argc,char** argv){
112
- context=ctx;showing=false;durationMs=150000;for(int i=1;i<argc;i++){std::string arg=argv[i];if(arg=="--production"){production=true;durationMs=0;}if(arg=="--show"){showing=true;durationMs=0;}if(arg=="--proof")saveProof=true;if(arg=="--verify-atlas")verifyAtlas=true;if(arg.rfind("--duration=",0)==0)durationMs=(ULONGLONG)(std::stod(arg.substr(11))*1000);}
91
+ context=ctx;showing=false;durationMs=150000;for(int i=1;i<argc;i++){std::string arg=argv[i];if(arg=="--production"){production=true;durationMs=0;}if(arg=="--show"){showing=true;durationMs=0;}if(arg=="--proof")saveProof=true;if(arg=="--verify-atlas")verifyAtlas=true;if(arg=="--verify-regions"){verifyAtlas=true;verifyEveryPaint=true;}if(arg.rfind("--proof=",0)==0){saveProof=true;proofPath=arg.substr(8);}if(arg.rfind("--duration=",0)==0)durationMs=(ULONGLONG)(std::stod(arg.substr(11))*1000);}
113
92
  JSValue global=JS_GetGlobalObject(ctx),config=JS_GetPropertyStr(ctx,global,"__nativeConfig");if(JS_IsObject(config)){petKind=propertyString(ctx,config,"pet","dog");inkName=propertyString(ctx,config,"ink",petKind=="cat"?"gold":"cream");returnStyle=propertyString(ctx,config,"returnStyle","auto");inkExplicit=propertyBool(ctx,config,"inkExplicit");diagnostic=propertyBool(ctx,config,"diagnostic");testPicker=diagnostic&&propertyBool(ctx,config,"testPicker");}if(petKind!="dog"&&petKind!="cat")throw std::runtime_error("Invalid native pet");if(inkName!="cream"&&inkName!="gold"&&inkName!="dark")throw std::runtime_error("Invalid native ink");
114
93
  SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);Gdiplus::GdiplusStartupInput gdip;ULONG_PTR token;if(Gdiplus::GdiplusStartup(&token,&gdip,nullptr)!=Gdiplus::Ok)throw std::runtime_error("GDI+ startup");
115
94
  if(production){std::wstring name=wide("Local\\BiteFoundry.Retrieviewer.Overlay."+userSid()+(diagnostic?".Diagnostics."+std::to_string(GetCurrentProcessId()):""));instanceMutex=CreateMutexW(nullptr,TRUE,name.c_str());if(!instanceMutex||GetLastError()==ERROR_ALREADY_EXISTS)throw std::runtime_error("A Retrieviewer companion is already running");}
116
95
  HINSTANCE instance=GetModuleHandle(nullptr);WNDCLASS wc{};wc.lpfnWndProc=procedure;wc.hInstance=instance;wc.lpszClassName=L"RetrieviewerNativeHost";wc.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);RegisterClass(&wc);
117
96
  const bool attach=production&&(!diagnostic||testPicker);target=CreateWindowEx(WS_EX_TOOLWINDOW|(attach?WS_EX_NOACTIVATE:0),wc.lpszClassName,attach?L"Retrieviewer controls":L"Retrieviewer diagnostic scene",showing?WS_OVERLAPPEDWINDOW:WS_POPUP,showing?80:-12000,showing?80:-12000,attach?552:showing?1280:3840,attach?40:showing?800:2160,nullptr,nullptr,instance,nullptr);if(!target)throw std::runtime_error("Native host window");if(!attach)followed=target;
118
- WNDCLASS pet{};pet.lpfnWndProc=DefWindowProc;pet.hInstance=instance;pet.lpszClassName=L"RetrieviewerNativeDots";RegisterClass(&pet);overlay=CreateWindowEx(WS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOOLWINDOW|WS_EX_NOACTIVATE|WS_EX_TOPMOST,pet.lpszClassName,L"",WS_POPUP,0,0,1,1,nullptr,nullptr,instance,nullptr);if(!overlay)throw std::runtime_error("Native overlay window");
97
+ WNDCLASS pet{};pet.lpfnWndProc=DefWindowProc;pet.hInstance=instance;pet.lpszClassName=L"RetrieviewerNativeDots";RegisterClass(&pet);
119
98
  if(attach){addPetCombo(target);for(int id=1;id<=8;id++)CreateWindow(L"BUTTON",L"",WS_CHILD|BS_PUSHBUTTON,0,0,1,1,target,(HMENU)(INT_PTR)id,instance,nullptr);layoutControls();chooseWindow();}
120
99
  else if(showing){const wchar_t* names[]={L"Ball",L"Treat",L"Tug",L"Pause",L"Paw",L"Wall",L"Quit"};for(int i=0;i<7;i++)CreateWindow(L"BUTTON",names[i],WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,8+i*70,8,66,28,target,(HMENU)(INT_PTR)(i+1),instance,nullptr);}
121
100
  if(!attach)ShowWindow(target,showing?SW_SHOW:SW_SHOWNOACTIVATE);startTime=GetTickCount64();lastTick=0;timings.reserve(1024);SetTimer(target,1,33,nullptr);
122
101
  if(production){updateStatus();control.start(propertyString(ctx,config,"pipe"),propertyString(ctx,config,"token"),propertyString(ctx,config,"sessionFile"),[](const std::string& command){PostMessage(target,command=="stop"?WM_CLOSE:WM_APP+1,command=="pet-cat"?2:command=="pet-dog"?1:0,0);});printf("{\"ready\":true,\"pid\":%lu}\n",GetCurrentProcessId());fflush(stdout);startupGuard=std::thread([]{std::string ack;bool ok=std::getline(std::cin,ack)&&ack=="DETACH";if(ok){detached=true;printf("{\"detached\":true}\n");fflush(stdout);}else{DWORD pid=0;GetWindowThreadProcessId(target,&pid);if(pid==GetCurrentProcessId())PostMessage(target,WM_CLOSE,0,0);}});}
123
102
  else{detached=true;printf("{\"stage\":\"native-window\",\"width\":%d,\"height\":%d,\"durationMs\":%llu,\"pid\":%lu}\n",showing?1280:3840,showing?800:2160,durationMs,GetCurrentProcessId());fflush(stdout);}
124
103
  JS_FreeValue(ctx,config);JS_FreeValue(ctx,global);MSG msg;while(GetMessage(&msg,nullptr,0,0)>0){TranslateMessage(&msg);DispatchMessage(&msg);}
125
- if(startupGuard.joinable()){CancelSynchronousIo(startupGuard.native_handle());startupGuard.join();}control.close();JSValue result=call("__desktopDispose");JS_FreeValue(ctx,result);surface.release();if(instanceMutex){ReleaseMutex(instanceMutex);CloseHandle(instanceMutex);instanceMutex=nullptr;}Gdiplus::GdiplusShutdown(token);
104
+ if(startupGuard.joinable()){CancelSynchronousIo(startupGuard.native_handle());startupGuard.join();}control.close();JSValue result=call("__desktopDispose");JS_FreeValue(ctx,result);releasePanels();if(instanceMutex){ReleaseMutex(instanceMutex);CloseHandle(instanceMutex);instanceMutex=nullptr;}Gdiplus::GdiplusShutdown(token);
126
105
  std::sort(timings.begin(),timings.end());double sum=0;for(auto t:timings)sum+=t;if(!production||diagnostic){printf("{\"stage\":\"native-exit\",\"painted\":%d,\"renders\":%llu,\"meanMs\":%.3f,\"p95Ms\":%.3f,\"maxMs\":%.3f,\"failed\":%s}\n",painted,frames,timings.empty()?0:sum/timings.size(),timings.empty()?0:timings[(size_t)(timings.size()*.95)],timings.empty()?0:timings.back(),failed?"true":"false");fflush(stdout);}return failed?1:0;
127
106
  }
128
107
  }
@@ -0,0 +1,28 @@
1
+ #pragma once
2
+ #include <algorithm>
3
+ #include <cstdint>
4
+ #include <limits>
5
+ #include <stdexcept>
6
+ #include <vector>
7
+
8
+ // One final-mask crop and one layered-window upload per scene. Separate HWNDs
9
+ // cannot present a repartitioned frame atomically, even with identical pixels.
10
+ namespace PetPaint {
11
+ constexpr size_t MaxRegions=1;
12
+ struct Rect {
13
+ int left=std::numeric_limits<int>::max(),top=std::numeric_limits<int>::max();
14
+ int right=0,bottom=0;
15
+ bool empty() const {return left>=right||top>=bottom;}
16
+ int width() const {return empty()?0:right-left;}
17
+ int height() const {return empty()?0:bottom-top;}
18
+ void include(int x,int y){left=std::min(left,x);top=std::min(top,y);right=std::max(right,x+1);bottom=std::max(bottom,y+1);}
19
+ void include(const Rect& r){if(!r.empty()){left=std::min(left,r.left);top=std::min(top,r.top);right=std::max(right,r.right);bottom=std::max(bottom,r.bottom);}}
20
+ };
21
+ inline int rounded(int n){return (n+63)/64*64;}
22
+ inline uint64_t bytes(const Rect& r,int cell){return r.empty()?0:(uint64_t)rounded(r.width()*cell)*rounded(r.height()*cell*2)*4;}
23
+ inline std::vector<Rect> partition(const std::vector<uint8_t>& masks,int columns,int rows,int cell){
24
+ if(columns<1||rows<1||(uint64_t)columns*rows>125000||masks.size()!=(size_t)columns*rows||cell<6||cell>32)throw std::runtime_error("Invalid paint mask grid");
25
+ Rect all;for(int y=0;y<rows;y++)for(int x=0;x<columns;x++)if(masks[(size_t)y*columns+x])all.include(x,y);
26
+ return all.empty()?std::vector<Rect>{}:std::vector<Rect>{all};
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitefoundry/retrieviewer",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
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/",
@@ -197,7 +197,10 @@
197
197
  "integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs",
198
198
  "integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs",
199
199
  "integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs",
200
- "integrations/native-windows/application.manifest"
200
+ "integrations/native-windows/application.manifest",
201
+ "integrations/native-windows/paint-regions.hpp",
202
+ "integrations/native-windows/native-paint.hpp",
203
+ "SPARSE-PAINT.md"
201
204
  ],
202
205
  "scripts": {
203
206
  "bake:motion": "node tools/bake-pet-motion.mjs",