import {MODES,GROUPS,percentile as pct,compare,qualityGate,createExecution,summarizeProfile} from './execution.js'; const $=id=>document.getElementById(id),REPO='RabbitDaisuke/rtmw3d-webgpu-onnx'; const video=$('video'),crop=$('crop'),ctx=crop.getContext('2d',{willReadFrequently:true}); let catalog,revision,base,ort,stopped=false,results=[],videoURL,runContext,preparedInputs; const runtimes=new Map(); const status=s=>$('status').textContent=s; async function hash(bytes){return [...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(x=>x.toString(16).padStart(2,'0')).join('');} async function get(url){const r=await fetch(url,{credentials:'omit',referrerPolicy:'no-referrer'});if(!r.ok)throw Error(`${r.status}: ${url}`);return r;} async function bytes(file,sha){ const source=base+file,key=new URL('/__rtmw3d-cache__/sha256/'+sha,location.origin).href,cache=await caches.open('rtmw3d-comparison-v1');let r=await cache.match(key); if(r){const b=await r.arrayBuffer();if(await hash(b)===sha)return b;await cache.delete(key);} for(const old of await cache.keys()){if(!old.url.endsWith('/'+file))continue;const cached=await cache.match(old);if(!cached)continue;const b=await cached.arrayBuffer();if(await hash(b)!==sha)continue;try{await cache.put(key,new Response(b));await cache.delete(old);}catch{}return b;} status(`取得・照合中: ${file}`);r=await get(source);const b=await r.arrayBuffer();if(await hash(b)!==sha)throw Error(`SHA-256不一致: ${file}`); try{await cache.put(key,new Response(b));}catch{status('キャッシュ容量不足。取得済みモデルで計測を続けます。');}return b; } async function loadRuntime(flavour='patched'){ if(runtimes.has(flavour))return runtimes.get(flavour);if(!navigator.gpu)throw Error('WebGPU対応のPCブラウザで開いてください'); const adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)throw Error('WebGPUアダプターを取得できません'); const manifest=catalog.runtime,entry=manifest.files.find(f=>f.path===(flavour==='original'?manifest.originalEntrypoint:manifest.entrypoint)),glue=manifest.files.find(f=>f.path.endsWith('.jsep.mjs')); if(!entry)throw Error('このHFリビジョンには未パッチJSEPランタイムがありません'); const js=URL.createObjectURL(new Blob([await bytes(entry.path,entry.sha256)],{type:'text/javascript'})); const module=URL.createObjectURL(new Blob([await bytes(glue.path,glue.sha256)],{type:'text/javascript'})); ort=await import(js);ort.env.wasm.numThreads=1;ort.env.wasm.proxy=false; ort.env.wasm.wasmPaths={mjs:module,wasm:base+'runtime/ort-wasm-simd-threaded.jsep.wasm'}; ort.env.webgpu.powerPreference='high-performance';ort.env.webgpu.profiling={mode:'off',ondata:()=>{}}; runtimes.set(flavour,ort);return ort; } async function seek(t){if(Math.abs(video.currentTime-t)<.001&&video.readyState>=2)return;await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Error('動画シークがタイムアウトしました')),15000);video.addEventListener('seeked',()=>{clearTimeout(timeout);resolve();},{once:true});video.currentTime=t;});} async function sampleInputs(){ if(!video.videoWidth)throw Error('ローカル動画を選択してください');video.pause(); const vw=video.videoWidth,vh=video.videoHeight,x=Number($('x').value)/100*vw,y=Number($('y').value)/100*vh; let w=Number($('w').value)/100*vw,h=Number($('h').value)/100*vh; if(w<=0||h<=0)throw Error('ROIサイズを確認してください');const cx=x+w/2,cy=y+h/2; if(w/h>.75)h=w/.75;else w=h*.75; const inputs=[],mean=[123.675,116.28,103.53],std=[58.395,57.12,57.375]; for(const f of[.2,.5,.8]){await seek(Math.min(video.duration-.01,video.duration*f));ctx.fillStyle='black';ctx.fillRect(0,0,288,384);ctx.save();ctx.setTransform(288/w,0,0,384/h,-(cx-w/2)*288/w,-(cy-h/2)*384/h);ctx.drawImage(video,0,0);ctx.restore(); const pixels=ctx.getImageData(0,0,288,384).data,n=288*384,data=new Float32Array(n*3);for(let i=0;ip?.observations?`${p.xyP95_px.toFixed(2)}px / ${(p.zP95_m*1000).toFixed(1)}mm`:'観測不足'; const verdict=r.error||(r.reference?'基準':r.qualityPassed===true?'差分基準内':r.qualityPassed===false?'差分基準外':'計測中'); for(const value of[r.id,r.executionLabel,(r.sizeBytes/1e6).toFixed(1),metrics?`${metrics.p50Ms.toFixed(2)} / ${metrics.p95Ms.toFixed(2)}`:'—',metrics?metrics.aggregateFPS.toFixed(1):'—',metrics?metrics.perInputFPS.toFixed(1):'—',part(r.body),part(r.ankles),part(r.feet),verdict]){const td=document.createElement('td');td.textContent=value;tr.append(td);}if(r.error||r.qualityPassed===false)tr.className='bad';$('results').append(tr); } } async function profile(runtime,data,mode,inputs){ const events=[];let runner; runtime.env.webgpu.profiling={mode:'default',ondata:e=>events.push(e)}; try{ runner=await createExecution(runtime,data,mode); for(let i=0;i<3;i++)await runner.run(inputs[i%inputs.length]); await runner.device.queue.onSubmittedWorkDone();await new Promise(resolve=>setTimeout(resolve,0));events.length=0; await runner.run(inputs[0]);await runner.device.queue.onSubmittedWorkDone();await new Promise(resolve=>setTimeout(resolve,0)); return summarizeProfile(events); }finally{if(runner)await runner.release();runtime.env.webgpu.profiling={mode:'off',ondata:()=>{}};} } async function measure(){ stopped=false;results=[];render();$('start').disabled=true;$('stop').disabled=false;$('download').disabled=true;$('native-input').disabled=true; try{ const selected=[...document.querySelectorAll('#models input:checked')].map(x=>x.value),set=new Set(selected); for(const id of selected){const family=catalog.variants.find(v=>v.id===id).model;set.add(family+'-fp32');} const variants=catalog.variants.filter(v=>set.has(v.id)).toSorted((a,b)=>(a.policy==='fp32'?0:1)-(b.policy==='fp32'?0:1)||a.id.localeCompare(b.id)); const requestedModes=[...document.querySelectorAll('#modes input:checked')].map(x=>x.value); if(!selected.length||!requestedModes.length)throw Error('モデルと実行経路を選択してください'); const modes=MODES.filter(m=>m.id==='standard'||requestedModes.includes(m.id)); const runtime=await loadRuntime(),inputs=await sampleInputs(),reference=new Map(),views=Number($('views').value),iterations=Math.max(12,Math.min(1000,Number($('iterations').value)||200)); preparedInputs=inputs;$('native-input').disabled=false; runContext={source:{width:video.videoWidth,height:video.videoHeight,duration:video.duration},inputShape:[1,3,384,288],sampleFractions:[.2,.5,.8],roiPercent:Object.fromEntries(['x','y','w','h'].map(k=>[k,Number($(k).value)])),views,iterations,warmupRuns:5}; for(const variant of variants){ if(stopped)break;const data=await bytes(variant.file,variant.sha256); const routes=modes.filter(mode=>requestedModes.includes(mode.id)||(variant.policy==='fp32'&&mode.id==='standard')).map(mode=>({mode,flavour:'patched'})); if($('original').checked&&variant.policy==='fp32')routes.push({mode:MODES[0],flavour:'original'}); for(const {mode,flavour} of routes){ if(stopped)break; const record={id:variant.id,modelSha256:variant.sha256,sizeBytes:variant.sizeBytes,views,requestedBackend:'webgpu',executionMode:mode.id,executionLabel:mode.label+(flavour==='original'?' / 未パッチJSEP':''),preferredLayout:mode.layout,graphCaptureRequested:mode.capture,cpuFallbackDisabled:mode.capture,runtime:flavour==='original'?'1.27.0-JSEP-original':'1.27.0-JSEP-f32-accumulation-patch',hfRevision:revision}; results.push(record);let runner; try{ status(`初期化: ${variant.id} / ${record.executionLabel}`);const load=performance.now(),activeRuntime=await loadRuntime(flavour); activeRuntime.env.webgpu.profiling={mode:'off',ondata:()=>{}}; runner=await createExecution(activeRuntime,data,mode);record.loadMs=performance.now()-load; record.enabledGPUFeatures=[...runner.device.features]; for(let i=0;i<5;i++)await runner.run(inputs[i%inputs.length]); const times=[],submitTimes=[],readbacks=[],observed=[];status(`計測: ${variant.id} / ${mode.label}`);const started=performance.now(); for(let i=0;i{});render();} } } status(stopped?'停止しました':'計測完了。GPU完了と座標の読戻しを含む時間です。Graph Capture失敗は結果欄に記録します。');$('download').disabled=false; }catch(e){status(String(e.message||e));}finally{$('start').disabled=false;$('stop').disabled=true;} } $('video-file').onchange=e=>{if(!e.target.files[0])return;if(videoURL)URL.revokeObjectURL(videoURL);videoURL=URL.createObjectURL(e.target.files[0]);video.src=videoURL;video.load();}; $('start').onclick=measure;$('stop').onclick=()=>{stopped=true;}; $('download').onclick=()=>{const data={schema:'rtmw3d-gpu-comparison/v2',createdAt:new Date().toISOString(),source:runContext?.source,runContext,results,notes:['All variants output all 133 joints','Differences are relative to the architecture FP32 standard-path result, not ground truth','Timings include input upload, GPU completion and fresh 133-joint CPU readback','GPU timestamp profiling uses a separate session and is excluded from speed measurements','requestedBackend is the requested provider; it does not prove that every node ran on GPU','1–4 inputs measure a shared-session workload, not synchronized physical cameras','GPU utilization, VRAM, device identity, video names and video contents are not exported']};const u=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=u;a.download='rtmw3d-fullbody-gpu-comparison.json';a.click();setTimeout(()=>URL.revokeObjectURL(u),1000);}; $('native-input').onclick=()=>{if(!preparedInputs)return;if(new Uint8Array(new Uint32Array([1]).buffer)[0]!==1)throw Error('この端末ではlittle-endian入力を保存できません');const url=URL.createObjectURL(new Blob(preparedInputs,{type:'application/octet-stream'})),a=document.createElement('a');a.href=url;a.download='rtmw3d-native-inputs.f32';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}; try{ const model=await(await get(`https://huggingface.co/api/models/${REPO}`)).json();revision=model.sha;base=`https://huggingface.co/${REPO}/resolve/${revision}/`;catalog=await(await get(base+'models.json')).json(); if(catalog.keypointCount!==133)throw Error('全身133点のカタログではありません');$('models').replaceChildren(); const experimentalResponse=await fetch(base+'experiments/catalog.json',{credentials:'omit',referrerPolicy:'no-referrer'}); if(experimentalResponse.ok){const extra=await experimentalResponse.json();if(extra.keypointCount!==133||extra.schema!=='rtmw3d-head-experiments/v1')throw Error('実験カタログの全身出力契約が不正です');for(const v of extra.variants){if(!catalog.variants.some(existing=>existing.id===v.id))catalog.variants.push(v);}} else if(experimentalResponse.status!==404)throw Error('実験カタログの取得に失敗: '+experimentalResponse.status); for(const v of catalog.variants){const label=document.createElement('label'),checkbox=document.createElement('input');checkbox.type='checkbox';checkbox.value=v.id;checkbox.checked=v.id==='rtmw3d-l-fp32'||(v.experimental&&v.qualityPassed);label.append(checkbox,document.createTextNode(`${v.id} · ${(v.sizeBytes/1e6).toFixed(0)}MB${v.experimental?' · 等価FP32実験':''}`));$('models').append(label);} for(const m of MODES){const label=document.createElement('label'),checkbox=document.createElement('input');checkbox.type='checkbox';checkbox.value=m.id;checkbox.checked=m.id==='nhwc'||(m.id==='capture'&&catalog.variants.some(v=>v.gpuDecoderMinimum));label.append(checkbox,document.createTextNode(m.label));$('modes').append(label);} $('start').disabled=false;status(`HF revision ${revision}\n全候補:全133点。FP16・INT8の速さを仮定しません。`); }catch(e){status(String(e.message||e));}