three-base 2.0.168 → 2.0.169
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MktMesh/MktMesh.js +4 -1
- package/MktMesh/MktMesh.ts +4 -1
- package/MktMesh/panoSlotBind.js +18 -2
- package/MktMesh/panoSlotBind.ts +18 -1
- package/MktMesh/panoTileStitcher.d.ts +35 -7
- package/MktMesh/panoTileStitcher.js +71 -11
- package/MktMesh/panoTileStitcher.ts +73 -12
- package/package.json +1 -1
package/MktMesh/MktMesh.js
CHANGED
|
@@ -795,7 +795,10 @@ export default class MktMesh extends Mesh {
|
|
|
795
795
|
const p = rtStitchKeyParts(key);
|
|
796
796
|
if (!p)
|
|
797
797
|
return null;
|
|
798
|
-
|
|
798
|
+
// ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:透传 renderer,
|
|
799
|
+
// 新全景还没画进格时 peek 会**先把 RT 建出来(已涂缺失灰)并交出** ⇒ 这一帧拿到深灰底,
|
|
800
|
+
// 而非 null(null 会被 panoSlotBind 判成「判不齐」⇒ 沿用上一批 ⇒ 墙上闪上一个全景)。
|
|
801
|
+
const tex = this._stitcher.peek(p.batch, p.slot, this._glRenderer);
|
|
799
802
|
if (!tex)
|
|
800
803
|
return null;
|
|
801
804
|
// ⭐ 皇上旨 09-29「两个大头别在同一帧」:普通瓦片不许在 render 首绑现场 texSubImage2D ⇒ 走
|
package/MktMesh/MktMesh.ts
CHANGED
|
@@ -826,7 +826,10 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
826
826
|
if (isRtStitchKey(key)) {
|
|
827
827
|
const p = rtStitchKeyParts(key);
|
|
828
828
|
if (!p) return null;
|
|
829
|
-
|
|
829
|
+
// ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:透传 renderer,
|
|
830
|
+
// 新全景还没画进格时 peek 会**先把 RT 建出来(已涂缺失灰)并交出** ⇒ 这一帧拿到深灰底,
|
|
831
|
+
// 而非 null(null 会被 panoSlotBind 判成「判不齐」⇒ 沿用上一批 ⇒ 墙上闪上一个全景)。
|
|
832
|
+
const tex = this._stitcher.peek(p.batch, p.slot, this._glRenderer);
|
|
830
833
|
if (!tex) return null;
|
|
831
834
|
// ⭐ 皇上旨 09-29「两个大头别在同一帧」:普通瓦片不许在 render 首绑现场 texSubImage2D ⇒ 走
|
|
832
835
|
// 「每 tick 一张」预上传队列;没排到的帧返回 null,自动推迟绑定 ⇒ render 里从此没有首绑上传。
|
package/MktMesh/panoSlotBind.js
CHANGED
|
@@ -79,8 +79,24 @@ export function panoSlotBind(mat, panos, base, tiles, tiled, slot, deps) {
|
|
|
79
79
|
// ⭐ 09-28 旨后不再退缩图档(过渡档去参 = 同一张原图,兜底只剩双下载)
|
|
80
80
|
got.push(deps.peek(tiles[k], tiled));
|
|
81
81
|
}
|
|
82
|
-
if (!got.every((t) => !!t))
|
|
83
|
-
|
|
82
|
+
if (!got.every((t) => !!t)) {
|
|
83
|
+
// ⭐⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:
|
|
84
|
+
// 判不齐时**不再沿用上一批** —— 旧行为让 `panos[]` 继续挂着上一个全景的 RT 纹理,
|
|
85
|
+
// 墙上就一直显示上一个全景的内容(皇上报的现象),直到新全景某格画满才换过去。
|
|
86
|
+
// 现在:只要这一帧**有任何一张**已经能拿到纹理(`got` 里非 null 的那张,是新批次的 RT、
|
|
87
|
+
// 建好即涂 `MISSING_TILE_GRAY` 缺失灰 ⇒ 深灰底),就**先把整槽换成新批次**:
|
|
88
|
+
// 拿得到的那几格用深灰 RT,仍在路上的那几格用占位(1×1 素色,与深灰同调,不会闪旧图)。
|
|
89
|
+
// ⛔ 一张都拿不到(连 RT 都没建起来)才真的什么都不动,保留上一批,等下一帧重试。
|
|
90
|
+
const any = got.filter((t) => !!t);
|
|
91
|
+
if (any.length > 0) {
|
|
92
|
+
for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
|
|
93
|
+
panos[base + k] = got[k] ?? deps.placeholder;
|
|
94
|
+
keys[base + k] = tiles[k];
|
|
95
|
+
}
|
|
96
|
+
return false; // 账本已切到新批次 ⇒ 返回 false(矩阵与权重这一帧不动,下一帧继续判)
|
|
97
|
+
}
|
|
98
|
+
return false; // 一张都拿不到:图 + 矩阵 + 账本一概留着(与旧行为一致)
|
|
99
|
+
}
|
|
84
100
|
for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
|
|
85
101
|
panos[base + k] = got[k];
|
|
86
102
|
keys[base + k] = tiles[k];
|
package/MktMesh/panoSlotBind.ts
CHANGED
|
@@ -128,7 +128,24 @@ export function panoSlotBind(
|
|
|
128
128
|
// ⭐ 09-28 旨后不再退缩图档(过渡档去参 = 同一张原图,兜底只剩双下载)
|
|
129
129
|
got.push(deps.peek(tiles[k], tiled));
|
|
130
130
|
}
|
|
131
|
-
if (!got.every((t) => !!t))
|
|
131
|
+
if (!got.every((t) => !!t)) {
|
|
132
|
+
// ⭐⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:
|
|
133
|
+
// 判不齐时**不再沿用上一批** —— 旧行为让 `panos[]` 继续挂着上一个全景的 RT 纹理,
|
|
134
|
+
// 墙上就一直显示上一个全景的内容(皇上报的现象),直到新全景某格画满才换过去。
|
|
135
|
+
// 现在:只要这一帧**有任何一张**已经能拿到纹理(`got` 里非 null 的那张,是新批次的 RT、
|
|
136
|
+
// 建好即涂 `MISSING_TILE_GRAY` 缺失灰 ⇒ 深灰底),就**先把整槽换成新批次**:
|
|
137
|
+
// 拿得到的那几格用深灰 RT,仍在路上的那几格用占位(1×1 素色,与深灰同调,不会闪旧图)。
|
|
138
|
+
// ⛔ 一张都拿不到(连 RT 都没建起来)才真的什么都不动,保留上一批,等下一帧重试。
|
|
139
|
+
const any = got.filter((t) => !!t);
|
|
140
|
+
if (any.length > 0) {
|
|
141
|
+
for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
|
|
142
|
+
panos[base + k] = got[k] ?? deps.placeholder;
|
|
143
|
+
keys[base + k] = tiles[k];
|
|
144
|
+
}
|
|
145
|
+
return false; // 账本已切到新批次 ⇒ 返回 false(矩阵与权重这一帧不动,下一帧继续判)
|
|
146
|
+
}
|
|
147
|
+
return false; // 一张都拿不到:图 + 矩阵 + 账本一概留着(与旧行为一致)
|
|
148
|
+
}
|
|
132
149
|
for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
|
|
133
150
|
panos[base + k] = got[k] as Texture;
|
|
134
151
|
keys[base + k] = tiles[k];
|
|
@@ -6,8 +6,14 @@ import { Texture, type WebGLRenderer } from "three";
|
|
|
6
6
|
export interface PanoStitcher {
|
|
7
7
|
/** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做) */
|
|
8
8
|
ensure(urls: string[]): void;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
|
|
11
|
+
* ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
|
|
12
|
+
* 先把 RT 建出来(建完即整张涂 `MISSING_TILE_GRAY` 缺失灰)并返回它** ⇒ 调用方切批次的那一帧
|
|
13
|
+
* 拿到的就是**深灰底**而非 null(null 会让它沿用上一批 ⇒ 墙上闪上一个全景)。
|
|
14
|
+
* ⛔ 没传 `renderer`(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
|
|
15
|
+
*/
|
|
16
|
+
peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
|
|
11
17
|
/** 每帧驱动(消费方每帧只调一次)。`renderer` = 调用方当前的 WebGLRenderer,还没捕获到就传 null
|
|
12
18
|
* ⇒ 这一帧什么都不画(⛔ 不打假读数,也⛔ 不退回任何 CPU 拼合路径)。 */
|
|
13
19
|
pump(renderer: WebGLRenderer | null): void;
|
|
@@ -36,6 +42,12 @@ export declare class PanoTileStitcher implements PanoStitcher {
|
|
|
36
42
|
private _retryAt;
|
|
37
43
|
private _fails;
|
|
38
44
|
private _dead;
|
|
45
|
+
/** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
|
|
46
|
+
* ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
|
|
47
|
+
* 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
|
|
48
|
+
* —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
|
|
49
|
+
* 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
|
|
50
|
+
private _uploadQueued;
|
|
39
51
|
/** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
|
|
40
52
|
private _renderer;
|
|
41
53
|
/** 推进计数(`progressStamp`)*/
|
|
@@ -59,7 +71,7 @@ export declare class PanoTileStitcher implements PanoStitcher {
|
|
|
59
71
|
ensure(urls: string[]): void;
|
|
60
72
|
/** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
|
|
61
73
|
* RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
|
|
62
|
-
peek(batch: string, slot: number): Texture | null;
|
|
74
|
+
peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
|
|
63
75
|
/** ⭐ 这条路交出去的槽纹理本来就是 GL 渲染目标的附件 —— 像素已经在显存里,没有「上传」这一步。
|
|
64
76
|
* 消费方(`MktMesh._peekTileAt` / 编辑器 `_peekTileAt`)据此**跳过**「每 tick 一张 `initTexture`」那道闸。 */
|
|
65
77
|
resident(tex: Texture): boolean;
|
|
@@ -82,11 +94,27 @@ export declare class PanoTileStitcher implements PanoStitcher {
|
|
|
82
94
|
private _drawPass;
|
|
83
95
|
/** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
|
|
84
96
|
* 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
|
|
97
|
+
/**
|
|
98
|
+
* 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
|
|
99
|
+
*
|
|
100
|
+
* ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
|
|
101
|
+
* 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
|
|
102
|
+
* 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
|
|
103
|
+
* (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
|
|
104
|
+
* 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
|
|
105
|
+
*
|
|
106
|
+
* ⭐ 改挂`threeUtils.addWork` 的三个好处:
|
|
107
|
+
* ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
|
|
108
|
+
* ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
|
|
109
|
+
* ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
|
|
110
|
+
* 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
|
|
111
|
+
*/
|
|
85
112
|
private _uploadPass;
|
|
86
|
-
/**
|
|
87
|
-
* ⚠️
|
|
88
|
-
*
|
|
89
|
-
*
|
|
113
|
+
/** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
|
|
114
|
+
* ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
|
|
115
|
+
* 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
|
|
116
|
+
* 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
|
|
117
|
+
* (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
|
|
90
118
|
private _uploadTile;
|
|
91
119
|
/** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(16 格里的一格)。
|
|
92
120
|
* ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 16 帧**,
|
|
@@ -76,8 +76,15 @@
|
|
|
76
76
|
*/
|
|
77
77
|
import { ClampToEdgeWrapping, DataTexture, LinearFilter, LinearMipmapLinearFilter, Mesh, NoBlending, OrthographicCamera, PlaneGeometry, RGBAFormat, Scene, ShaderMaterial, SRGBColorSpace, UnsignedByteType, Vector3, Vector4, WebGLRenderTarget, } from "three";
|
|
78
78
|
import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
|
|
79
|
+
// ⭐ 2026-10-09 皇上旨「上传也走 threeUtils.addWork 队列」:上传节流改为挂公共队列,见 `_uploadPass`。
|
|
80
|
+
import threeUtils from "../threeUtils";
|
|
79
81
|
import { MISSING_TILE_GRAY, RT_STITCH_CELLS_PER_SLOT, RT_STITCH_SLOT_COUNT, RT_STITCH_SLOT_H, RT_STITCH_SLOT_TILES, RT_STITCH_SLOT_W, RT_STITCH_SLOT_GRID, RT_STITCH_TILES_PER_BATCH, rtStitchBatchOf, rtStitchCellPos, rtStitchTileInset, } from "./mktShader";
|
|
80
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* ⛔⛔ 2026-10-09 皇上旨:「每帧最多几张新纹理进 GPU」这条**节流已不再由此常量实现**,改由
|
|
84
|
+
* `threeUtils.addWork` 队列承担(见 `_uploadPass`)—— 因为函数内局部 budget 管不住「一帧多次 pump」。
|
|
85
|
+
* 此常量保留仅作**文档锚点**,标出「一帧一张」这个不可回退的设计承诺(改队列实现前先看这里)。
|
|
86
|
+
* ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
|
|
87
|
+
*/
|
|
81
88
|
const PANO_STITCH_UPLOADS_PER_FRAME = 1;
|
|
82
89
|
/** 每帧最多发起几张下载(32 片一批,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次解码) */
|
|
83
90
|
const PANO_STITCH_DOWNLOADS_PER_FRAME = 2;
|
|
@@ -141,6 +148,12 @@ export class PanoTileStitcher {
|
|
|
141
148
|
_retryAt = new Map();
|
|
142
149
|
_fails = new Map();
|
|
143
150
|
_dead = new Set();
|
|
151
|
+
/** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
|
|
152
|
+
* ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
|
|
153
|
+
* 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
|
|
154
|
+
* —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
|
|
155
|
+
* 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
|
|
156
|
+
_uploadQueued = new Set();
|
|
144
157
|
/** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
|
|
145
158
|
_renderer = null;
|
|
146
159
|
/** 推进计数(`progressStamp`)*/
|
|
@@ -198,10 +211,24 @@ export class PanoTileStitcher {
|
|
|
198
211
|
}
|
|
199
212
|
/** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
|
|
200
213
|
* RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
|
|
201
|
-
peek(batch, slot) {
|
|
214
|
+
peek(batch, slot, renderer) {
|
|
202
215
|
const g = this._groups.get(batch);
|
|
203
|
-
if (!g
|
|
216
|
+
if (!g)
|
|
204
217
|
return null;
|
|
218
|
+
if (!g.drawnCount[slot]) {
|
|
219
|
+
// ⭐ 皇上 2026-10-09 旨「切换全景时上一个全景的内容要渲染成深灰」:
|
|
220
|
+
// 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
|
|
221
|
+
// **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(皇上报的现象)。
|
|
222
|
+
// 这里改为:只要调用方给了 renderer,就**先把这张 RT 建出来**(`_ensureRt` 建完立刻
|
|
223
|
+
// 整张涂 `MISSING_TILE_GRAY` 缺失灰)并交出去 ⇒ 切换瞬间该象限就是**深灰底**,
|
|
224
|
+
// 随后随`pump` 逐格显影。观感 = 深灰 → 逐格填图,不再闪上一个全景的内容。
|
|
225
|
+
// ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
|
|
226
|
+
if (!renderer)
|
|
227
|
+
return null;
|
|
228
|
+
if (!this._renderer)
|
|
229
|
+
return null;
|
|
230
|
+
return this._ensureRt(this._renderer, g, slot).texture;
|
|
231
|
+
}
|
|
205
232
|
const rt = g.rts[slot];
|
|
206
233
|
return rt ? rt.texture : null;
|
|
207
234
|
}
|
|
@@ -247,6 +274,7 @@ export class PanoTileStitcher {
|
|
|
247
274
|
for (const batch of [...this._groups.keys()])
|
|
248
275
|
this.disposeGroup(batch);
|
|
249
276
|
this._texs.clear();
|
|
277
|
+
this._uploadQueued.clear(); // ⭐ 2026-10-09 旨:同上,连队列里那批未执行的项一并作废
|
|
250
278
|
this._wanted.clear();
|
|
251
279
|
this._retryAt.clear();
|
|
252
280
|
this._fails.clear();
|
|
@@ -323,8 +351,28 @@ export class PanoTileStitcher {
|
|
|
323
351
|
}
|
|
324
352
|
/** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
|
|
325
353
|
* 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
|
|
354
|
+
/**
|
|
355
|
+
* 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
|
|
356
|
+
*
|
|
357
|
+
* ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
|
|
358
|
+
* 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
|
|
359
|
+
* 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
|
|
360
|
+
* (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
|
|
361
|
+
* 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
|
|
362
|
+
*
|
|
363
|
+
* ⭐ 改挂`threeUtils.addWork` 的三个好处:
|
|
364
|
+
* ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
|
|
365
|
+
* ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
|
|
366
|
+
* ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
|
|
367
|
+
* 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
|
|
368
|
+
*/
|
|
326
369
|
_uploadPass(renderer) {
|
|
327
|
-
|
|
370
|
+
// ⛔⛔ 队列里已有待上传项 ⇒ **本次直接不排**。
|
|
371
|
+
// 这一句是本改法的关键闸门:若只靠下面 `has(rec)` 跳过「同一片」,遍历会**继续往下找下一片**
|
|
372
|
+
// 于是 pump 几次就把多张全排进队列(13 全景 × 32 片 = 416 项 ⇒ workList 膨胀、`includes` 退化成 O(n)、
|
|
373
|
+
// 队尾那张要等几百帧才传完)。先判「队列非空就收手」⇒ 队列恒 ≤2 项(一占位+一真活),延迟最低。
|
|
374
|
+
if (this._uploadQueued.size > 0)
|
|
375
|
+
return;
|
|
328
376
|
for (const g of this._groups.values()) {
|
|
329
377
|
if (g.pending <= 0)
|
|
330
378
|
continue;
|
|
@@ -339,18 +387,25 @@ export class PanoTileStitcher {
|
|
|
339
387
|
const rec = this._texs.get(url);
|
|
340
388
|
if (!rec || !rec.tex || rec.uploaded)
|
|
341
389
|
continue;
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
|
|
390
|
+
// ⛔ 已在队列里 ⇒ 跳过(`addWork` 按引用判重对「新闭包」无效 ⇒ 必须自备 Set,见 `_uploadQueued`)
|
|
391
|
+
if (this._uploadQueued.has(rec))
|
|
392
|
+
continue;
|
|
393
|
+
this._uploadQueued.add(rec);
|
|
394
|
+
threeUtils.addWork(() => { }); // 占一格(什么都不干),把真活推到下一个 tick
|
|
395
|
+
threeUtils.addWork(() => this._uploadTile(rec));
|
|
396
|
+
return; // ⛔ 本次 pump 只排一张:其余名额留给下一帧的队列,绝不在这一帧再排第二张
|
|
345
397
|
}
|
|
346
398
|
}
|
|
347
399
|
}
|
|
348
400
|
}
|
|
349
|
-
/**
|
|
350
|
-
* ⚠️
|
|
351
|
-
*
|
|
352
|
-
*
|
|
401
|
+
/** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
|
|
402
|
+
* ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
|
|
403
|
+
* 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
|
|
404
|
+
* 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
|
|
405
|
+
* (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
|
|
353
406
|
_uploadTile(rec) {
|
|
407
|
+
if (!this._uploadQueued.delete(rec))
|
|
408
|
+
return;
|
|
354
409
|
const renderer = this._renderer;
|
|
355
410
|
if (!renderer || !rec.tex)
|
|
356
411
|
return;
|
|
@@ -477,6 +532,8 @@ export class PanoTileStitcher {
|
|
|
477
532
|
for (const batch of [...this._groups.keys()])
|
|
478
533
|
this.disposeGroup(batch);
|
|
479
534
|
this._texs.clear();
|
|
535
|
+
// ⭐ 2026-10-09 旨:在册组整份作废 ⇒ 上传队列里那些项的 rec 也一并作废(否则会拿旧 renderer 上传)
|
|
536
|
+
this._uploadQueued.clear();
|
|
480
537
|
this._wanted.clear();
|
|
481
538
|
this._retryAt.clear();
|
|
482
539
|
this._fails.clear();
|
|
@@ -510,6 +567,9 @@ export class PanoTileStitcher {
|
|
|
510
567
|
const rec = this._texs.get(url);
|
|
511
568
|
if (rec && rec.tex)
|
|
512
569
|
releasePanoTexture(rec.tex);
|
|
570
|
+
// ⭐ 2026-10-09 旨:已排进上传队列的那一片也要**作废**(否则队列回调会对已释放纹理 initTexture)
|
|
571
|
+
if (rec)
|
|
572
|
+
this._uploadQueued.delete(rec);
|
|
513
573
|
this._texs.delete(url);
|
|
514
574
|
this._wanted.delete(url);
|
|
515
575
|
}
|
|
@@ -96,6 +96,8 @@ import {
|
|
|
96
96
|
type WebGLRenderer,
|
|
97
97
|
} from "three";
|
|
98
98
|
import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
|
|
99
|
+
// ⭐ 2026-10-09 皇上旨「上传也走 threeUtils.addWork 队列」:上传节流改为挂公共队列,见 `_uploadPass`。
|
|
100
|
+
import threeUtils from "../threeUtils";
|
|
99
101
|
import {
|
|
100
102
|
MISSING_TILE_GRAY,
|
|
101
103
|
RT_STITCH_CELLS_PER_SLOT,
|
|
@@ -110,7 +112,12 @@ import {
|
|
|
110
112
|
rtStitchTileInset,
|
|
111
113
|
} from "./mktShader";
|
|
112
114
|
|
|
113
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* ⛔⛔ 2026-10-09 皇上旨:「每帧最多几张新纹理进 GPU」这条**节流已不再由此常量实现**,改由
|
|
117
|
+
* `threeUtils.addWork` 队列承担(见 `_uploadPass`)—— 因为函数内局部 budget 管不住「一帧多次 pump」。
|
|
118
|
+
* 此常量保留仅作**文档锚点**,标出「一帧一张」这个不可回退的设计承诺(改队列实现前先看这里)。
|
|
119
|
+
* ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
|
|
120
|
+
*/
|
|
114
121
|
const PANO_STITCH_UPLOADS_PER_FRAME = 1;
|
|
115
122
|
|
|
116
123
|
/** 每帧最多发起几张下载(32 片一批,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次解码) */
|
|
@@ -199,8 +206,14 @@ interface StitchGroup {
|
|
|
199
206
|
export interface PanoStitcher {
|
|
200
207
|
/** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做) */
|
|
201
208
|
ensure(urls: string[]): void;
|
|
202
|
-
/**
|
|
203
|
-
|
|
209
|
+
/**
|
|
210
|
+
* 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
|
|
211
|
+
* ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
|
|
212
|
+
* 先把 RT 建出来(建完即整张涂 `MISSING_TILE_GRAY` 缺失灰)并返回它** ⇒ 调用方切批次的那一帧
|
|
213
|
+
* 拿到的就是**深灰底**而非 null(null 会让它沿用上一批 ⇒ 墙上闪上一个全景)。
|
|
214
|
+
* ⛔ 没传 `renderer`(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
|
|
215
|
+
*/
|
|
216
|
+
peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
|
|
204
217
|
/** 每帧驱动(消费方每帧只调一次)。`renderer` = 调用方当前的 WebGLRenderer,还没捕获到就传 null
|
|
205
218
|
* ⇒ 这一帧什么都不画(⛔ 不打假读数,也⛔ 不退回任何 CPU 拼合路径)。 */
|
|
206
219
|
pump(renderer: WebGLRenderer | null): void;
|
|
@@ -230,6 +243,12 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
230
243
|
private _retryAt: Map<string, number> = new Map();
|
|
231
244
|
private _fails: Map<string, number> = new Map();
|
|
232
245
|
private _dead: Set<string> = new Set();
|
|
246
|
+
/** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
|
|
247
|
+
* ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
|
|
248
|
+
* 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
|
|
249
|
+
* —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
|
|
250
|
+
* 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
|
|
251
|
+
private _uploadQueued: Set<TileRec> = new Set();
|
|
233
252
|
/** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
|
|
234
253
|
private _renderer: WebGLRenderer | null = null;
|
|
235
254
|
/** 推进计数(`progressStamp`)*/
|
|
@@ -288,9 +307,21 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
288
307
|
|
|
289
308
|
/** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
|
|
290
309
|
* RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
|
|
291
|
-
peek(batch: string, slot: number): Texture | null {
|
|
310
|
+
peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null {
|
|
292
311
|
const g = this._groups.get(batch);
|
|
293
|
-
if (!g
|
|
312
|
+
if (!g) return null;
|
|
313
|
+
if (!g.drawnCount[slot]) {
|
|
314
|
+
// ⭐ 皇上 2026-10-09 旨「切换全景时上一个全景的内容要渲染成深灰」:
|
|
315
|
+
// 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
|
|
316
|
+
// **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(皇上报的现象)。
|
|
317
|
+
// 这里改为:只要调用方给了 renderer,就**先把这张 RT 建出来**(`_ensureRt` 建完立刻
|
|
318
|
+
// 整张涂 `MISSING_TILE_GRAY` 缺失灰)并交出去 ⇒ 切换瞬间该象限就是**深灰底**,
|
|
319
|
+
// 随后随`pump` 逐格显影。观感 = 深灰 → 逐格填图,不再闪上一个全景的内容。
|
|
320
|
+
// ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
|
|
321
|
+
if (!renderer) return null;
|
|
322
|
+
if (!this._renderer) return null;
|
|
323
|
+
return this._ensureRt(this._renderer, g, slot).texture;
|
|
324
|
+
}
|
|
294
325
|
const rt = g.rts[slot];
|
|
295
326
|
return rt ? rt.texture : null;
|
|
296
327
|
}
|
|
@@ -338,6 +369,7 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
338
369
|
dispose(): void {
|
|
339
370
|
for (const batch of [...this._groups.keys()]) this.disposeGroup(batch);
|
|
340
371
|
this._texs.clear();
|
|
372
|
+
this._uploadQueued.clear(); // ⭐ 2026-10-09 旨:同上,连队列里那批未执行的项一并作废
|
|
341
373
|
this._wanted.clear();
|
|
342
374
|
this._retryAt.clear();
|
|
343
375
|
this._fails.clear();
|
|
@@ -408,8 +440,27 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
408
440
|
|
|
409
441
|
/** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
|
|
410
442
|
* 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
|
|
443
|
+
/**
|
|
444
|
+
* 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
|
|
445
|
+
*
|
|
446
|
+
* ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
|
|
447
|
+
* 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
|
|
448
|
+
* 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
|
|
449
|
+
* (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
|
|
450
|
+
* 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
|
|
451
|
+
*
|
|
452
|
+
* ⭐ 改挂`threeUtils.addWork` 的三个好处:
|
|
453
|
+
* ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
|
|
454
|
+
* ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
|
|
455
|
+
* ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
|
|
456
|
+
* 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
|
|
457
|
+
*/
|
|
411
458
|
private _uploadPass(renderer: WebGLRenderer): void {
|
|
412
|
-
|
|
459
|
+
// ⛔⛔ 队列里已有待上传项 ⇒ **本次直接不排**。
|
|
460
|
+
// 这一句是本改法的关键闸门:若只靠下面 `has(rec)` 跳过「同一片」,遍历会**继续往下找下一片**
|
|
461
|
+
// 于是 pump 几次就把多张全排进队列(13 全景 × 32 片 = 416 项 ⇒ workList 膨胀、`includes` 退化成 O(n)、
|
|
462
|
+
// 队尾那张要等几百帧才传完)。先判「队列非空就收手」⇒ 队列恒 ≤2 项(一占位+一真活),延迟最低。
|
|
463
|
+
if (this._uploadQueued.size > 0) return;
|
|
413
464
|
for (const g of this._groups.values()) {
|
|
414
465
|
if (g.pending <= 0) continue;
|
|
415
466
|
for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
|
|
@@ -420,18 +471,24 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
420
471
|
if (!url || this._dead.has(url)) continue;
|
|
421
472
|
const rec = this._texs.get(url);
|
|
422
473
|
if (!rec || !rec.tex || rec.uploaded) continue;
|
|
423
|
-
|
|
424
|
-
if (
|
|
474
|
+
// ⛔ 已在队列里 ⇒ 跳过(`addWork` 按引用判重对「新闭包」无效 ⇒ 必须自备 Set,见 `_uploadQueued`)
|
|
475
|
+
if (this._uploadQueued.has(rec)) continue;
|
|
476
|
+
this._uploadQueued.add(rec);
|
|
477
|
+
threeUtils.addWork(() => {}); // 占一格(什么都不干),把真活推到下一个 tick
|
|
478
|
+
threeUtils.addWork(() => this._uploadTile(rec));
|
|
479
|
+
return; // ⛔ 本次 pump 只排一张:其余名额留给下一帧的队列,绝不在这一帧再排第二张
|
|
425
480
|
}
|
|
426
481
|
}
|
|
427
482
|
}
|
|
428
483
|
}
|
|
429
484
|
|
|
430
|
-
/**
|
|
431
|
-
* ⚠️
|
|
432
|
-
*
|
|
433
|
-
*
|
|
485
|
+
/** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
|
|
486
|
+
* ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
|
|
487
|
+
* 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
|
|
488
|
+
* 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
|
|
489
|
+
* (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
|
|
434
490
|
private _uploadTile(rec: TileRec): void {
|
|
491
|
+
if (!this._uploadQueued.delete(rec)) return;
|
|
435
492
|
const renderer = this._renderer;
|
|
436
493
|
if (!renderer || !rec.tex) return;
|
|
437
494
|
try {
|
|
@@ -562,6 +619,8 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
562
619
|
this._renderer = renderer;
|
|
563
620
|
for (const batch of [...this._groups.keys()]) this.disposeGroup(batch);
|
|
564
621
|
this._texs.clear();
|
|
622
|
+
// ⭐ 2026-10-09 旨:在册组整份作废 ⇒ 上传队列里那些项的 rec 也一并作废(否则会拿旧 renderer 上传)
|
|
623
|
+
this._uploadQueued.clear();
|
|
565
624
|
this._wanted.clear();
|
|
566
625
|
this._retryAt.clear();
|
|
567
626
|
this._fails.clear();
|
|
@@ -595,6 +654,8 @@ export class PanoTileStitcher implements PanoStitcher {
|
|
|
595
654
|
private _dropTile(url: string): void {
|
|
596
655
|
const rec = this._texs.get(url);
|
|
597
656
|
if (rec && rec.tex) releasePanoTexture(rec.tex);
|
|
657
|
+
// ⭐ 2026-10-09 旨:已排进上传队列的那一片也要**作废**(否则队列回调会对已释放纹理 initTexture)
|
|
658
|
+
if (rec) this._uploadQueued.delete(rec);
|
|
598
659
|
this._texs.delete(url);
|
|
599
660
|
this._wanted.delete(url);
|
|
600
661
|
}
|