From 8f14bdd66cb002b2f89265b2a578ad93249089c9 Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 26 Mar 2026 07:03:01 +0100 Subject: feat(cnn_v3): upgrade architecture to enc_channels=[8,16] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double encoder capacity: enc0 4→8ch, enc1 8→16ch, bottleneck 16→16ch, dec1 32→8ch, dec0 16→4ch. Total weights 2476→7828 f16 (~15.3 KB). FiLM MLP output 40→72 params (L1: 16×40→16×72). 16-ch textures split into _lo/_hi rgba32uint pairs (enc1, bottleneck). enc0 and dec1 textures changed from rgba16float to rgba32uint (8ch). GBUF_RGBA32UINT node gains CopySrc for parity test readback. - WGSL shaders: all 5 passes rewritten for new channel counts - C++ CNNv3Effect: new weight offsets/sizes, 8ch uniform structs - Web tool (shaders.js + tester.js): matching texture formats and bindings - Parity test: readback_rgba32uint_8ch helper, updated vector counts - Training scripts: default enc_channels=[8,16], updated docstrings - Docs + architecture PNG regenerated handoff(Gemini): CNN v3 [8,16] upgrade complete. All code, tests, web tool, training scripts, and docs updated. Next: run training pass. --- cnn_v3/tools/shaders.js | 186 +++++++++++++++++++++++++++--------------------- cnn_v3/tools/tester.js | 88 +++++++++++++---------- cnn_v3/tools/weights.js | 4 +- 3 files changed, 159 insertions(+), 119 deletions(-) (limited to 'cnn_v3/tools') diff --git a/cnn_v3/tools/shaders.js b/cnn_v3/tools/shaders.js index 36f53c8..6f2176d 100644 --- a/cnn_v3/tools/shaders.js +++ b/cnn_v3/tools/shaders.js @@ -1,10 +1,10 @@ 'use strict'; // CNN v3 WGSL shaders — matches cnn_v3/shaders/*.wgsl exactly. -// Weight offsets (f16 index): enc0=0, enc1=724, bn=1020, dec1=1604, dec0=2184, total=2476 -// BN is now Conv(8→8, 3×3, dilation=2): 8*8*9+8=584 weights (was 72 for 1×1) +// Architecture: enc_channels=[8,16] +// Weight offsets (f16 index): enc0=0, enc1=1448, bn=2616, dec1=4936, dec0=7248, total=7828 -const ENC0_OFF=0, ENC1_OFF=724, BN_OFF=1020, DEC1_OFF=1604, DEC0_OFF=2184; -const TOTAL_F16=2476, TOTAL_U32=1238; +const ENC0_OFF=0, ENC1_OFF=1448, BN_OFF=2616, DEC1_OFF=4936, DEC0_OFF=7248; +const TOTAL_F16=7828, TOTAL_U32=3914; // Inlined helpers — prepended to shaders that need them. const H = ` @@ -41,20 +41,24 @@ fn main(@builtin(global_invocation_id) id:vec3u){ 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 +// Enc0: Conv(20→8, 3×3, zero-pad) + FiLM + ReLU → rgba32uint (pack2x16float, 8ch) +// Params (80 bytes): wo u32 _pad×3 gl gh bl bh (vec4f×4) const ENC0_SHADER=H+` -struct P{wo:u32,_a:u32,_b:u32,_c:u32,g:vec4f,b:vec4f} +struct P{wo:u32,_a:u32,_b:u32,_c:u32,gl:vec4f,gh:vec4f,bl:vec4f,bh: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; +@group(0) @binding(4) 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 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 t=textureLoad(t0,c,0); + let a=unpack2x16float(t.x);let b=unpack2x16float(t.y); + let cc=unpack2x16float(t.z);let dd=unpack2x16float(t.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); } @@ -62,41 +66,50 @@ fn feat(c:vec2i,d:vec2i)->array{ 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; + const IN:u32=20u; const OUT:u32=8u; + var o:array; for(var oc:u32=0u;oc; +struct P{wo:u32,_a:u32,_b:u32,_c:u32,g0:vec4f,g1:vec4f,g2:vec4f,g3:vec4f,b0:vec4f,b1:vec4f,b2:vec4f,b3:vec4f} +@group(0) @binding(0) var e0:texture_2d; @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); +@group(0) @binding(3) var olo:texture_storage_2d; +@group(0) @binding(4) var ohi:texture_storage_2d; +fn fg(o:u32)->f32{ + if(o<4u){return p.g0[o];}if(o<8u){return p.g1[o-4u];} + if(o<12u){return p.g2[o-8u];}return p.g3[o-12u];} +fn fb(o:u32)->f32{ + if(o<4u){return p.b0[o];}if(o<8u){return p.b1[o-4u];} + if(o<12u){return p.b2[o-8u];}return p.b3[o-12u];} +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.,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(e0,clamp(hc*2+vec2i(x,y),vec2i(0),fd-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 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; + const IN:u32=8u; const OUT:u32=16u; + 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; +@group(0) @binding(0) var elo:texture_2d; +@group(0) @binding(1) var ehi:texture_2d; +@group(0) @binding(2) var weights:array; +@group(0) @binding(3) var p:P; +@group(0) @binding(4) var olo:texture_storage_2d; +@group(0) @binding(5) var ohi: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.,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];} + let c2=clamp(qc*2+vec2i(x,y),vec2i(0),hd-vec2i(1)); + let lo=unpack8(elo,c2);let hi=unpack8(ehi,c2); + for(var i:u32=0u;i<8u;i++){s[i]+=lo[i];s[i+8u]+=hi[i];} }} - for(var i:u32=0u;i<8u;i++){s[i]*=.25;} return s; + for(var i:u32=0u;i<16u;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); + let hd=vec2i(textureDimensions(elo)); let qd=hd/2; let c=vec2i(id.xy); if(c.x>=qd.x||c.y>=qd.y){return;} - var o:array; - for(var oc:u32=0u;oc<8u;oc++){ - var s=get_w(p.wo,576u+oc); + var o:array; + for(var oc:u32=0u;oc<16u;oc++){ + var s=get_w(p.wo,2304u+oc); for(var ky:i32=-1;ky<=1;ky++){for(var kx:i32=-1;kx<=1;kx++){ let ft=avg(c+vec2i(kx,ky)*2,hd); let ki=u32(ky+1)*3u+u32(kx+1); - for(var i:u32=0u;i<8u;i++){s+=get_w(p.wo,oc*72u+i*9u+ki)*ft[i];} + for(var i:u32=0u;i<16u;i++){s+=get_w(p.wo,oc*144u+i*9u+ki)*ft[i];} }} o[oc]=max(0.,s); } - textureStore(out,c,vec4u(pack2x16float(vec2f(o[0],o[1])),pack2x16float(vec2f(o[2],o[3])), + textureStore(olo,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])))); + textureStore(ohi,c,vec4u(pack2x16float(vec2f(o[8],o[9])),pack2x16float(vec2f(o[10],o[11])), + pack2x16float(vec2f(o[12],o[13])),pack2x16float(vec2f(o[14],o[15])))); }`; -// Dec1: NearestUp(bn)+cat(enc1_skip) → Conv(16→4,3×3) + FiLM + ReLU → rgba16float half-res -// Params (48 bytes): same layout as enc0 +// Dec1: NearestUp(bn_lo/hi)+cat(enc1_lo/hi) → Conv(32→8,3×3) + FiLM + ReLU → rgba32uint half-res +// Params (80 bytes): wo u32 _pad×3 gl gh bl bh (vec4f×4) 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]); +struct P{wo:u32,_a:u32,_b:u32,_c:u32,gl:vec4f,gh:vec4f,bl:vec4f,bh:vec4f} +@group(0) @binding(0) var bnlo:texture_2d; +@group(0) @binding(1) var bnhi:texture_2d; +@group(0) @binding(2) var e1lo:texture_2d; +@group(0) @binding(3) var e1hi:texture_2d; +@group(0) @binding(4) var weights:array; +@group(0) @binding(5) var p:P; +@group(0) @binding(6) 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 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.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.);} + let qd=hd/2; let q=clamp(hc/2,vec2i(0),qd-vec2i(1)); + let b0=unpack8(bnlo,q);let b1=unpack8(bnhi,q); + let s0=unpack8(e1lo,hc);let s1=unpack8(e1hi,hc); + return array(b0[0],b0[1],b0[2],b0[3],b0[4],b0[5],b0[6],b0[7], + b1[0],b1[1],b1[2],b1[3],b1[4],b1[5],b1[6],b1[7], + s0[0],s0[1],s0[2],s0[3],s0[4],s0[5],s0[6],s0[7], + s1[0],s1[1],s1[2],s1[3],s1[4],s1[5],s1[6],s1[7]); } @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) id:vec3u){ - let hd=vec2i(textureDimensions(e1)); let c=vec2i(id.xy); + let hd=vec2i(textureDimensions(e1lo)); 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; + const IN:u32=32u; const OUT:u32=8u; + var o:array; for(var oc:u32=0u;oc; -@group(0) @binding(1) var e0:texture_2d; +@group(0) @binding(0) var d1:texture_2d; +@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.);} +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.,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); + let a=unpack8(d1,clamp(c/2,vec2i(0),hd-vec2i(1))); + let b=unpack8(e0,c); + return array(a[0],a[1],a[2],a[3],a[4],a[5],a[6],a[7], + b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7]); } @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; + const IN:u32=16u; const OUT:u32=4u; var o:array; for(var oc:u32=0u;oc; @@ -265,8 +293,6 @@ struct Vu{ch:u32,_p:u32,ox:i32,oy:i32} // Full G-buffer pack: assembles feat_tex0/feat_tex1 from individual G-buffer images. // Bindings: albedo(0) normal(1) depth(2) matid(3) shadow(4) transp(5) f0(6) f1(7) -// All source textures are rgba8unorm (browser-loaded images, R channel for depth/matid/shadow/transp). -// Uses textureLoad() only (no sampler needed). Matches gbuf_pack.wgsl packing exactly. const FULL_PACK_SHADER=` @group(0) @binding(0) var albedo: texture_2d; @group(0) @binding(1) var normal: texture_2d; @@ -295,7 +321,7 @@ fn main(@builtin(global_invocation_id) id:vec3u){ if(c.x>=d.x||c.y>=d.y){return;} let alb=textureLoad(albedo,c,0).rgb; let nrm=textureLoad(normal,c,0).rg; - let oct=nrm*2.-vec2f(1.); // [0,1] -> [-1,1] + let oct=nrm*2.-vec2f(1.); let dv=ld(c,d); let dzdx=(ld(c+vec2i(1,0),d)-ld(c-vec2i(1,0),d))*.5; let dzdy=(ld(c+vec2i(0,1),d)-ld(c-vec2i(0,1),d))*.5; diff --git a/cnn_v3/tools/tester.js b/cnn_v3/tools/tester.js index 69f358b..ebe888a 100644 --- a/cnn_v3/tools/tester.js +++ b/cnn_v3/tools/tester.js @@ -105,9 +105,9 @@ class CNNv3Tester { 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:584},{n:'dec1',off:DEC1_OFF,cnt:580}, - {n:'dec0',off:DEC0_OFF,cnt:292}, + {n:'enc0',off:ENC0_OFF,cnt:1448},{n:'enc1',off:ENC1_OFF,cnt:1168}, + {n:'bn', off:BN_OFF, cnt:2320},{n:'dec1',off:DEC1_OFF,cnt:2312}, + {n:'dec0',off:DEC0_OFF,cnt:580}, ]; let html=`
Size: ${(buf.byteLength/1024).toFixed(1)} KB   Weights: ${TOTAL_F16} f16
`; @@ -126,11 +126,11 @@ class CNNv3Tester { parseFilm(buf) { const f32=new Float32Array(buf); - if (f32.length < 776) throw new Error(`FiLM too small: ${f32.length}`); + if (f32.length < 1320) 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`); + const l0w=f32.slice(o,o+=80), l0b=f32.slice(o,o+=16); + const l1w=f32.slice(o,o+=1152),l1b=f32.slice(o,o+=72); + this.log(`FiLM MLP: L0(16×5) L1(72×16), ${f32.length} f32`); return {l0w,l0b,l1w,l1b}; } @@ -138,22 +138,24 @@ class CNNv3Tester { 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;} + const o=new Float32Array(72); + for(let j=0;j<72;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 I4=Array(4).fill(1),Z4=Array(4).fill(0); + const I8=Array(8).fill(1),Z8=Array(8).fill(0); + const I16=Array(16).fill(1),Z16=Array(16).fill(0); + if (!this.filmMlp) return {ge0:I8,be0:Z8,ge1:I16,be1:Z16,gd1:I8,bd1:Z8,gd0:I4,bd0:Z4}; const v=document.getElementById.bind(document); 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)], + ge0:[...f.slice(0,8)], be0:[...f.slice(8,16)], + ge1:[...f.slice(16,32)],be1:[...f.slice(32,48)], + gd1:[...f.slice(48,56)],bd1:[...f.slice(56,64)], + gd0:[...f.slice(64,68)],bd0:[...f.slice(68,72)], }; } @@ -177,6 +179,14 @@ class CNNv3Tester { for(let i=0;i<4;i++)v.setFloat32(64+i*4,b[i+4],true); return buf; } + // Params16 (144 bytes): wo u32 _pad×3 gamma[16] beta[16] vec4f×8 + u16(wo,g,b){ + const buf=new ArrayBuffer(144),v=new DataView(buf); + v.setUint32(0,wo,true); + for(let i=0;i<16;i++)v.setFloat32(16+i*4,g[i],true); + for(let i=0;i<16;i++)v.setFloat32(80+i*4,b[i],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;} @@ -330,8 +340,10 @@ class CNNv3Tester { 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); + const e0=mk('rgba32uint',w,h); // 8ch + const e1_lo=mk('rgba32uint',W2,H2),e1_hi=mk('rgba32uint',W2,H2); // 16ch split + const bn_lo=mk('rgba32uint',W4,H4),bn_hi=mk('rgba32uint',W4,H4); // 16ch split + const d1=mk('rgba32uint',W2,H2),ot=mk('rgba16float',w,h); // d1=8ch // Weights GPU buffer (cached) if(!this.weightsGPU){ @@ -346,11 +358,11 @@ class CNNv3Tester { 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 uE0=wu(this.u8( ENC0_OFF,fp.ge0,fp.be0)); + const uE1=wu(this.u16(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 uD1=wu(this.u8( 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); @@ -366,9 +378,9 @@ class CNNv3Tester { 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.getEnc1(), bg(this.getEnc1(), rv(e0),{buffer:wg},{buffer:uE1},rv(e1_lo),rv(e1_hi)), ceil8(W2),ceil8(H2)); + cp(this.getBN(), bg(this.getBN(), rv(e1_lo),rv(e1_hi),{buffer:wg},{buffer:uBN},rv(bn_lo),rv(bn_hi)), ceil8(W4),ceil8(H4)); + cp(this.getDec1(), bg(this.getDec1(), rv(bn_lo),rv(bn_hi),rv(e1_lo),rv(e1_hi),{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}); @@ -387,7 +399,7 @@ class CNNv3Tester { // Store for layer viz & redisplay this.destroyLayerTex(); - this.layerTextures={feat0:f0,feat1:f1,enc0:e0,enc1:e1,bn,dec1:d1,dec0:ot}; + this.layerTextures={feat0:f0,feat1:f1,enc0:e0,enc1:e1_lo,bn:bn_lo,dec1:d1,dec0:ot}; this.lastResult={ot,itex:this.inputTex,uDp,dispPL:this.getDisp(),w,h}; this.updateVizPanel(); this.refreshZoom(); @@ -442,10 +454,10 @@ class CNNv3Tester { 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:'enc0', lbl:'Enc0', t:'u32',nch:8, ch:['c0','c1','c2','c3','c4','c5','c6','c7']}, {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:'dec1', lbl:'Dec1', t:'u32',nch:8, ch:['c0','c1','c2','c3','c4','c5','c6','c7']}, {id:'dec0', lbl:'Dec0', t:'f32',nch:4, ch:['R','G','B','A']}, ]; this.vizDefs=DEFS; @@ -753,8 +765,10 @@ class CNNv3Tester { const mk = (fmt, tw, th) => this.device.createTexture({size:[tw,th], format:fmt, usage:GPUTextureUsage.STORAGE_BINDING|GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_SRC}); - 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); + const e0=mk('rgba32uint',w,h); // 8ch + const e1_lo=mk('rgba32uint',W2,H2),e1_hi=mk('rgba32uint',W2,H2); // 16ch split + const bn_lo=mk('rgba32uint',W4,H4),bn_hi=mk('rgba32uint',W4,H4); // 16ch split + const d1=mk('rgba32uint',W2,H2), ot=mk('rgba16float',w,h); // d1=8ch if (!this.weightsGPU) { this.weightsGPU = this.device.createBuffer({size:this.weightsBuffer.byteLength, @@ -767,11 +781,11 @@ class CNNv3Tester { 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 uE0=wu(this.u8( ENC0_OFF,fp.ge0,fp.be0)); + const uE1=wu(this.u16(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 uD1=wu(this.u8( DEC1_OFF,fp.gd1,fp.bd1)); + const uD0=wu(this.u4( DEC0_OFF,fp.gd0,fp.bd0)); const dispData=new ArrayBuffer(16); new DataView(dispData).setFloat32(4, this.blend, true); const uDp=wu(dispData); @@ -784,9 +798,9 @@ class CNNv3Tester { const ceil8 = (n) => Math.ceil(n/8); 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.getEnc1(), bg(this.getEnc1(), rv(e0),{buffer:wg},{buffer:uE1},rv(e1_lo),rv(e1_hi)), ceil8(W2), ceil8(H2)); + cp(this.getBN(), bg(this.getBN(), rv(e1_lo),rv(e1_hi),{buffer:wg},{buffer:uBN},rv(bn_lo),rv(bn_hi)), ceil8(W4), ceil8(H4)); + cp(this.getDec1(), bg(this.getDec1(), rv(bn_lo),rv(bn_hi),rv(e1_lo),rv(e1_hi),{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}); @@ -807,7 +821,7 @@ class CNNv3Tester { } this.destroyLayerTex(); - this.layerTextures = {feat0:f0, feat1:f1, enc0:e0, enc1:e1, bn, dec1:d1, output:ot}; + this.layerTextures = {feat0:f0, feat1:f1, enc0:e0, enc1:e1_lo, bn:bn_lo, dec1:d1, output:ot}; this.lastResult = {ot, itex:this.inputTex, uDp, dispPL:this.getDisp(), w, h}; this.updateVizPanel(); this.refreshZoom(); diff --git a/cnn_v3/tools/weights.js b/cnn_v3/tools/weights.js index dde1ed4..2c7b31b 100644 --- a/cnn_v3/tools/weights.js +++ b/cnn_v3/tools/weights.js @@ -1,4 +1,4 @@ 'use strict'; // Auto-generated by export_cnn_v3_weights.py --html — do not edit by hand. -const CNN_V3_WEIGHTS_B64='ias6I32xLDG5Masbdq4qIz+xrLQcshe3Ja1drluwb7crtHi38DZ8OL02eTh4Oe44HDTzN381TpwQqDCpCiP2pjipZywPL7CjNipXJc2qwiraJoetwijzphmqfCimJRgsX6tvqeuie6cRqoMpBhvSpbUiWSMFIlqrzCnHDiSiE6zJpYshR6udJMAmdRSkqHMVq6v0J68o6SL3p/mroia3I0uqEKobrMSdOqY5LAmgACqWqjch/SpcopUq6iJkJpEs6CumqH+lYqvLqjUs0ip5oKAkdqq9CTcnfamjJgSp36TmLZAsQLHTotyn27QPs1a0L7RwtOGwerXMswizdDeQOPw4BDj+OB85ADUuNhU4r7QGsAG0b7PsrP6ynrUosfuzALX4tNi3yrbNtDu2RbhRtSy4/TTgNrE1DDV2NyE2ijIMNqIyMibho5KvrSA1olmnQy31LbSm1KZPLHgpYqytLEKsuqVeHq+sWTWQObez6ze1PLI0krIhOdgsdaEKN6C2EzOBOzQ2WLePNx8wMrTRMiW4J7LMOBcvrrq/LkWtdq1vLIIgdKqAJSAvxa0SKXsoR6/QnXosIarzLJwjxa/VKmclhabVKL4lTSo7LNopdaspqzIp3Z4oqU2pNyxpqQ6fWKvCq3cmtqksmaEgF6hfoI2fc6rSJ0SoxxpYoRGfvijcqTqmEiNpLHKa6yDtKqUsgaj5IVsnopQupcSsNabjKqkq4SuGqjWIeavyItemJhvpK2WsI6snKwKruqijqY+sZzM/NsGzjDWZOTU0oKnJNmMtI7DBMSyx4C9JOZs0UbTvMl+nd7OMJYa1z6jONRQwy7d/EQir56pzMUqvhCzONFIj8rJVJZOvU60gLiuwMyxgNSwh9rPcLpiuDazUMWGs/6iENDsw1bSUKHCvsLFKL4UtSqloIxUw57DtoHEtYayELMEiLCDlqpymv6nIqhWqIq+9sfOwH7JjtLG0HrJAsLety7RKtbm0ibSysym0ebE1r4cdSzRJNYw0LzWVN6s3ZThgOOA4m6uKLSqsuCuOM1sucrK7pp+qPLF2J8+sIR2BM5uaH7ARLzGtn6eSK90npywwK1aoZKV6rC0qyxREq6asNCmZJVGo2is9qZ2VhSpLmYosHiZ0o3WWqCJ/qSAoXiY7Kk0owJ+QK+SoR6vnptEV3apbqVglNizUoiWigqZupOKdCayWKW8r1imMqvYiyyu6pOYr8imLKd4pGyyAK0ekOagyp1Mo36/ur8q0wa+Zsrmx567Er3e0FbXLtKO0Fa/1suazq611rHWupzVoNuY12ze3OLk32DgVOac5N7a1s5G0SrVpsZO0VLXJsvGxVLQ1tB20YbWGsTawL7JDsMSwtTXBN0E3AzhuOGM4GDcVOYM5hrGVLaSmV6DHMwowdLO9Lw2wtiHHp3ms1SUyKiccY6Z6qWciQhnhqO8o1CY9rNclpKwpKoYjlyDzo/WrwiQdpcCrXiqmqh4qgZ0TJvynaCzGnUCsTyWfLL+nqSpMHYkkOaTpmian9iiRmL0q4SWtJd4n9KiXqQKmrhVtKAusHZ/KKJGqISYXJxUhXCkEmKGsVSorpK0sDKxUpygmHCA+KWqquyfzqoqosabFqi2hoCsZp7or5ilzKtsmrxmCo2is0ycBJjUsvKyzrD8o9KkGq+aqtSz4qMYlcKTuKLOo4yClIAshSKzLJIKoDqCYpxwshqwSingh55sqkSQrgqa+JH4qQSVRKBId0JUYrDWdciwRIderTKW9LGasCSyuKpKnrKriqomXayzqJn6oMh5OIUwr4R1EKOSkaqbBqnUqoJ0mK9KfXiugH8ckgyyIqsKr8ic8ojklvCYcq86nKiaoKpeooqusoQ+s3Si6oQQfvaCgK88qviqtq0ShcBhLJxKstaXCqgEYTSrrnM2goK9ft+m5NCcrHicvOjDPoWMztSiasJ4lU7U/N0A2rbSlLO426i/psy60Qrgtti61kLDcsEmzRqfYNbMbVzc/sGYnTq6LrjIsIzDgrZAqQa+/sEcpE5jkLrCuya6uqGqkabBbIWunbyzBroyw9JkIMDkp7K7NryUoiyrIKC2wqCtvrUIo8q2ArdOtkCrdrgKuRLFXMGwtiC8+r78wSrCGsIwwRy7MMPUvKbCZqmWwzSkqIdEwOyWYKQCxX621LHmwDCFhK6UwERn4LFcrOS5SrWSnEa7qqMUmnC9uin+u1jBzN6stMjaAsf24abC+MJuxVbGZNIKrZDiBsM+xbzVOLZKzrzQtOsW3rqkiMUC8PKnlwZW9TDEMpLem4DBfrRwvQ61PLnKg7q4KtiAy0TTtqWg0jTFzMn01kjGFtX4vvjVZuW0qJTPVtNIwxzNzuNMlF6uWudsZgLaQuBW0qLrNJLEweDDdplgkAa5TrAgxbKa5MyoznTp4K7Ez3zhhqZMxHTT4Mvkl37LXr0KmILF+NrIsA6oCvMexUT3Pvw69sjjowffAyLEPsEgfMZOMqmkwB5iuptgv7K8KuKyxFaBZupe2P7G2u6a2ma6ELwc0lbEeL6I0LiwKLrM0+SY1O3Q5ITdVsJ+zSTN8vOe0iCZBI8cwTjFAKo0vYiyhrCOtvLB6rAgyfTb8tHoc3KuOrycy1TSSONswxrWINDOx2bfWLFavDrH8MsY3G7QppUM7JsAuOOs8o79nozyvW6+3LLcwZSLboPKrKCtbJH+ovKt6tV0q8rKWtFut2rgSNaozfrzPvIg0HTXvJLkvViTFL4ktfjDbNlsmIbUYKXc1ka/ROE23XLVmKlQxkat9MeK0MrAtN982nrgRMa43YjIXtUU276BsuD24Izj4OXMwQLSLLl0x8zI+uJ4xp6tctGg0RDBsNyM0VrpHuRO71DEmKaq8tadjIzy0lb0krDu6QTZxsBKudbkrtIc4GjiHORQ9Bjf3Nzk6PzVQOWk8o6gHIxsuXS0tMoczyTLoJoE1kDSdOC07MzO/N5w4XCihM+c4sDJBLls4EjFVrJ4y/jScMKsxc67nqHa2h7OcsvG3TbdbtpG47rNnNOA0aDBGNnQ1KjH9N1I3/LQ/uGS8brktur+6ji3mta6pir0Ruti9qLLMLla637i7tuS7PbESrfkf2TRVKBEsgTYppricvSxdqDkgOawrKqIoHqzmqOQpGStPKkqpIiQVodasTKhUKAgq9zEhJE6vRi1PKZUrfCyZLkk2Oi5BMewsW7FBsWk0JC+BrkouNq5VqZayjiqpLauwtTDnLtKzobX7IqOmkKfUKuUr+LaWsiuyHqupNLCt9KpBL6Om1zEUM+g2fLvgtvq8YblxuSy5I7gGtLK8UD0KOaA6jqwaLbo87zE+uZ6p5Tdnscu3TTZnMd80/y1Kp7q4HDFHMIWTSjKPLQgxR7ImtPC0DrPiNpMxz7mjtPazZLIvr2QuzTimLj23wjT1phOylzpkpU+04bsNuwc5n7kau52+p7T4uku5TbF0Phw+qzF5PPM70SkuP0M+pyYCKpcxFyBerTgzCK51sSg0VCi6KampeiwHIIYpZirjKU0jKqUjH/weJKAnI14rr6sapGgsHTPsM4Kw96J2NEC0NzYpNa223zYMN2s2I7TeMxStDyfENpMx6TBTsiuzDTEHrUYoJaXBrHirNi4PNQ46obXxuGOwSilyqPY4RLVetoe0dzDtsN21ArFet4+04z7PNBU30DsLuW67QDAuvCS8BbgtMfY277XitNkzaB9WLho5kzccs2+4ZTkBKyG2lzufMGW3Aj0OprcxQDi9tjukFTeYtW4vCLyhNni8lrqLPCa4OLwPNGq9IDUotpG7p6gmuZe8HDVNukq7xLzwpJK8kDDEO2M0Z7AGOJIx6r1DJHG0W7TgOcMwr7lRNQCy6yrjKqO3p6mYLs2zKCBsKl+xPqxPongsKKQuK3MsbiwUrdsrVCgqJv8s6yq5pBAiYyRnrGylCLUcsKm3K6mKrlG00zZhNxAxrTTtIjqwjikDsKI1lCyqs24h3ahiKTa3jbCRsO22ECQCr963R7LqNOqqva5jN1st9ykBOHgf+bLKmWiwFbM1pXOxCzi/q8M1ni+ssbAwwzTgMIk3kjwCPFY9MbFXo3C09bSksnK0mK5hsCesTbTVtaS07DTEpkYoCTydOlI89K0ltYeyJSymrHusIDhZNkA2/zBFMymwBLE/rBmxzLrVuTG79661ItEiNCxTsBs2cjqIMu81ma4VN4829rDzp6YwzLtavGO6AbtIMZ63C7u1LO+5w76EuQ693Le6Mlom3LT1NIqi8rJFNtKliSjXpxefyiIOKE8fnSj0KpusFSCWqCklBiQjKeMj5ayBnYYphiUiqFsw4i8CtHU0dqL2LEY1DSl4M5g2GbZosbUwyTFFMyy027SQri+u9pXuMTmvpbDYr0Gxf6iOJxyseaWKsM4jErSosJit4qsrLHmyEjA2I+MyBCkLOJI0PzNYMT41OTRSMOwx0jUIo80wcTFzpDIsAzMFtm20ALNms8mrIalPtDexMLKCNqguODZENfQukzQ7NKMJwi7CKzyzD7bWNKUrd6SGN641bzTyMd6xjRwYrM6z67P+MUKchZvgJ8+2C6pHsh+48a8PrWm1IbRdsD2wuK/Nr8eva7J1rae2t7ObLr4dca47q6Wls6b5rGoqOS68r8mxW7boIPWvwbLToXCuwbDcNXk0MTOtMwgr8ShONF8x5TF5tp+uTLZQtYevpLRutIeW+65GpysyOzY1tZenHiiZt0m1CrUkr/8uYZIMMDU1/jKrtIUlmSsnnDw21yp6L0c44DD0L5k1MTNBMJow5jH/MbYyPC96LRI2XjUyq28mvRpBrL6l2a4GJRWWS65srz2yGraAKqSyh7F8qmGhIbLmNTo0BzPWMjkurShmNN8wpzFbtn2uL7ZOtcGu1bSltEWewK/4rI0ycDaYtB2qkZ0zt4m1b7RFsykvCCxEMQw2NDPFtGoxvq7BJas2iR4iL8k30zEJL6k14jMbLnwvDi0JLaIvHy/6KuA0bTRFI0gtqyxloqitLRjaKbYk8izCNeo1LzQyOEE3NjcMNtU35DXXLwAz4jCJOAY6njTkOEQ43zA1Ns43ITfhN8A3BDjrNGs2zTUBM7w1mTT8Nh44yTc7Nl03TDcNNc4zdTXQMgU0KjO8NIUyZTQ4OXU4TzjcOPM3xDjLOEQ5wThuMhwphTHFJO4xQDNVL0kxOy8wLzgiIas2LlEvay2YHyittiW9sto0jjFhOw=='; -const CNN_V3_FILM_MLP_B64='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'; +const CNN_V3_WEIGHTS_B64='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'; +const CNN_V3_FILM_MLP_B64='hvXqvX4wsL37Zt2+Ium8vv4fqz7JHEc+yyEiPv+RiT5u+UK+Pv7RvY3EWD6BBpM+W10mvZ1agL5uvRg+ofWtPsNorT18tZw+coVIvoQMpL69pmy8AN/PvTbaAT4sGey9t/xuviGtlr56gVE+u+WrPmKfAD6uL46+f/sbvuU0bb5dBuK9487nvkOmgL0PsUG+BvcGPigbpD10eH4+kVu7O+s+3D7qE4k99gKAPjjW/D7DcQA9+Cenvr+rzr1jM9g+uH6cvgc8CD3sida9KaeTPo6yeD1neKi+XNk5vhKzmb3ptg4+xLvbPn0XWb1qk4a+bW3JPuhz/T6aAck+o+Svvq94PL4UGk0+DWd5PoqmbD4XMtk91T/bPnTZgz69sb2+08+mvYPKqL7Je5Q+5jEFvm2Lrr624eO+8l+GvvGyvT4MF2a+jDN0vgllDD+k50i+as2xOhYbGb50yRc+Ps6evh1Zqb6I5qa+aVNdvdaq3L5ppFy++4UaPzJb3b6kqnW9aYMavufZxj3WrJy99KMDPkSjKz4mESy+h81oPcCyvr1v5zC+CfzdvbmSGD0bdJS9HrN8vrfBVb4k/m89tzXcvfRMmr0RhHy+PlrZvpqpOL6K+Pg8diGwvhCAAr67pmc+osXavRsQYz6aXq+8otKTvoIvm7707fS+zp1pPYbINT4pbHG+9T04vvsger5QiRc+JzI9vXoKYr7/tEY+wHSBPc9Md77xpoq9BkOLvrBFPz4Y1568CrSUvr2bZD6WCMY8hPn4vWhFFT4UL/M99BDxveaDKb7UIdA9npchvjDCBT2MgiI+QqJGPq7VVb7wkkq+yO4PPTC4gD0kfHI+RDq+PVmaeL4r6Cw+XQoxPoPc9z0iOyK+Nj5tvtzsST59Sow+R3CIuxZ3MD2Qx8c9ByoFvlvNRr7+GWy+JXYZPhm80rwMFQm+MJFuPv2Txr0ZF0m+EAr/vMnGjz4s5cw9ZIVevgLeaT2N5Dq+4P5EPrAZhz1xBL49UO5PPnziXz67Seo9btIjvgLAcj27Oic+XTgmPSIfm70iHgE+M/+rPEwEK72FU6a7/NdFvjxj+721zLI91qUzvuRDv706VMq92/9QvvTUmT1ELna+4NoCvARb9b0gsG2+fCitvQDTILxwkS69wBOMPF6qdT5eURu+gITfu0J9OD5gJma+7qoTvkCAJj7wiXM+9gxuPo0Epb29d3a+oBmUPCDkRj4nc0s+mDv5PTXNT75gycs85UrOvSSyeD59O4+9jgq8PRiuHb36wGa+XetMvhKxBL0hGwS+gWkXvgtMQD7IZD2+/ABdPY80ET7Un/w9V+04PmEKcr6Ap3W+z9kpPR+Ft70gpow9qqgyPq7Kfr6DQ0W+RnGIvgzIeL5KvBc+iMhVvhoECb0rniy+nJEPPrbXsj1ArwU+X/xcPv8JrLwnAWK+z91aPqpTgz2EoHO9fDLfvcFdwT13fM29fc9FvmtBTD6XcTk+sgCFvEY/NT2qrCg+9TQ6vG81F76+hGI+C83vPQrLLz7i2Bs9AKEbPjuvzT1PEV4+WVGNPrEl8Dxyw/M9ZCg9vvN/fD7t1xg+MGe+PSCzMT7iHs09mXeqvWN80D5o4fA9eufHPeF4vj0QwbG85ip5vsPb2r10ems9aY4MvooXv7zXt1Q+FIqmvVgpzbxPxJI7gySEujCCir4Z/jG+VlfkPSLA7T3OLk6+mYvTvbhqBL6k+Q8+N60ovoEwtj2l1I4+FVbYPVQ/lzs8/ei9c8B+PfTWUz6jEII9GPpJPWCEhr17GIk9IWBZPTo4+D21lg4+P53gPdbiTL6yllQ9iUyUPe4dWT6J+zI+0PSFPOtLhz65mkG+yghLPJ4biT7Y7cc9jsRPvvK/Aj6ketY9IvV7PnXdI75mN9I9HtwtvvadrL0w/Ze8D8U2PiRQYz43flm+gMBuPpW+tb3Q03m++D5wPtA6+jyekYs99t00PiWf1r27YY4+Ra4VPgUcw71via68gXlgPpOzrrsxXgU++2OCvE+YJL6PgDg+TnSAPg3DbT7YY0A+zXMovQ7pVL4g89w9bicnPvLgAL5VTWU+hJQvvtpVJ74KPXU+7knZPQEv4z2L9XY9AqepPb682D2uyzy+Y7gmvvYjOT61vzG+c4C+PfIDpbwHdR2+nQ76PWNqZ70F1Z28Hct7Pi9gVj4XXl6+cHuhPc6vhz580k69gZXsPXp/EL3eyA++DhMqPoORX72tShO+qrYGvlfVFz62qWK+jTb2vTIli71RpV++foCsvS9+rL2pZg++1oWqPa7VIj5NRky+gpEkvsilLT4zINe9+rPVPeZ6ET4drjA+QZZCO43RQb5TSAC+OH4IPsT6Tz4CFxa+DT7fvY67eD6/6/s9RWIBPWFWNr7L6Jq91mM4Pkla/D1mx089V1LDPUfQH74qJzc9UjpvPmGAIT4JQk6+Nzn6PXCsqb1CRqa9XH64PUVK/L3Y3UO9Cr6mvWhBjz6bCvW8lCdAPQ3fTT6IqPO9wE4FPq1zBz5fbAk8oWBdvli/Hj58uri9Gh2YPuP2ab73ky4+n2EXPhlQAL5p5II94sYuvijgz70D4UK+3WzZPaJGHL5BbHU+wXksvuviaT4mKhE+3ZQrvtDFgD5snXy+uQNYvvoNWj6/iio+XLiVvFtbDL7DMx4+1O1NvgsPET5HiKQ9dwsLPsVzTD7cb+o9u0lhPRiBAr66BF0+teAoPf7ycb6U9r+9dawkPmiK6rzqEGg+SB4Cvk4qyT2AbH+9AE2oOlNeZz4s9mW+r3jyvfisyb0Yx8c8kAeovWzmeL4DHz8+giZ+vreLPLvgzV++a/tOPkwvoj06nCC+nK+cvcSmHr5Jh4i+hdAlu8blmD1KdVi+Kv4APnU5VT2/WTi+cqZ1PaviW74AaFM+xm9TPR82U7wkk0y+dsYBvk0QI715o0E+OG4KvnikZD0bik2+fo30vS859T37roU+O2CmPRqliT6Q2vW9I2NiPZvFbj2tG2U+nSjTumtvTb7pKT69eHgAvB716T0H04w986opvn2JGr53/AW+XblJvpQ0IL4xNnk8WJp5Ph6CgD2Z+xa9KdO/PPhu0T2O4Xs+VqsXPkg1AL1CIoi9rPkxPookhj3wfDY+Fp9AvnCiA76wUaO9pHthPtmrPr5XSHI+/dyPvSIoFL5QWZK9fJO9PW6tnL0CC029VbQMvuIhqT3Dj0u9JtdSvrqUUDx0U9497GsSvDDBzb10LzU9r0kevtjuJL5DaAk8n5VsvRLtQr6M/im+46MOPoan5j23jW6+eDozPtU4kbsu5w0+9vNyvhf6SD77aB8+coxAvvg8qD0VYGK+AlVJPVL3j75/YR6+Y1UNPn6zFT4NTaA9AgVxPrHw6zziMho+pfxvPYZecb7bxME9x1oYPuP4ET5KFmK+R0bJvfrmUz6bXjO9WrUPvuvk7D1Tfja+hBPRvbqeGT0kAA0+mqs4vrzlfz0HeJa+DskwPrHggL42J3++ibNXPrWNzDuXjQk+Q7pOPvAulj3Vj8A9dnVVPN74pbsBmgS7am0Tvos9CL6td4++kiGGvpt/er3+ZxI+/TN2vlRPCz6Z46Y9HinMveD3oD15ZA0+SS1EPopWJb4RC/K7xI4pvruITD5JSEK+1/iqvVDLpj1nQDW9hWPwuSD0ej7+UWe+/Vb+PeB0Fr65tKy9w8nrPTA8B77S15E9JvNQPtXOib2/1VU+G4doPrOvQ76n6j8+l6xBvoP7RL0aF429drjfvadXXr6fdDU+sxt4vgCjU76ep0C+zwMRPmBeNrxzOhS+VnCvPMHlBL5yTI69gPe/PYGWrz3Upb+9jtIrvsOFxz1ylVQ+G9A3vmoi7b2eUU2+MX+VPbO24b1xFao9QW4Lu8x0hb1Bq1Y97pvWPUZZCz4LalU9GysovloRrb6aF9O9sl4QvqDpKT02yPu7EXV2PrFtLj6eKA6+7bcOPpwiBL4T4gS8cs53vTHZED4khZO9fuwwPqpZDr4SsAS9ClxZPnqEYD3xfTo8O3nvvTBrjL04P7E+FpzwvTHNbz4JjJG8da2RvZt1Ob4Atbw+X2tHPiLV3r1pZwU+Fp6pvqkuxb4rbWC+EYjXvejynjszfje+LUucPclbBb4='; -- cgit v1.2.3
LayerOffsetCountMinMax