From 35355b17576e93b035a2a78ecd05771e98f068ee Mon Sep 17 00:00:00 2001 From: skal Date: Sat, 21 Mar 2026 10:50:02 +0100 Subject: feat(cnn_v3): HTML WebGPU tool (index.html + shaders.js + tester.js) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3-file tool, 939 lines total. Implements full U-Net+FiLM inference in the browser: Pack→Enc0→Enc1→Bottleneck→Dec1→Dec0 compute passes, layer visualisation (Feat/Enc0/Enc1/BN/Dec1/Output), FiLM MLP sliders, drag-drop weights + image/video, Save PNG, diff/blend view modes. HOW_TO_CNN.md §7 updated to reflect tool is implemented. Co-Authored-By: Claude Sonnet 4.6 --- cnn_v3/docs/HOW_TO_CNN.md | 145 +++++-------- cnn_v3/tools/index.html | 147 +++++++++++++ cnn_v3/tools/shaders.js | 252 ++++++++++++++++++++++ cnn_v3/tools/tester.js | 540 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 991 insertions(+), 93 deletions(-) create mode 100644 cnn_v3/tools/index.html create mode 100644 cnn_v3/tools/shaders.js create mode 100644 cnn_v3/tools/tester.js (limited to 'cnn_v3') diff --git a/cnn_v3/docs/HOW_TO_CNN.md b/cnn_v3/docs/HOW_TO_CNN.md index 8c41ab0..f325a38 100644 --- a/cnn_v3/docs/HOW_TO_CNN.md +++ b/cnn_v3/docs/HOW_TO_CNN.md @@ -646,120 +646,76 @@ If results drift after shader edits, verify these invariants match the Python re ## 7. HTML WebGPU Tool -### Current state +**Location:** `cnn_v3/tools/` — three files, no build step. -There is no dedicated CNN v3 HTML tool yet. -The CNN v2 tool (`cnn_v2/tools/cnn_v2_test/index.html`) is the reference pattern. +| File | Lines | Contents | +|------|-------|----------| +| `index.html` | 147 | HTML + CSS | +| `shaders.js` | 252 | WGSL shader constants, weight-offset constants | +| `tester.js` | 540 | `CNNv3Tester` class, event wiring | -### CNN v2 tool as reference +### Usage -The v2 tool is a single self-contained HTML file demonstrating: -- Inline WGSL shaders (no build step) -- Drag-and-drop `.bin` weight loading -- Image/video file input -- Intermediate layer visualisation -- View modes: CNN output / original / diff×10 -- Side panel with per-layer weight statistics - -A v3 tool follows the same pattern with a more complex texture chain. - -### What a CNN v3 HTML tool requires - -**WGSL shaders to inline** (resolve `#include "cnn_v3/common"` via JS string substitution): +```bash +# Requires HTTP server (WebGPU blocked on file://) +cd /path/to/demo +python3 -m http.server 8080 +# Open: http://localhost:8080/cnn_v3/tools/ +``` -```js -const common = `/* contents of cnn_v3_common.wgsl */`; -const enc0_src = enc0_template.replace('#include "cnn_v3/common"', common); +Or on macOS with Chrome: +```bash +open -a "Google Chrome" --args --allow-file-access-from-files +open cnn_v3/tools/index.html ``` -**Texture chain:** +### Workflow -| Texture | Format | Size | -|---------|--------|------| -| feat_tex0 (input) | rgba32uint | W × H | -| feat_tex1 (input) | rgba32uint | W × H | -| enc0_tex | rgba16float | W × H | -| enc1_tex | rgba32uint | W/2 × H/2 | -| bottleneck_tex | rgba32uint | W/4 × H/4 | -| dec1_tex | rgba16float | W/2 × H/2 | -| output_tex | rgba16float | W × H | - -`rgba32uint` textures cannot be sampled; use `textureLoad` — already done in the shaders. - -**Weight loading:** - -```js -const resp = await fetch('cnn_v3_weights.bin'); -const buf = await resp.arrayBuffer(); -const gpu_buf = device.createBuffer({ - size: buf.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST -}); -device.queue.writeBuffer(gpu_buf, 0, buf); -``` - -**FiLM MLP inference (JS-side):** - -```js -// Load cnn_v3_film_mlp.bin as Float32Array -const mlp = new Float32Array(await (await fetch('cnn_v3_film_mlp.bin')).arrayBuffer()); -const L0_W = mlp.subarray(0, 80); // (16×5) row-major -const L0_b = mlp.subarray(80, 96); -const L1_W = mlp.subarray(96, 736); // (40×16) row-major -const L1_b = mlp.subarray(736, 776); - -function mlp_forward(cond5) { - // h = relu(L0_W @ cond + L0_b) - const h = new Float32Array(16); - for (let o = 0; o < 16; o++) { - let s = L0_b[o]; - for (let i = 0; i < 5; i++) s += L0_W[o * 5 + i] * cond5[i]; - h[o] = Math.max(0, s); - } - // out = L1_W @ h + L1_b - const out = new Float32Array(40); - for (let o = 0; o < 40; o++) { - let s = L1_b[o]; - for (let i = 0; i < 16; i++) s += L1_W[o * 16 + i] * h[i]; - out[o] = s; - } - return out; // [γenc0×4, βenc0×4, γenc1×8, βenc1×8, γdec1×4, βdec1×4, γdec0×4, βdec0×4] -} -``` +1. **Drop `cnn_v3_weights.bin`** onto the left "weights" drop zone. +2. **Drop a PNG or video** onto the centre canvas → CNN runs immediately. +3. _(Optional)_ **Drop `cnn_v3_film_mlp.bin`** → FiLM sliders become active. +4. Adjust **beat_phase / beat_norm / audio_int / style_p0 / style_p1** sliders → reruns on change. +5. Click layer buttons (**Feat · Enc0 · Enc1 · BN · Dec1 · Output**) in the right panel to inspect activations. +6. **Save PNG** to export the current output. -The 40 outputs split into per-layer γ/β and uploaded to the 5 params uniform buffers -before each compute dispatch. +Keyboard: `[SPACE]` toggle original · `[D]` diff×10. -**Input feature assembly from a photo:** +### Input files -For simple (photo-only) mode, build `feat_tex0` and `feat_tex1` from the image data: -- `feat_tex0`: pack albedo RGB (f16×3), normal XY (128,128 neutral → 0.0 in oct), depth (0), depth_grad (0,0) as `pack2x16float` into rgba32uint -- `feat_tex1`: pack mat_id (0), prev.rgb (0,0,0), mip1.rgb, mip2.rgb, shadow (1.0), transp (0) as `pack4x8unorm` into rgba32uint +| File | Format | Notes | +|------|--------|-------| +| `cnn_v3_weights.bin` | raw u32 (no header) | 982 u32 = 1964 f16 = ~3.9 KB | +| `cnn_v3_film_mlp.bin` | raw f32 | 776 f32 = 3.1 KB; optional — identity FiLM used if absent | -See `cnn_v3/shaders/gbuf_pack.wgsl` for the exact packing layout (mirrors `GBufferEffect`). +Both produced by `export_cnn_v3_weights.py` (§3). -### Serving locally +### Texture chain + +| Texture | Format | Size | +|---------|--------|------| +| `feat_tex0` | rgba32uint | W × H (8 f16: albedo, normal, depth, depth_grad) | +| `feat_tex1` | rgba32uint | W × H (12 u8: mat_id, prev, mip1, mip2, shadow, transp) | +| `enc0_tex` | rgba16float | W × H | +| `enc1_tex` | rgba32uint | W/2 × H/2 (8 f16 packed) | +| `bn_tex` | rgba32uint | W/4 × H/4 | +| `dec1_tex` | rgba16float | W/2 × H/2 | +| `output_tex` | rgba16float | W × H → displayed on canvas | -Chrome requires a real HTTP server for WebGPU (not `file://`): +### Simple mode (photo input) -```bash -python3 -m http.server 8080 -# Open: http://localhost:8080/cnn_v3/tools/cnn_v3_test/index.html -``` +Albedo = image RGB, mip1/mip2 from GPU mipmaps, shadow = 1.0, transp = 1 − alpha, +all geometric channels (normal, depth, depth_grad, mat_id, prev) = 0. ### Browser requirements -- Chrome 113+ with WebGPU enabled (default on desktop) +- Chrome 113+ / Edge 113+ (WebGPU on by default) - Firefox Nightly with `dom.webgpu.enabled = true` -- Required features: check `device.features.has('shader-f16')` for f16 support; - fall back to f32 accumulation if absent ### Pitfalls -- `rgba32uint` requires `STORAGE` + `TEXTURE_BINDING` usage flags; missing either causes bind group creation failure -- WGSL `#include "cnn_v3/common"` must be resolved via JS string replace before passing to `device.createShaderModule()` -- Workgroup dispatch: `Math.ceil(W / 8)` × `Math.ceil(H / 8)` — same formula as C++ -- Cross-origin image loading requires CORS headers or same-origin hosting +- `rgba32uint` and `rgba16float` textures both need `STORAGE_BINDING | TEXTURE_BINDING` usage. +- Weight offsets are **f16 indices** (enc0=0, enc1=724, bn=1020, dec1=1092, dec0=1672). +- Uniform buffer layouts must match WGSL `Params` structs exactly (padding included). --- @@ -780,6 +736,9 @@ python3 -m http.server 8080 | `cnn_v3/src/gbuffer_effect.h/.cc` | GBufferEffect: rasterise + pack G-buffer feature textures | | `src/tests/gpu/test_cnn_v3_parity.cc` | Per-pixel parity test (WGSL vs. Python reference) | | `cnn_v3/docs/CNN_V3.md` | Full architecture spec (U-Net, FiLM, WGSL uniform layouts) | +| `cnn_v3/tools/index.html` | HTML tool — UI shell + CSS | +| `cnn_v3/tools/shaders.js` | HTML tool — inline WGSL shaders + weight-offset constants | +| `cnn_v3/tools/tester.js` | HTML tool — CNNv3Tester class, inference pipeline, layer viz | | `cnn_v2/tools/cnn_v2_test/index.html` | HTML tool reference pattern (v2) | --- diff --git a/cnn_v3/tools/index.html b/cnn_v3/tools/index.html new file mode 100644 index 0000000..eba532e --- /dev/null +++ b/cnn_v3/tools/index.html @@ -0,0 +1,147 @@ + + + + + + +CNN v3 Tool + + + +

CNN v3 Testing Tool

U-Net + FiLM · WebGPU
+ +
+
+ + + +
Drop cnn_v3_weights.bin
+
Drop cnn_v3_film_mlp.bin (optional)
+ +
+
Input Mode
+
+
+ + +
+ +
+
+ +
+
Weights Info
+

No weights loaded

+
+ +
+
FiLM Conditioning
+
+
No FiLM MLP — identity (γ=1, β=0)
+
+ 0.00 + + 0.00 + + 0.00 + + 0.00 + + 0.00 + +
+
+
+
+ +
+
+
+ + + +
+
+ + + 1.0 +
+ +
+ +
+ +
+
+
Layer Visualization
+
+

Load image + weights

+
+
+
+
+ +
+
+ Drop PNG/video on canvas · drop .bin weights on left + [SPACE] Original [D] Diff×10 +
+
+
+ + + + + diff --git a/cnn_v3/tools/shaders.js b/cnn_v3/tools/shaders.js new file mode 100644 index 0000000..c3e994d --- /dev/null +++ b/cnn_v3/tools/shaders.js @@ -0,0 +1,252 @@ +'use strict'; +// CNN v3 WGSL shaders — matches cnn_v3/shaders/*.wgsl exactly. +// Weight offsets (f16 index): enc0=0, enc1=724, bn=1020, dec1=1092, dec0=1672, total=1964 + +const ENC0_OFF=0, ENC1_OFF=724, BN_OFF=1020, DEC1_OFF=1092, DEC0_OFF=1672; +const TOTAL_F16=1964, TOTAL_U32=982; + +// Inlined helpers — prepended to shaders that need them. +const H = ` +fn get_w(base:u32,idx:u32)->f32{ + let i=base+idx; let v=unpack2x16float(weights[i>>1u]); + return select(v.y,v.x,(i&1u)==0u); +} +fn unpack8(tex:texture_2d,c:vec2i)->array{ + let t=textureLoad(tex,c,0); + let a=unpack2x16float(t.x);let b=unpack2x16float(t.y); + let d=unpack2x16float(t.z);let e=unpack2x16float(t.w); + return array(a.x,a.y,b.x,b.y,d.x,d.y,e.x,e.y); +}`; + +// Pack simple image (albedo+mips+alpha) into feat_tex0/1 +const PACK_SHADER=` +@group(0) @binding(0) var inp:texture_2d; +@group(0) @binding(1) var smp:sampler; +@group(0) @binding(2) var f0:texture_storage_2d; +@group(0) @binding(3) var f1:texture_storage_2d; +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) id:vec3u){ + let c=vec2i(id.xy); let d=vec2i(textureDimensions(inp)); + if(c.x>=d.x||c.y>=d.y){return;} + let uv=(vec2f(c)+.5)/vec2f(d); + let px=textureLoad(inp,c,0); + let alb=px.rgb; let tr=1.-px.a; + let m1=textureSampleLevel(inp,smp,uv,1.).rgb; + let m2=textureSampleLevel(inp,smp,uv,2.).rgb; + textureStore(f0,c,vec4u(pack2x16float(alb.rg),pack2x16float(vec2f(alb.b,0.)), + pack2x16float(vec2f(0.,0.)),pack2x16float(vec2f(0.,0.)))); + textureStore(f1,c,vec4u(pack4x8unorm(vec4f(0.,0.,0.,0.)), + pack4x8unorm(vec4f(m1.r,m1.g,m1.b,m2.r)), + pack4x8unorm(vec4f(m2.g,m2.b,1.,tr)),0u)); +}`; + +// Enc0: Conv(20→4, 3×3, zero-pad) + FiLM + ReLU → rgba16float +// Params (48 bytes): weight_offset u32 _pad×3 gamma vec4f beta vec4f +const ENC0_SHADER=H+` +struct P{wo:u32,_a:u32,_b:u32,_c:u32,g:vec4f,b:vec4f} +@group(0) @binding(0) var t0:texture_2d; +@group(0) @binding(1) var t1:texture_2d; +@group(0) @binding(2) var weights:array; +@group(0) @binding(3) var p:P; +@group(0) @binding(4) var out:texture_storage_2d; +fn feat(c:vec2i,d:vec2i)->array{ + if(c.x<0||c.y<0||c.x>=d.x||c.y>=d.y){return array(0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.);} + let a=unpack2x16float(textureLoad(t0,c,0).x); let b=unpack2x16float(textureLoad(t0,c,0).y); + let cc=unpack2x16float(textureLoad(t0,c,0).z);let dd=unpack2x16float(textureLoad(t0,c,0).w); + let e=unpack4x8unorm(textureLoad(t1,c,0).x); let f=unpack4x8unorm(textureLoad(t1,c,0).y); + let g=unpack4x8unorm(textureLoad(t1,c,0).z); + return array(a.x,a.y,b.x,b.y,cc.x,cc.y,dd.x,dd.y,e.x,e.y,e.z,e.w,f.x,f.y,f.z,f.w,g.x,g.y,g.z,g.w); +} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) id:vec3u){ + let c=vec2i(id.xy); let d=vec2i(textureDimensions(t0)); + if(c.x>=d.x||c.y>=d.y){return;} + const IN:u32=20u; const OUT:u32=4u; + var o:array; + for(var oc:u32=0u;oc; +@group(0) @binding(1) var weights:array; +@group(0) @binding(2) var p:P; +@group(0) @binding(3) var out:texture_storage_2d; +fn fg(o:u32)->f32{if(o<4u){return p.gl[o];}return p.gh[o-4u];} +fn fb(o:u32)->f32{if(o<4u){return p.bl[o];}return p.bh[o-4u];} +fn avg(hc:vec2i,fd:vec2i)->array{ + let hd=fd/2; if(hc.x<0||hc.y<0||hc.x>=hd.x||hc.y>=hd.y){return array(0.,0.,0.,0.);} + var s=vec4f(0.); + for(var y:i32=0;y<2;y++){for(var x:i32=0;x<2;x++){s+=textureLoad(e0,clamp(hc*2+vec2i(x,y),vec2i(0),fd-vec2i(1)),0);}} + let a=s*.25; return array(a.x,a.y,a.z,a.w); +} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) id:vec3u){ + let fd=vec2i(textureDimensions(e0)); let hd=fd/2; let c=vec2i(id.xy); + if(c.x>=hd.x||c.y>=hd.y){return;} + const IN:u32=4u; const OUT:u32=8u; + var o:array; + for(var oc:u32=0u;oc; +@group(0) @binding(1) var weights:array; +@group(0) @binding(2) var p:P; +@group(0) @binding(3) var out:texture_storage_2d; +fn avg(qc:vec2i,hd:vec2i)->array{ + let qd=hd/2; if(qc.x<0||qc.y<0||qc.x>=qd.x||qc.y>=qd.y){return array(0.,0.,0.,0.,0.,0.,0.,0.);} + var s:array; + for(var y:i32=0;y<2;y++){for(var x:i32=0;x<2;x++){ + let f=unpack8(e1,clamp(qc*2+vec2i(x,y),vec2i(0),hd-vec2i(1))); + for(var i:u32=0u;i<8u;i++){s[i]+=f[i];} + }} + for(var i:u32=0u;i<8u;i++){s[i]*=.25;} return s; +} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) id:vec3u){ + let hd=vec2i(textureDimensions(e1)); let qd=hd/2; let c=vec2i(id.xy); + if(c.x>=qd.x||c.y>=qd.y){return;} + let ft=avg(c,hd); var o:array; + for(var oc:u32=0u;oc<8u;oc++){ + var s=get_w(p.wo,64u+oc); + for(var i:u32=0u;i<8u;i++){s+=get_w(p.wo,oc*8u+i)*ft[i];} + o[oc]=max(0.,s); + } + textureStore(out,c,vec4u(pack2x16float(vec2f(o[0],o[1])),pack2x16float(vec2f(o[2],o[3])), + pack2x16float(vec2f(o[4],o[5])),pack2x16float(vec2f(o[6],o[7])))); +}`; + +// Dec1: NearestUp(bn)+cat(enc1_skip) → Conv(16→4,3×3) + FiLM + ReLU → rgba16float half-res +// Params (48 bytes): same layout as enc0 +const DEC1_SHADER=H+` +struct P{wo:u32,_a:u32,_b:u32,_c:u32,g:vec4f,b:vec4f} +@group(0) @binding(0) var bn:texture_2d; +@group(0) @binding(1) var e1:texture_2d; +@group(0) @binding(2) var weights:array; +@group(0) @binding(3) var p:P; +@group(0) @binding(4) var out:texture_storage_2d; +fn cat(hc:vec2i,hd:vec2i)->array{ + if(hc.x<0||hc.y<0||hc.x>=hd.x||hc.y>=hd.y){return array(0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.);} + let qd=hd/2; let b=unpack8(bn,clamp(hc/2,vec2i(0),qd-vec2i(1))); + let s=unpack8(e1,hc); + return array(b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7]); +} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) id:vec3u){ + let hd=vec2i(textureDimensions(e1)); let c=vec2i(id.xy); + if(c.x>=hd.x||c.y>=hd.y){return;} + const IN:u32=16u; const OUT:u32=4u; + var o:array; + for(var oc:u32=0u;oc; +@group(0) @binding(1) var e0:texture_2d; +@group(0) @binding(2) var weights:array; +@group(0) @binding(3) var p:P; +@group(0) @binding(4) var out:texture_storage_2d; +fn cat(c:vec2i,fd:vec2i)->array{ + if(c.x<0||c.y<0||c.x>=fd.x||c.y>=fd.y){return array(0.,0.,0.,0.,0.,0.,0.,0.);} + let hd=vec2i(textureDimensions(d1)); + let a=textureLoad(d1,clamp(c/2,vec2i(0),hd-vec2i(1)),0); + let b=textureLoad(e0,c,0); + return array(a.x,a.y,a.z,a.w,b.x,b.y,b.z,b.w); +} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) id:vec3u){ + let fd=vec2i(textureDimensions(e0)); let c=vec2i(id.xy); + if(c.x>=fd.x||c.y>=fd.y){return;} + const IN:u32=8u; const OUT:u32=4u; + var o:array; + for(var oc:u32=0u;oc; +@group(0) @binding(1) var itex:texture_2d; +@group(0) @binding(2) var pr:vec4f; // x=mode y=blend +@vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f{ + var p=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.)); + return vec4f(p[i],0.,1.); +} +@fragment fn fs(@builtin(position) pos:vec4f)->@location(0) vec4f{ + let c=vec2i(pos.xy); let m=u32(pr.x); + let orig=textureLoad(itex,c,0).rgb; let cnn=textureLoad(otex,c,0).rgb; + if(m==1u){return vec4f(orig,1.);} + if(m==2u){return vec4f(abs(cnn-orig)*10.,1.);} + return vec4f(mix(orig,cnn,pr.y),1.); +}`; + +// Viz f32: show one channel of rgba16float layer +const VIZ_F32=` +@group(0) @binding(0) var t:texture_2d; +@group(0) @binding(1) var ch:u32; +@vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f{ + var p=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.)); + return vec4f(p[i],0.,1.); +} +@fragment fn fs(@builtin(position) pos:vec4f)->@location(0) vec4f{ + let v=textureLoad(t,vec2i(pos.xy),0); var a=array(v.x,v.y,v.z,v.w); + let x=clamp(a[min(ch,3u)],0.,1.); return vec4f(x,x,x,1.); +}`; + +// Viz u32: show one f16 channel of rgba32uint layer (8 channels packed) +const VIZ_U32=` +@group(0) @binding(0) var t:texture_2d; +@group(0) @binding(1) var ch:u32; +@vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f{ + var p=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.)); + return vec4f(p[i],0.,1.); +} +@fragment fn fs(@builtin(position) pos:vec4f)->@location(0) vec4f{ + let t2=textureLoad(t,vec2i(pos.xy),0); + let a=unpack2x16float(t2.x);let b=unpack2x16float(t2.y); + let c=unpack2x16float(t2.z);let d=unpack2x16float(t2.w); + var v=array(a.x,a.y,b.x,b.y,c.x,c.y,d.x,d.y); + let x=clamp(v[min(ch,7u)],0.,1.); return vec4f(x,x,x,1.); +}`; diff --git a/cnn_v3/tools/tester.js b/cnn_v3/tools/tester.js new file mode 100644 index 0000000..aa765a1 --- /dev/null +++ b/cnn_v3/tools/tester.js @@ -0,0 +1,540 @@ +'use strict'; + +class CNNv3Tester { + constructor() { + this.canvas = document.getElementById('canvas'); + this.statusEl= document.getElementById('status'); + this.conEl = document.getElementById('con'); + this.video = document.getElementById('vid'); + this.weightsU32 = null; + this.weightsBuffer = null; + this.weightsGPU = null; + this.filmMlp = null; + this.image = null; + this.isVideo = false; + this.viewMode= 0; // 0=cnn 1=orig 2=diff + this.blend = 1.0; + this.layerTextures = {}; + this.lastResult = null; + this.isProcessing = false; + this.fps = 30; + this.pipelines = {}; // cached: pack enc0 enc1 bn dec1 dec0 disp vizF32 vizU32 mip + this.linearSampler = null; + this.init(); + } + + log(msg, type='info') { + const d = document.createElement('div'); + d.className = `cl ${type}`; + d.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + this.conEl.appendChild(d); + this.conEl.scrollTop = this.conEl.scrollHeight; + } + setStatus(msg, err=false) { + this.statusEl.textContent = msg; + this.statusEl.style.color = err ? '#f44' : '#4a9eff'; + } + + async init() { + if (!navigator.gpu) { this.setStatus('WebGPU not supported',true); return; } + try { + this.adapter = await navigator.gpu.requestAdapter(); + this.device = await this.adapter.requestDevice(); + this.context = this.canvas.getContext('webgpu'); + this.format = navigator.gpu.getPreferredCanvasFormat(); + this.linearSampler = this.device.createSampler({magFilter:'linear',minFilter:'linear',mipmapFilter:'linear'}); + this.log('WebGPU ready'); + } catch(e) { this.setStatus(`GPU error: ${e.message}`,true); } + } + + getDims() { + return this.isVideo + ? {w:this.video.videoWidth, h:this.video.videoHeight} + : {w:this.image.width, h:this.image.height}; + } + + setMode(m) { + document.getElementById('mSimple').classList.toggle('act', m==='simple'); + document.getElementById('mFull').classList.toggle('act', m==='full'); + document.getElementById('fullHelp').style.display = m==='full' ? 'block' : 'none'; + } + + // ── Weight parsing ─────────────────────────────────────────────────────── + + parseWeights(buf) { + const u32 = new Uint32Array(buf); + if (u32.length < TOTAL_U32) throw new Error(`Too small: ${u32.length} u32, need ${TOTAL_U32}`); + const layers = [ + {n:'enc0',off:ENC0_OFF,cnt:724},{n:'enc1',off:ENC1_OFF,cnt:296}, + {n:'bn', off:BN_OFF, cnt: 72},{n:'dec1',off:DEC1_OFF,cnt:580}, + {n:'dec0',off:DEC0_OFF,cnt:292}, + ]; + let html=`
Size: ${(buf.byteLength/1024).toFixed(1)} KB   Weights: ${TOTAL_F16} f16
+ `; + for (const l of layers) { + let mn=Infinity,mx=-Infinity; + for (let i=l.off;i>1]); + const v=(i&1)?b:a; if(vmx)mx=v; + } + html+=``; + } + html+='
LayerOffsetCountMinMax
${l.n}${l.off}${l.cnt}${mn.toFixed(3)}${mx.toFixed(3)}
'; + document.getElementById('wInfo').innerHTML = html; + return u32; + } + + parseFilm(buf) { + const f32=new Float32Array(buf); + if (f32.length < 776) throw new Error(`FiLM too small: ${f32.length}`); + let o=0; + const l0w=f32.slice(o,o+=80), l0b=f32.slice(o,o+=16); + const l1w=f32.slice(o,o+=640),l1b=f32.slice(o,o+=40); + this.log(`FiLM MLP: L0(16×5) L1(40×16), ${f32.length} f32`); + return {l0w,l0b,l1w,l1b}; + } + + filmFwd(cond) { + const {l0w,l0b,l1w,l1b}=this.filmMlp; + const h=new Float32Array(16); + for(let j=0;j<16;j++){let s=l0b[j];for(let i=0;i<5;i++)s+=l0w[j*5+i]*cond[i];h[j]=Math.max(0,s);} + const o=new Float32Array(40); + for(let j=0;j<40;j++){let s=l1b[j];for(let i=0;i<16;i++)s+=l1w[j*16+i]*h[i];o[j]=s;} + return o; + } + + filmParams() { + const I4=[1,1,1,1],Z4=[0,0,0,0],I8=[1,1,1,1,1,1,1,1],Z8=[0,0,0,0,0,0,0,0]; + if (!this.filmMlp) return {ge0:I4,be0:Z4,ge1:I8,be1:Z8,gd1:I4,bd1:Z4,gd0:I4,bd0:Z4}; + const v=document.getElementById; + const cond=[v('sBP').value,v('sBN').value,v('sAI').value,v('sP0').value,v('sP1').value].map(Number); + const f=this.filmFwd(cond); + return { + ge0:[...f.slice(0,4)], be0:[...f.slice(4,8)], + ge1:[...f.slice(8,16)],be1:[...f.slice(16,24)], + gd1:[...f.slice(24,28)],bd1:[...f.slice(28,32)], + gd0:[...f.slice(32,36)],bd0:[...f.slice(36,40)], + }; + } + + // ── Uniform buffers ────────────────────────────────────────────────────── + + // Params4 (48 bytes): wo u32 _pad×3 gamma vec4f beta vec4f + u4(wo,g,b){ + const buf=new ArrayBuffer(48),v=new DataView(buf); + v.setUint32(0,wo,true); + for(let i=0;i<4;i++)v.setFloat32(16+i*4,g[i],true); + for(let i=0;i<4;i++)v.setFloat32(32+i*4,b[i],true); + return buf; + } + // Params8 (80 bytes): wo u32 _pad×3 gl gh bl bh vec4f×4 + u8(wo,g,b){ + const buf=new ArrayBuffer(80),v=new DataView(buf); + v.setUint32(0,wo,true); + for(let i=0;i<4;i++)v.setFloat32(16+i*4,g[i],true); + for(let i=0;i<4;i++)v.setFloat32(32+i*4,g[i+4],true); + for(let i=0;i<4;i++)v.setFloat32(48+i*4,b[i],true); + for(let i=0;i<4;i++)v.setFloat32(64+i*4,b[i+4],true); + return buf; + } + // ParamsBN (16 bytes): wo u32 _pad×3 + ubn(wo){const buf=new ArrayBuffer(16);new DataView(buf).setUint32(0,wo,true);return buf;} + + // ── Pipeline cache ─────────────────────────────────────────────────────── + + computePL(code,entry) { + return this.device.createComputePipeline({layout:'auto', + compute:{module:this.device.createShaderModule({code}),entryPoint:entry}}); + } + renderPL(code,vs,fs) { + const m=this.device.createShaderModule({code}); + return this.device.createRenderPipeline({layout:'auto', + vertex:{module:m,entryPoint:vs}, + fragment:{module:m,entryPoint:fs,targets:[{format:this.format}]}}); + } + pl(key,fn){if(!this.pipelines[key])this.pipelines[key]=fn();return this.pipelines[key];} + + getPack() {return this.pl('pack', ()=>this.computePL(PACK_SHADER,'main'));} + getEnc0() {return this.pl('enc0', ()=>this.computePL(ENC0_SHADER,'main'));} + getEnc1() {return this.pl('enc1', ()=>this.computePL(ENC1_SHADER,'main'));} + getBN() {return this.pl('bn', ()=>this.computePL(BN_SHADER,'main'));} + getDec1() {return this.pl('dec1', ()=>this.computePL(DEC1_SHADER,'main'));} + getDec0() {return this.pl('dec0', ()=>this.computePL(DEC0_SHADER,'main'));} + getDisp() {return this.pl('disp', ()=>this.renderPL(DISP_SHADER,'vs','fs'));} + getVizF32() {return this.pl('vf32', ()=>this.renderPL(VIZ_F32,'vs','fs'));} + getVizU32() {return this.pl('vu32', ()=>this.renderPL(VIZ_U32,'vs','fs'));} + + getMip() { + return this.pl('mip', ()=>{ + const code=`@group(0) @binding(0) var src:texture_2d; + @vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f{ + var p=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.)); + return vec4f(p[i],0.,1.);} + @fragment fn fs(@builtin(position) pos:vec4f)->@location(0) vec4f{ + let c=vec2i(i32(pos.x)*2,i32(pos.y)*2); var s=vec4f(0.); + for(var y:i32=0;y<2;y++){for(var x:i32=0;x<2;x++){s+=textureLoad(src,c+vec2i(x,y),0);}} + return s*.25;}`; + const m=this.device.createShaderModule({code}); + return this.device.createRenderPipeline({layout:'auto', + vertex:{module:m,entryPoint:'vs'}, + fragment:{module:m,entryPoint:'fs',targets:[{format:'rgba8unorm'}]}}); + }); + } + + // ── Mipmap generation ──────────────────────────────────────────────────── + + generateMipmaps(tex,w,h) { + const enc=this.device.createCommandEncoder(); + const pl=this.getMip(); + for(let mip=1;mip<3;mip++){ + const mw=Math.max(1,w>>mip),mh=Math.max(1,h>>mip); + const bg=this.device.createBindGroup({layout:pl.getBindGroupLayout(0), + entries:[{binding:0,resource:tex.createView({baseMipLevel:mip-1,mipLevelCount:1})}]}); + const rp=enc.beginRenderPass({colorAttachments:[{ + view:tex.createView({baseMipLevel:mip,mipLevelCount:1}),loadOp:'clear',storeOp:'store'}]}); + rp.setPipeline(pl);rp.setBindGroup(0,bg);rp.setViewport(0,0,mw,mh,0,1);rp.draw(6);rp.end(); + } + this.device.queue.submit([enc.finish()]); + } + + // ── File loading ───────────────────────────────────────────────────────── + + async loadImage(file) { + this.image=await createImageBitmap(file); this.isVideo=false; + this.canvas.width=this.image.width; this.canvas.height=this.image.height; + this.setVideoCtrl(false); + this.log(`Image: ${file.name} (${this.image.width}×${this.image.height})`); + if(this.weightsU32){this.setStatus('Ready');this.run();} + else{this.setStatus('Image loaded — drop weights .bin');this.showOriginal();} + } + + async loadVideo(file) { + return new Promise((res,rej)=>{ + this.video.src=URL.createObjectURL(file); + this.video.onloadedmetadata=()=>{ + const w=this.video.videoWidth,h=this.video.videoHeight; + if(!w||!h){rej(new Error('Bad dims'));return;} + this.isVideo=true; this.canvas.width=w; this.canvas.height=h; + this.log(`Video: ${file.name} (${w}×${h})`); + this.setVideoCtrl(true); + this.video.onpause=()=>{document.getElementById('btnPP').textContent='Play';}; + this.video.onplay =()=>{document.getElementById('btnPP').textContent='Pause';this.playLoop();}; + const go=()=>{ + this.video.onseeked=()=>{if(!this.isProcessing)this.procFrame();}; + if(this.video.readyState>=2){this.weightsU32?this.procFrame().then(res):res(this.showOriginal());} + else setTimeout(go,50); + }; + this.video.onseeked=go; this.video.currentTime=0; + }; + this.video.onerror=()=>rej(new Error('Video load failed')); + }); + } + + setVideoCtrl(en){['btnPP','btnBk','btnFw'].forEach(id=>document.getElementById(id).disabled=!en);} + togglePlay(){this.video.paused?this.video.play():this.video.pause();} + stepFrame(d){if(!this.isVideo)return;this.video.pause(); + this.video.currentTime=Math.max(0,Math.min(this.video.duration,this.video.currentTime+d/this.fps));} + playLoop(){if(this.video.paused||this.video.ended)return; + if(!this.isProcessing)this.procFrame();requestAnimationFrame(()=>this.playLoop());} + async procFrame(){if(!this.weightsU32||this.isProcessing)return;this.isProcessing=true;await this.run();this.isProcessing=false;} + + async loadWeights(file) { + try { + const buf=await file.arrayBuffer(); + this.weightsU32=this.parseWeights(buf); this.weightsBuffer=buf; + if(this.weightsGPU){this.weightsGPU.destroy();this.weightsGPU=null;} + const el=document.getElementById('wDrop'); + el.textContent=`✓ ${file.name}`; el.classList.add('ok'); + this.log(`Weights: ${file.name}`); + if(this.image||this.isVideo){this.setStatus('Ready');this.run();} + else this.setStatus('Weights loaded — drop image/video'); + } catch(e){this.log(`Weights error: ${e.message}`,'err');document.getElementById('wDrop').classList.add('err');} + } + + async loadFilm(file) { + try { + const buf=await file.arrayBuffer(); + this.filmMlp=this.parseFilm(buf); + const el=document.getElementById('fDrop'); + el.textContent=`✓ ${file.name}`; el.classList.add('ok'); + document.getElementById('fSt').textContent='FiLM MLP loaded'; + document.getElementById('fSt').style.color='#28a745'; + if(this.image||this.isVideo)this.run(); + } catch(e){this.log(`FiLM error: ${e.message}`,'err');document.getElementById('fDrop').classList.add('err');} + } + + fslide(valId,el){document.getElementById(valId).textContent=parseFloat(el.value).toFixed(2);this.rerun();} + rerun(){if(this.image||this.isVideo)this.run();} + setBlend(v){this.blend=parseFloat(v);document.getElementById('blendV').textContent=this.blend.toFixed(2);if(this.lastResult)this.redisplay();} + + // ── Main run ───────────────────────────────────────────────────────────── + + async run() { + if(!this.weightsU32||!this.device)return; + const src=this.isVideo?this.video:this.image; + if(!src)return; + const t0=performance.now(); + const {w,h}=this.getDims(); + const W2=w>>1,H2=h>>1,W4=W2>>1,H4=H2>>1; + + this.context.configure({device:this.device,format:this.format}); + + // Input texture with mipmaps + if(this.inputTex)this.inputTex.destroy(); + this.inputTex=this.device.createTexture({size:[w,h],format:'rgba8unorm',mipLevelCount:3, + usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT}); + this.device.queue.copyExternalImageToTexture({source:src},{texture:this.inputTex,mipLevel:0},[w,h]); + this.generateMipmaps(this.inputTex,w,h); + + // Intermediate textures + const mk=(fmt,tw,th)=>this.device.createTexture({size:[tw,th],format:fmt, + usage:GPUTextureUsage.STORAGE_BINDING|GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_SRC}); + const f0=mk('rgba32uint',w,h),f1=mk('rgba32uint',w,h); + const e0=mk('rgba16float',w,h),e1=mk('rgba32uint',W2,H2); + const bn=mk('rgba32uint',W4,H4),d1=mk('rgba16float',W2,H2),ot=mk('rgba16float',w,h); + + // Weights GPU buffer (cached) + if(!this.weightsGPU){ + this.weightsGPU=this.device.createBuffer({size:this.weightsBuffer.byteLength, + usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}); + this.device.queue.writeBuffer(this.weightsGPU,0,this.weightsBuffer); + } + const wg=this.weightsGPU; + + const fp=this.filmParams(); + const wu=(data)=>{ + const b=this.device.createBuffer({size:data.byteLength,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}); + this.device.queue.writeBuffer(b,0,data); return b; + }; + const uE0=wu(this.u4(ENC0_OFF,fp.ge0,fp.be0)); + const uE1=wu(this.u8(ENC1_OFF,fp.ge1,fp.be1)); + const uBN=wu(this.ubn(BN_OFF)); + const uD1=wu(this.u4(DEC1_OFF,fp.gd1,fp.bd1)); + const uD0=wu(this.u4(DEC0_OFF,fp.gd0,fp.bd0)); + + const dispData=new ArrayBuffer(16); + const dispView=new DataView(dispData); + dispView.setFloat32(0,this.viewMode,true); dispView.setFloat32(4,this.blend,true); + const uDp=wu(dispData); + + const enc=this.device.createCommandEncoder(); + const bg=(pl,...entries)=>this.device.createBindGroup({layout:pl.getBindGroupLayout(0), + entries:entries.map((r,i)=>({binding:i,resource:r}))}); + const rv=(t)=>t.createView(); + const cp=(pl,bgr,wx,wy)=>{const p=enc.beginComputePass();p.setPipeline(pl);p.setBindGroup(0,bgr);p.dispatchWorkgroups(wx,wy);p.end();}; + const ceil8=(n)=>Math.ceil(n/8); + + cp(this.getPack(), bg(this.getPack(), rv(this.inputTex),this.linearSampler,rv(f0),rv(f1)), ceil8(w),ceil8(h)); + cp(this.getEnc0(), bg(this.getEnc0(), rv(f0),rv(f1),{buffer:wg},{buffer:uE0},rv(e0)), ceil8(w),ceil8(h)); + cp(this.getEnc1(), bg(this.getEnc1(), rv(e0),{buffer:wg},{buffer:uE1},rv(e1)), ceil8(W2),ceil8(H2)); + cp(this.getBN(), bg(this.getBN(), rv(e1),{buffer:wg},{buffer:uBN},rv(bn)), ceil8(W4),ceil8(H4)); + cp(this.getDec1(), bg(this.getDec1(), rv(bn),rv(e1),{buffer:wg},{buffer:uD1},rv(d1)), ceil8(W2),ceil8(H2)); + cp(this.getDec0(), bg(this.getDec0(), rv(d1),rv(e0),{buffer:wg},{buffer:uD0},rv(ot)), ceil8(w),ceil8(h)); + + const dbg=bg(this.getDisp(),rv(ot),rv(this.inputTex),{buffer:uDp}); + const rp=enc.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),loadOp:'clear',storeOp:'store'}]}); + rp.setPipeline(this.getDisp());rp.setBindGroup(0,dbg);rp.draw(6);rp.end(); + + this.device.queue.submit([enc.finish()]); + await this.device.queue.onSubmittedWorkDone(); + + const ms=(performance.now()-t0).toFixed(1); + this.setStatus(`${ms}ms · ${w}×${h} · ${['CNN','Orig','Diff'][this.viewMode]}`); + this.log(`Run: ${ms}ms`); + + // Cleanup uniforms + [uE0,uE1,uBN,uD1,uD0].forEach(b=>b.destroy()); + + // Store for layer viz & redisplay + this.destroyLayerTex(); + this.layerTextures={feat0:f0,feat1:f1,enc0:e0,enc1:e1,bn,dec1:d1,output:ot}; + this.lastResult={ot,itex:this.inputTex,uDp,dispPL:this.getDisp(),w,h}; + this.updateVizPanel(); + } + + destroyLayerTex(){for(const t of Object.values(this.layerTextures||{}))try{t.destroy();}catch(_){} this.layerTextures={};} + + redisplay() { + if(!this.lastResult||!this.device)return; + const {ot,itex,dispPL,w,h}=this.lastResult; + const dispData=new ArrayBuffer(16),dv=new DataView(dispData); + dv.setFloat32(0,this.viewMode,true);dv.setFloat32(4,this.blend,true); + const uDp=this.device.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}); + this.device.queue.writeBuffer(uDp,0,dispData); + const dbg=this.device.createBindGroup({layout:dispPL.getBindGroupLayout(0),entries:[ + {binding:0,resource:ot.createView()},{binding:1,resource:itex.createView()},{binding:2,resource:{buffer:uDp}}]}); + this.context.configure({device:this.device,format:this.format}); + const enc=this.device.createCommandEncoder(); + const rp=enc.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),loadOp:'clear',storeOp:'store'}]}); + rp.setPipeline(dispPL);rp.setBindGroup(0,dbg);rp.draw(6);rp.end(); + this.device.queue.submit([enc.finish()]); + uDp.destroy(); + this.setStatus(`${w}×${h} · ${['CNN','Orig','Diff'][this.viewMode]}`); + } + + showOriginal() { + const src=this.isVideo?this.video:this.image; + if(!src||!this.device)return; + const {w,h}=this.getDims(); + this.context.configure({device:this.device,format:this.format}); + const tex=this.device.createTexture({size:[w,h],format:'rgba8unorm', + usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT}); + this.device.queue.copyExternalImageToTexture({source:src},{texture:tex},[w,h]); + const code=`@group(0) @binding(0) var t:texture_2d; + @vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f{ + var p=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.)); + return vec4f(p[i],0.,1.);} + @fragment fn fs(@builtin(position) pos:vec4f)->@location(0) vec4f{return textureLoad(t,vec2i(pos.xy),0);}`; + const pl=this.device.createRenderPipeline({layout:'auto', + vertex:{module:this.device.createShaderModule({code}),entryPoint:'vs'}, + fragment:{module:this.device.createShaderModule({code}),entryPoint:'fs',targets:[{format:this.format}]}}); + const bg=this.device.createBindGroup({layout:pl.getBindGroupLayout(0),entries:[{binding:0,resource:tex.createView()}]}); + const enc=this.device.createCommandEncoder(); + const rp=enc.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),loadOp:'clear',storeOp:'store'}]}); + rp.setPipeline(pl);rp.setBindGroup(0,bg);rp.draw(6);rp.end(); + this.device.queue.submit([enc.finish()]); + tex.destroy(); + } + + // ── Layer visualization ────────────────────────────────────────────────── + + updateVizPanel() { + const DEFS=[ + {id:'feat0', lbl:'Feat', t:'u32',nch:8, ch:['alb.r','alb.g','alb.b','nrm.x','nrm.y','depth','dgx','dgy']}, + {id:'enc0', lbl:'Enc0', t:'f32',nch:4, ch:['c0','c1','c2','c3']}, + {id:'enc1', lbl:'Enc1', t:'u32',nch:8, ch:['c0','c1','c2','c3','c4','c5','c6','c7']}, + {id:'bn', lbl:'BN', t:'u32',nch:8, ch:['c0','c1','c2','c3','c4','c5','c6','c7']}, + {id:'dec1', lbl:'Dec1', t:'f32',nch:4, ch:['c0','c1','c2','c3']}, + {id:'output',lbl:'Output', t:'f32',nch:4, ch:['R','G','B','A']}, + ]; + this.vizDefs=DEFS; + const panel=document.getElementById('layerViz'); + let html='
'; + for(const d of DEFS) html+=``; + html+='
'; + panel.innerHTML=html; + this.vizLayer('output'); + } + + async vizLayer(id) { + const tex=this.layerTextures[id]; if(!tex||!this.device)return; + this.vizDefs.forEach(d=>document.getElementById(`vb_${d.id}`)?.classList.remove('act')); + document.getElementById(`vb_${id}`)?.classList.add('act'); + const def=this.vizDefs.find(d=>d.id===id); if(!def)return; + const grid=document.getElementById('chgrid'); grid.innerHTML=''; + for(let c=0;c{ + const s=(bits>>15)&1,e=(bits>>10)&0x1F,f=bits&0x3FF; + if(e===0)return 0;if(e===31)return s?0:1; + return Math.max(0,Math.min(1,(s?-1:1)*Math.pow(2,e-15)*(1+f/1024))); + }; + const px=new Uint8ClampedArray(w*h*4); + for(let y=0;ycvs.toBlob(r,'image/png')); + const a=document.createElement('a');a.href=URL.createObjectURL(blob); + a.download=`cnn_v3_${w}x${h}.png`;a.click();URL.revokeObjectURL(a.href); + this.log(`Saved: ${a.download}`); + } catch(e){this.log(`Save failed: ${e.message}`,'err');} + } + + f16pair(packed) { + const lo=packed&0xFFFF,hi=(packed>>16)&0xFFFF; + const f=(b)=>{const s=(b>>15)&1,e=(b>>10)&0x1F,m=b&0x3FF; + if(e===0)return(s?-1:1)*Math.pow(2,-14)*(m/1024); + if(e===31)return m?NaN:(s?-Infinity:Infinity); + return(s?-1:1)*Math.pow(2,e-15)*(1+m/1024);}; + return [f(lo),f(hi)]; + } +} + +// ── UI helpers ─────────────────────────────────────────────────────────────── + +function togglePanel(hdr) { + hdr.parentElement.classList.toggle('collapsed'); + const sp=hdr.querySelector('span'); + if(sp)sp.textContent=hdr.parentElement.classList.contains('collapsed')?'▶':'▼'; +} + +// ── Init & events ───────────────────────────────────────────────────────── + +const tester=new CNNv3Tester(); + +document.getElementById('wFile').addEventListener('change',e=>{if(e.target.files[0])tester.loadWeights(e.target.files[0]);}); +document.getElementById('fFile').addEventListener('change',e=>{if(e.target.files[0])tester.loadFilm(e.target.files[0]);}); + +const mainEl=document.getElementById('mainDrop'); +mainEl.addEventListener('dragover',e=>{e.preventDefault();mainEl.classList.add('dragover');}); +mainEl.addEventListener('dragleave',()=>mainEl.classList.remove('dragover')); +mainEl.addEventListener('drop',async e=>{ + e.preventDefault();mainEl.classList.remove('dragover'); + for(const f of e.dataTransfer.files){ + if(f.name.endsWith('.bin')){ + if(f.name.includes('film')||f.name.includes('mlp'))tester.loadFilm(f); + else tester.loadWeights(f); + } else if(f.type.startsWith('image/'))tester.loadImage(f); + else if(f.type.startsWith('video/'))tester.loadVideo(f); + } +}); + +['wDrop','fDrop'].forEach(id=>{ + const el=document.getElementById(id); + el.addEventListener('dragover',e=>{e.preventDefault();el.classList.add('dragover');}); + el.addEventListener('dragleave',()=>el.classList.remove('dragover')); + el.addEventListener('drop',async e=>{ + e.preventDefault();el.classList.remove('dragover'); + const f=e.dataTransfer.files[0];if(!f)return; + if(id==='fDrop')tester.loadFilm(f);else tester.loadWeights(f); + }); +}); + +document.addEventListener('keydown',e=>{ + if(e.target.tagName==='INPUT')return; + if(e.key===' '){e.preventDefault();tester.viewMode=tester.viewMode===1?0:1;tester.redisplay();} + else if(e.key==='d'||e.key==='D'){tester.viewMode=tester.viewMode===2?0:2;tester.redisplay();} +}); -- cgit v1.2.3