y
This commit is contained in:
parent
178cac581c
commit
d185d2d8e9
6 changed files with 153 additions and 28 deletions
|
|
@ -8,6 +8,7 @@
|
|||
- 計算中は現在の視点の暫定画像を表示する。反復上限は「自動」または指定回数を選べる。未脱出を集合内の証明とは扱わない。
|
||||
- 画面と同じ解像度・1 sampleのPNGは、補修済みの完成画像をそのまま保存する。
|
||||
- 色相変化は表示済みの数値結果を使い、計算中も色を更新する。停止ボタンまたは色相位置の手動操作で停止する。
|
||||
- 彩色は「ピンクと黒」を追加し、「桃翠」「ネオン」「紅碧」「深海」を削除した。彩色の変更は計算中でも表示済みの数値結果を使い、数値再計算を起動しない。
|
||||
- 描画時間は初期描画から精細化・数値補修・最終GPU処理までの合計。計算中は経過時間、完了時は確定した時間を表示する。
|
||||
|
||||
BLAと比較専用の監査処理は削除済み。[整理内容と検証範囲](CLEANUP.md)を参照。
|
||||
|
|
@ -32,7 +33,7 @@ node tests/regression.mjs
|
|||
node tests/browser_smoke.mjs 9333 all
|
||||
```
|
||||
|
||||
`all` は `reset`、`fast`、`mid`、`legacy`、`axis`、`black`、`kernels`、`cancel`、`budget`、`ui` に置き換えて単独実行できる。描画6ケース(旧方式指定の移行を含む)、キューの被覆、独立高精度サンプル、計算中のキャンセル、指定反復予算、PNGの復号、色相変化、時間表示を確認する。
|
||||
`all` は `reset`、`fast`、`mid`、`legacy`、`axis`、`black`、`kernels`、`cancel`、`budget`、`ui`、`palette` に置き換えて単独実行できる。描画6ケース(旧方式指定の移行を含む)、キューの被覆、独立高精度サンプル、計算中のキャンセル、指定反復予算、PNGの復号、色相変化、時間表示、彩色変更時の数値再計算の抑止を確認する。
|
||||
|
||||
起動時は拡張機能と同期を無効にし、専用プロファイルのタブを1つだけにする。テストはそのタブを再利用し、最後に `about:blank` へ戻す。終了後は検証用ブラウザーを閉じる。
|
||||
|
||||
|
|
|
|||
|
|
@ -33,3 +33,29 @@
|
|||
追加した中間倍率(Seahorse、span=1e-6)と旧Deep専用の黒領域は、それぞれ12サンプルの分類と脱出反復数が独立BigInt計算に一致した。一方、連続色付け用smooth値の差は最大約0.0367反復分/0.0516反復分だった。両ケースではsmoothのULP精度を保証せず、偏差をテスト出力に記録する。高速Seahorseと実軸の8 ULP以内という検査は維持している。旧正確モードと同じ色の数値精度を維持したという主張ではない。
|
||||
|
||||
最終版でCPU回帰と実GPUの `all` が通過した。6描画ケースの数値失敗は0。1100×720の実画面でも自動色相変化による色の変化、描画完了直後の「2.32 s」、描画方式の選択肢がないことを確認した。検証用Edgeと専用プロファイルは終了・削除済みで、残存Edgeプロセスは0だった。
|
||||
|
||||
## 2026-09-07:配色整理と彩色変更の応答
|
||||
|
||||
「桃翠」「ネオン」「紅碧」「深海」をUIと色シェーダーから削除し、「ピンクと黒」(ID 19)を追加した。残る配色のIDは維持し、削除した配色を指定する旧URLは「昼夜」に戻す。
|
||||
|
||||
通常の彩色変更ハンドラー自体は数値再計算を起動していなかった。しかし計算中の彩色変更は、自動色相変化を有効にしたときだけ使える公開済みデータに依存し、無効時は計算終了まで待たされていた。計算中にも公開済みデータを保持することで、通常の彩色変更・色相位置・色周期も数値計算とは独立して反映する。連続入力は描画フレームごとに最新値へまとめる。
|
||||
|
||||
この変更に伴い、上記の追加領域は自動色相変化中に加えて数値計算中にも保持する。完成後、自動色相変化が無効で保留中の彩色もなければ解放する。追加容量は12 byte/画素+32 byteで変わらない。
|
||||
|
||||
色だけが違うURLへの移動・表示履歴の復元は再彩色だけにし、描画サイズが変わらないresize通知からの数値再計算も抑止した。計算用シェーダーは変更していない。
|
||||
|
||||
`node tests/browser_smoke.mjs 9333 palette` にて、320×240の彩色反映は完成後約19.1 ms、実際の数値補修中約9.3 msだった(各1回の測定、機器や負荷により変動)。数値投入回数・生成ID・計算結果が彩色変更で変わらず、計算中の彩色変更でも最終数値ハッシュが一致し、PNGの画素不一致が0であることを確認した。
|
||||
|
||||
CPU回帰と実GPUの `palette`・`ui` が通過。1100×720の実画面で新配色を確認し、削除済み配色のURLを開いても再計算せず「昼夜」へ戻ることも確認した。
|
||||
|
||||
その後「ピンクと黒」の暗部をプラム色へ調整。深いプラムからモーヴ、くすんだローズ、淡いピンクへ移る5色のグラデーションとし、この配色の未脱出・内点表示も紫がかった暗色にした。数値判定は変更せず、CPU回帰と1100×720の実GPU表示で確認した。
|
||||
|
||||
## 色相変化中のパン
|
||||
|
||||
彩色が遅いと、新しい色相変更が待機中に届き続け、再彩色ループが `recoloring=true` のままパン後の数値計算開始を阻害していた。1回の彩色で必ず処理を返し、視点更新があるときは数値計算を優先するようにした。
|
||||
|
||||
ドラッグ中は再投影による移動表示を優先し、自動色相の描画更新は操作終了後に再開する。数値計算中の自動彩色は最大10回/秒、静止中は従来の最大20回/秒。公開済みデータを使う彩色も未完了のGPU処理を1件までに制限し、遅いGPUへ処理を積み続けない。色相変化のオン状態は維持する。
|
||||
|
||||
320×240で同一のパンを実マウスイベントから実行した結果、完了までオフ約343 ms、オン約352 msだった。彩色へ意図的に100 msの遅延を加えた検査でも約677 msで完了し、3条件の数値ハッシュが一致した。各1回の測定であり、機器・座標に依存する。再現検査は `node tests/browser_smoke.mjs 9333 pan`。
|
||||
|
||||
プラムの暗部も再調整し、未脱出・内点は `#0e0a13`、グラデーションの最暗部は `#140e1a` とした。ローズと淡いピンクの色は維持する。
|
||||
|
|
|
|||
64
index.html
64
index.html
|
|
@ -34,7 +34,7 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
|
|||
<div class="group">
|
||||
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
|
||||
<div class="line"><label for="continuationBudget">反復上限</label><select id="continuationBudget"><option value="0">自動</option><option value="4096">4,096 回</option><option value="16384">16,384 回</option><option value="32768">32,768 回</option><option value="65536">65,536 回</option><option value="150000">150,000 回</option></select><output></output></div>
|
||||
<div class="line"><label for="palette">彩色</label><select id="palette" aria-label="彩色プリセット"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option><option value="3">インフェルノ</option><option value="5">氷晶</option><option value="7">オーロラ</option><option value="9">深海</option><option value="10">黄金</option><option value="11">ネオン</option><option value="13">電光</option><option value="15">蒼橙</option><option value="17">紅碧</option><option value="18">桃翠</option></select><output></output></div>
|
||||
<div class="line"><label for="palette">彩色</label><select id="palette" aria-label="彩色プリセット"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option><option value="3">インフェルノ</option><option value="5">氷晶</option><option value="7">オーロラ</option><option value="10">黄金</option><option value="13">電光</option><option value="15">蒼橙</option><option value="19">ピンクと黒</option></select><output></output></div>
|
||||
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
|
||||
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false" title="色周期は固定し、色相位置だけを動かします">色相変化</button></div>
|
||||
|
|
@ -819,22 +819,20 @@ fn palette_color(phase:f32)->vec3<f32>{
|
|||
if(p.palette==1u){return hsv(phase,0.92,1.0);}
|
||||
if(p.palette==2u){let g=(18.0+237.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3<f32>(g);}
|
||||
let t=tri_phase(phase);
|
||||
if(p.palette==19u){return ramp5(t,rgb8(20.0,14.0,26.0),rgb8(72.0,47.0,83.0),rgb8(161.0,92.0,130.0),rgb8(224.0,130.0,164.0),rgb8(249.0,193.0,205.0));}
|
||||
if(p.palette==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));}
|
||||
if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));}
|
||||
if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));}
|
||||
if(p.palette==9u){return ramp5(t,rgb8(0.0,5.0,20.0),rgb8(4.0,42.0,82.0),rgb8(0.0,113.0,136.0),rgb8(34.0,176.0,162.0),rgb8(221.0,246.0,202.0));}
|
||||
if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));}
|
||||
if(p.palette==11u){return ramp5(t,rgb8(3.0,0.0,16.0),rgb8(250.0,0.0,132.0),rgb8(0.0,228.0,255.0),rgb8(126.0,255.0,0.0),rgb8(255.0,247.0,255.0));}
|
||||
if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));}
|
||||
if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));}
|
||||
if(p.palette==17u){return ramp5(t,rgb8(20.0,1.0,14.0),rgb8(204.0,26.0,72.0),rgb8(255.0,226.0,182.0),rgb8(0.0,202.0,226.0),rgb8(226.0,249.0,255.0));}
|
||||
if(p.palette==18u){return ramp5(t,rgb8(22.0,8.0,24.0),rgb8(171.0,36.0,122.0),rgb8(255.0,211.0,149.0),rgb8(45.0,204.0,176.0),rgb8(232.0,255.0,243.0));}
|
||||
return current_palette(phase);
|
||||
}
|
||||
fn interior_color()->vec3<f32>{return select(vec3<f32>(0.0),rgb8(14.0,10.0,19.0),p.palette==19u);}
|
||||
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
|
||||
// Direct phase mapping: no iteration-band/anchor compensation.
|
||||
let phase=fract(p.shift+sm*p.cycle);
|
||||
let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(vec3<f32>(0.0,0.0,0.0),vec3<f32>(2.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
|
||||
let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(interior_color(),vec3<f32>(2.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
|
||||
}
|
||||
fn base_color(i:u32)->vec3<f32>{
|
||||
let m=fieldMeta[i];return escaped_color(m,fieldSmooth[i]);
|
||||
|
|
@ -848,8 +846,8 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
if(cls==FIELD_UNKNOWN){
|
||||
let reason=(m>>20u)&0xffu;
|
||||
// Iteration-limit pixels are not a numeric hole: display them as opaque
|
||||
// black so temporal history cannot paint stale exterior colour through.
|
||||
if(reason==6u){textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0,0.0,0.0,1.0));}
|
||||
// a dark opaque color so temporal history cannot paint stale exterior colour through.
|
||||
if(reason==6u){textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(interior_color(),1.0));}
|
||||
else{textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0));}
|
||||
return;
|
||||
}
|
||||
|
|
@ -857,7 +855,7 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
// Certified / likely interior is real image content, not a temporal hole.
|
||||
// Keep it opaque so a reprojected history frame can never leak exterior
|
||||
// colour into an already-known Mandelbrot interior pixel.
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0,0.0,0.0,1.0));
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(interior_color(),1.0));
|
||||
return;
|
||||
}
|
||||
var c=escaped_color(m,fieldSmooth[i]);
|
||||
|
|
@ -1002,7 +1000,7 @@ globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
|||
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
|
||||
const $=s=>document.querySelector(s),canvas=$('#view');
|
||||
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
|
||||
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=18,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,9,10,11,13,15,17,18]);
|
||||
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=19,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,10,13,15,19]);
|
||||
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
|
||||
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
|
||||
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
|
||||
|
|
@ -1051,7 +1049,8 @@ function deepNeeded(){return false}
|
|||
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,continuationBudget:state.continuationBudget,renderMode:state.renderMode}}
|
||||
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive,v.continuationBudget||0].join(':')}
|
||||
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
|
||||
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});ensurePrecision();syncControls();saveHash(false);markDirty()}
|
||||
function numericViewKey(){return[state.bits,state.re,state.im,state.span,state.baseIter,state.adaptive,state.continuationBudget].join(':')}
|
||||
function restoreView(v){if(!v)return;const before=numericViewKey();Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:VALID_PALETTE_IDS.has(v.palette)?v.palette:0,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});ensurePrecision();syncControls();saveHash(false);if(before===numericViewKey())requestRecolor();else markDirty()}
|
||||
|
||||
// ── iteration / quality policy ───────────────────────────────────────────
|
||||
const ADAPTIVE_ITER_QUANTUM=64;
|
||||
|
|
@ -1515,9 +1514,9 @@ class WebGpuRenderer{
|
|||
const source=this.colorSource;this.colorSource=null;this.colorSourceVisible=false;
|
||||
if(source){this.clearBindGroupCache();for(const k of ['meta','smooth','params','texture'])destroy(source[k])}
|
||||
}
|
||||
captureColorSource(view=state.frameView){
|
||||
captureColorSource(view=state.frameView,needed=state.colorAuto||state.rendering){
|
||||
this.colorSourceVisible=false;
|
||||
if(!state.colorAuto){this.releaseColorSource();return}
|
||||
if(!needed){this.releaseColorSource();return}
|
||||
const f=this.frame;if(!f||!view)return;
|
||||
const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
|
||||
if(!this.colorSource){this.colorSource={
|
||||
|
|
@ -1533,11 +1532,15 @@ class WebGpuRenderer{
|
|||
recolorPublished(){
|
||||
// Only published numerical data is sampled during in-flight refinement.
|
||||
// This pass uses its own uniforms/texture and never swaps numeric front/back.
|
||||
const source=this.colorSource,f=this.frame;if(!source||!f)return false;
|
||||
const source=this.colorSource,f=this.frame;if(!source||!f||this.publishedColorWork)return false;
|
||||
const d=this.device;d.queue.writeBuffer(source.params,0,this.colorParamsData(f.w,f.h));
|
||||
const bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:source.params}},{binding:1,resource:{buffer:source.meta}},{binding:2,resource:{buffer:source.smooth}},{binding:3,resource:this.cachedTextureView(source.texture)}]);
|
||||
const e=d.createCommandEncoder({label:'animate-published-color'}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);
|
||||
this.colorSourceVisible=true;runtime.gpuRecolors++;this.presentFrame();return true;
|
||||
this.colorSourceVisible=true;runtime.gpuRecolors++;this.presentFrame();
|
||||
// Bound optional color work even when the GPU is slower than animation.
|
||||
const work=d.queue.onSubmittedWorkDone();this.publishedColorWork=work;
|
||||
const done=()=>{if(this.publishedColorWork===work)this.publishedColorWork=null};work.then(done,done);
|
||||
return true;
|
||||
}
|
||||
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(token!==state.token||!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder(),bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuRecolors++;return true}
|
||||
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
|
||||
|
|
@ -1838,7 +1841,7 @@ function startRenderClock(token,startedAt){
|
|||
function finishRenderClock(token,status='complete'){
|
||||
const c=state.renderClock;if(!c||c.token!==token||c.status!=='running')return;
|
||||
c.elapsed=performance.now()-c.startedAt;c.status=status;
|
||||
if(status==='complete')state.lastRender=c.elapsed;
|
||||
if(status==='complete'){state.lastRender=c.elapsed;if(!state.colorAuto&&!state.recolorPending)renderer?.releaseColorSource()}
|
||||
clearInterval(renderClockTimer);renderClockTimer=0;
|
||||
$('#render').textContent=renderTimeLabel();
|
||||
}
|
||||
|
|
@ -1911,7 +1914,7 @@ async function runQualityRefinement(snap,token,stages,index){
|
|||
}
|
||||
async function renderFrame(){
|
||||
const token=++state.token,snap=snapshot(),finalIter=maxIter(),holdStable=canUseStableReprojection(),stages=qualityStages(finalIter),iter=stages[0],isSingleFinal=stages.length===1,t0=performance.now();
|
||||
if(renderer&&state.fieldView?.complete&&state.colorAuto)renderer.captureColorSource(state.frameView);
|
||||
if(renderer&&state.fieldView?.complete)renderer.captureColorSource(state.frameView,true);
|
||||
startRenderClock(token,t0);
|
||||
const generation=TEST_MODE?{token,startedAt:t0,viewKey:[snap.bits,snap.re,snap.im,snap.span].join(':'),width:canvas.width,height:canvas.height,previewIter:iter,finalIter,numericSubmitsStart:runtime.numericSubmits,correctionSubmitsStart:runtime.correctionSubmits,gpuSyncWaitsStart:runtime.gpuSyncWaits,status:'running'}:null;if(generation){runtime.generationMetrics.push(generation);if(runtime.generationMetrics.length>16)runtime.generationMetrics.shift();}
|
||||
state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.frontierRounds=0;state.frontierPixels=0;state.frontierEscaped=0;state.frontierIter=iter;state.frontierMinimumIter=PIXEL_FRONTIER_ENABLED?pixelFrontierIterationFloor(snap,canvas.width,iter):iter;state.frontierConverged=false;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.stageCounts={};runtime.stageMs={};runtime.frontierMaxDispatchWork=0;updateStats();
|
||||
|
|
@ -1933,9 +1936,19 @@ async function renderFrame(){
|
|||
if(!isSingleFinal)scheduleQualityRefinement(snap,token,stages,1);
|
||||
}catch(e){if(generation){generation.status=token===state.token?'error':'cancelled';generation.error=String(e&&e.message||e);}if(token!==state.token)return;finishRenderClock(token,'error');state.drawState='ERROR';state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
|
||||
}
|
||||
let recolorRaf=0;
|
||||
function requestRecolor(){
|
||||
state.colorRevision++;state.recolorPending=true;
|
||||
if(state.rendering||state.numericRecoveryRunning){renderer?.recolorPublished();return}
|
||||
queueRecolor();
|
||||
}
|
||||
function queueRecolor(){
|
||||
if(recolorRaf)return;
|
||||
recolorRaf=requestAnimationFrame(()=>{recolorRaf=0;flushRecolor()});
|
||||
}
|
||||
function flushRecolor(){
|
||||
if(state.pointerActive||state.wheelActive)return;
|
||||
if(state.dirty){schedule();return}
|
||||
if(state.rendering||state.numericRecoveryRunning){if(renderer?.colorSource&&!renderer.recolorPublished())queueRecolor();return}
|
||||
// Keep the old completed frame valid until the replacement colour pass has
|
||||
// actually finished. Invalidating history at slider-down caused apparent
|
||||
// non-response/flicker when GPU work was already in flight.
|
||||
|
|
@ -1943,10 +1956,10 @@ function requestRecolor(){
|
|||
}
|
||||
async function recolor(){
|
||||
if(!renderer?.frame||!state.fieldView){state.recolorPending=true;return false}
|
||||
if(state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false}
|
||||
if(state.dirty||state.pointerActive||state.wheelActive||state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false}
|
||||
state.recoloring=true;let painted=false;
|
||||
try{
|
||||
while(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning&&renderer?.frame&&state.fieldView){
|
||||
while(state.recolorPending&&!state.dirty&&!state.pointerActive&&!state.wheelActive&&!state.rendering&&!state.numericRecoveryRunning&&renderer?.frame&&state.fieldView){
|
||||
state.recolorPending=false;const revision=state.colorRevision,token=state.token,ok=await renderer.recolor(token);
|
||||
if(!ok||token!==state.token){state.recolorPending=true;continue}
|
||||
renderer.colorSourceVisible=false;
|
||||
|
|
@ -1957,10 +1970,13 @@ async function recolor(){
|
|||
// latest value instead of replaying stale colour states.
|
||||
if(revision===state.colorRevision&&state.qualityStage==='final'&&!state.refinePending&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false}
|
||||
updateStats();
|
||||
// Yield ownership after one pass: a stream of hue changes must not keep
|
||||
// recoloring=true forever and prevent a pending pan from computing.
|
||||
break;
|
||||
}
|
||||
return painted
|
||||
}catch(e){console.error(e);return false}
|
||||
finally{state.recoloring=false;if(state.dirty)schedule();if(state.recolorPending&&renderer?.frame&&!state.rendering&&!state.numericRecoveryRunning)queueMicrotask(recolor)}
|
||||
finally{state.recoloring=false;if(state.dirty)schedule();else if(state.recolorPending&&renderer?.frame&&!state.pointerActive&&!state.wheelActive&&!state.rendering&&!state.numericRecoveryRunning)queueRecolor()}
|
||||
}
|
||||
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering&&!state.recoloring)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
|
||||
|
||||
|
|
@ -1997,7 +2013,7 @@ $('#share').onclick=copyShareUrl;
|
|||
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
|
||||
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
|
||||
function stopColorAuto(repaint=true){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton();if(repaint)requestRecolor()}
|
||||
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
|
||||
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(!state.pointerActive&&!state.wheelActive&&now-colorAutoPaint>=(state.rendering?100:50)){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
|
||||
$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){if(!state.rendering&&!state.numericRecoveryRunning)renderer?.captureColorSource();colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
|
||||
$('#palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash(false)});
|
||||
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
|
||||
|
|
@ -2006,8 +2022,8 @@ $('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventList
|
|||
$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false));
|
||||
$('#continuationBudget').onchange=e=>{state.continuationBudget=Math.max(0,Math.min(150000,Number(e.target.value)||0));saveHash(false);recordView();markDirty()};
|
||||
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
|
||||
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
|
||||
addEventListener('resize',()=>{const w=canvas.width,h=canvas.height;resize();if(w!==canvas.width||h!==canvas.height)markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);const before=numericViewKey();if(loadHash()){syncControls();recordView();if(before===numericViewKey())requestRecolor();else markDirty()}});
|
||||
|
||||
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
|
||||
const exportJob={active:false,cancelled:false};
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const cases=[
|
|||
{id:'axis',re:'-0.75',im:'0',span:'0.02',renderMode:'fast',backend:'direct'},
|
||||
{id:'black',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,fixed:true,renderMode:'accurate',backend:'fast-extended'}
|
||||
];
|
||||
assert.ok(['all','kernels','cancel','bench','budget','ui'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
|
||||
assert.ok(['all','kernels','cancel','bench','budget','ui','palette','pan'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
|
||||
|
||||
// Execute the real production kernels on small, fully specified states.
|
||||
// This isolates terminal and queue behavior from render convergence heuristics.
|
||||
|
|
@ -137,6 +137,77 @@ try{
|
|||
for(let i=0;i<100;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(100)}
|
||||
const ready=await evaluate('globalThis.__MANDEL_TEST__.ensureGpuReady()');
|
||||
assert.equal(ready.ready,true,JSON.stringify(ready));
|
||||
if(selection==='pan'||selection==='all'){
|
||||
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
await evaluate('if(!document.body.classList.contains("ui-hidden"))document.querySelector("#uiToggle").click()');
|
||||
const reports=[];let expected;
|
||||
for(const mode of ['off','on','slow-color']){
|
||||
await evaluate('if(__MANDEL_TEST__.state().colorAuto)document.querySelector("#colorAuto").click()');
|
||||
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[0],bits:448,baseIter:512,adaptive:false})})`);
|
||||
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
if(mode==='slow-color')await evaluate('const r=__MANDEL_TEST__.kernelAccess().renderer;globalThis.originalRecolor=r.recolor;r.recolor=async function(...args){await new Promise(done=>setTimeout(done,100));return originalRecolor.apply(this,args)}');
|
||||
if(mode!=='off'){
|
||||
await evaluate('document.querySelector("#colorAuto").click()');
|
||||
if(mode==='slow-color')for(let i=0;i<100;i++){if(await evaluate('__MANDEL_TEST__.state().recoloring'))break;await sleep(5)}
|
||||
}
|
||||
const start=performance.now();
|
||||
await command('Input.dispatchMouseEvent',{type:'mousePressed',x:160,y:120,button:'left',buttons:1,clickCount:1});
|
||||
await command('Input.dispatchMouseEvent',{type:'mouseMoved',x:176,y:120,button:'left',buttons:1});
|
||||
await command('Input.dispatchMouseEvent',{type:'mouseReleased',x:176,y:120,button:'left',buttons:0,clickCount:1});
|
||||
await sleep(130);
|
||||
const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete({timeout:10000})');
|
||||
const elapsed=performance.now()-start,hash=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
||||
if(expected)assert.deepEqual(hash,expected,'animated pan must calculate the same view');expected=hash;
|
||||
if(mode!=='off')assert.equal(final.colorAuto,true,'animation stays enabled');
|
||||
if(mode==='slow-color')await evaluate('__MANDEL_TEST__.kernelAccess().renderer.recolor=originalRecolor;delete globalThis.originalRecolor');
|
||||
reports.push({mode,elapsedMs:elapsed,renderMs:final.lastRender});
|
||||
}
|
||||
assert.ok(reports[1].elapsedMs<reports[0].elapsedMs*2.5+750,'animation must not starve pan');
|
||||
assert.ok(reports[2].elapsedMs<reports[0].elapsedMs*2.5+1000,'slow color producer must yield to pan');
|
||||
const bounded=await evaluate('(async()=>{const r=__MANDEL_TEST__.kernelAccess().renderer;await r.device.queue.onSubmittedWorkDone();return Array.from({length:20},()=>r.recolorPublished())})()');
|
||||
assert.equal(bounded.filter(Boolean).length,1,'at most one published color pass in flight');
|
||||
await evaluate('document.querySelector("#colorAuto").click()');
|
||||
console.log(JSON.stringify({pan:reports}));
|
||||
}
|
||||
if(selection==='palette'||selection==='all'){
|
||||
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
const options=await evaluate('Array.from(document.querySelector("#palette").options,o=>({id:Number(o.value),name:o.textContent}))');
|
||||
assert.deepEqual(options.map(o=>o.id),[0,1,2,3,5,7,10,13,15,19]);assert.equal(options.at(-1).name,'ピンクと黒');
|
||||
const before=await evaluate('({s:__MANDEL_TEST__.state()})');
|
||||
const hashes=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
||||
const oldColor=await evaluate(`(${displayedColorHash.toString()})()`);
|
||||
const latency=await evaluate(`(async()=>{const t=performance.now(),e=document.querySelector('#palette');e.value='19';e.dispatchEvent(new Event('change'));while(__MANDEL_TEST__.state().recolorPending||__MANDEL_TEST__.state().recoloring)await new Promise(r=>setTimeout(r,5));return performance.now()-t})()`);
|
||||
assert.ok(latency<1000,'completed-frame recolor should not take seconds');
|
||||
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),oldColor);
|
||||
// Exercise resize events and color-only URL history independently of the
|
||||
// dropdown's replaceState. None may start Mandelbrot computation.
|
||||
await evaluate('dispatchEvent(new Event("resize"));const p=new URLSearchParams(location.hash.slice(1));p.set("pal","1");location.hash=p.toString()');
|
||||
await sleep(150);
|
||||
const after=await evaluate('__MANDEL_TEST__.state()');
|
||||
assert.equal(after.palette,1);assert.equal(after.numericSubmits,before.s.numericSubmits);assert.equal(after.correctionSubmits,before.s.correctionSubmits);
|
||||
assert.equal(after.renderClock.token,before.s.renderClock.token);assert.equal(after.lastRender,before.s.lastRender);
|
||||
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),hashes);
|
||||
// With animation OFF, change palette during real CPU repair, before the
|
||||
// ongoing numerical render completes. Read the texture actually displayed.
|
||||
const work=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`);
|
||||
let active=false;
|
||||
for(let i=0;i<1000;i++){const s=await evaluate('__MANDEL_TEST__.state()');if(s.rendering&&s.provisional&&s.engine.includes('数値補修')){active=true;break}await sleep(20)}
|
||||
assert.ok(active);
|
||||
const prior=await evaluate(`(${displayedColorHash.toString()})()`);
|
||||
const duringMs=await evaluate(`(async()=>{const start=performance.now(),e=document.querySelector('#palette');e.value='19';e.dispatchEvent(new Event('change'));while(!__MANDEL_TEST__.kernelAccess().renderer.colorSourceVisible)await new Promise(r=>setTimeout(r,5));await __MANDEL_TEST__.kernelAccess().renderer.device.queue.onSubmittedWorkDone();return performance.now()-start})()`);
|
||||
assert.ok(duringMs<1000,'palette must not wait for numeric completion');
|
||||
const during=await evaluate('__MANDEL_TEST__.state()');assert.equal(during.colorAuto,false);assert.equal(during.rendering,true);
|
||||
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),prior);
|
||||
await work;await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
for(let i=0;i<100;i++){if(await evaluate('!__MANDEL_TEST__.state().recolorPending&&!__MANDEL_TEST__.state().recoloring'))break;await sleep(20)}
|
||||
assert.equal(await evaluate('__MANDEL_TEST__.kernelAccess().renderer.frontColorKey.split(":")[0]'),'19');
|
||||
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSource'),false);
|
||||
const completeHashes=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
||||
assert.equal(completeHashes.metaSha256,'78d0fbfd138de5eccb0308ddd4bdca0839c29cbd6795d85c9d016a54dcc63080');
|
||||
assert.equal(completeHashes.smoothSha256,'b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e');
|
||||
assert.equal((await evaluate('__MANDEL_TEST__.pngRoundTrip()')).mismatches,0);
|
||||
console.log(JSON.stringify({palette:'PASS: pixels, no numeric rerender, color-only URL, resize, recolor during repair, PNG',latencyMs:latency,duringMs}));
|
||||
}
|
||||
if(selection==='ui'||selection==='all'){
|
||||
const initial=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
assert.equal(initial.renderClock.status,'complete');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"UNKNOWN_STATS_WGSL": "df1967bcdacf29831403fa53aadaae17ed138b36d1988cf20f6a6ed3e43e519a",
|
||||
"SYMMETRY_COPY_WGSL": "658e1cf2b9f40d8e759abe2269c041f582a80727cb24e2f48a405cb8676d70e1",
|
||||
"FAILURE_TILE_MAP_WGSL": "8308f57151b119363ad456a7e1255c5445ed5e022ccf53e923a4b13ab2a1ca91",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"COLOR_WGSL": "14267241d4022df33a03faeec0649c1d441ee482f2a328467c0d4215f8c90a60",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function appContext(search=''){
|
|||
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},addEventListener(){},classList:{toggle(){}},setAttribute(){}};
|
||||
const context={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element},navigator:{hardwareConcurrency:4},location:{search},performance,URLSearchParams,TextEncoder,console,addEventListener(){}};
|
||||
const code=scripts[1].slice(0,scripts[1].indexOf('// ── boot / teardown'))+
|
||||
'globalThis.internal={state,runtime,refs,refinePixelFrontier,setTestPresentation:fn=>{presentProvisional=fn},WebGpuRenderer,chooseBackend,startRenderClock,finishRenderClock,renderTimeLabel,fromDec,operationLimitIndices,numericalFailureIndices,pixelPrecisionBits,precisionBatchSize,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource};})();';
|
||||
'globalThis.internal={state,runtime,refs,refinePixelFrontier,setTestPresentation:fn=>{presentProvisional=fn},recolor,setRecolorRenderer:r=>{renderer=r;updateStats=()=>{}},WebGpuRenderer,chooseBackend,startRenderClock,finishRenderClock,renderTimeLabel,fromDec,operationLimitIndices,numericalFailureIndices,pixelPrecisionBits,precisionBatchSize,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource};})();';
|
||||
vm.runInNewContext(code,context);
|
||||
return context;
|
||||
}
|
||||
|
|
@ -29,6 +29,17 @@ assert.equal(typeof diagnostic.__MANDEL_TEST__.state,'function');
|
|||
const a=normal.internal;
|
||||
assert.equal(a.runtime.generationMetrics.length,0);
|
||||
|
||||
// A faster animation producer must not hold recoloring=true indefinitely.
|
||||
// Release after one GPU pass, and do not take ownership from a pending pan.
|
||||
{
|
||||
const c=appContext(),i=c.internal;let calls=0,scheduled=0;
|
||||
c.requestAnimationFrame=()=>++scheduled;
|
||||
Object.assign(i.state,{dirty:false,colorAuto:true,fieldView:{complete:true},recolorPending:true});
|
||||
i.setRecolorRenderer({frame:{},recolor:async()=>{calls++;i.state.recolorPending=calls<3;return true},presentFrame(){},presentTransform(){return{}},commitHistory(){}});
|
||||
await i.recolor();assert.equal(calls,1);assert.equal(i.state.recoloring,false);assert.ok(scheduled>0);
|
||||
i.state.dirty=true;await i.recolor();assert.equal(calls,1,'pending numeric render has priority');
|
||||
}
|
||||
|
||||
// Wall time survives stage boundaries; stale completions cannot finish a new
|
||||
// generation. No real timer remains running after completion or cancellation.
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue