@driftengine/core 3.61.4 → 3.63.0

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/CHANGELOG.json CHANGED
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "3.63.0",
4
+ "date": "2026-09-14",
5
+ "entries": [
6
+ {
7
+ "kind": "added",
8
+ "short": "A sprite sheet can carry a mip chain. Type baked into an atlas and drawn small was losing strokes, so a t read as an l and the same letter read differently in different words.",
9
+ "text": "**`mipmap` on `SpriteTextureOptions`, false by default.** A sprite sheet is usually pixel art, where a chain is memory nothing samples and a blur nobody asked for, so nothing changes for a sheet that does not ask. A glyph page is the sheet where that default is wrong, and the comment on `filter` said as much without offering a way out: it assumed a sheet that is not pixel art \"is normally drawn near its authored size, where the two filters differ by very little\".\n\nA consumer bakes one page per weight at 96 px and draws body copy at 11 to 13, which is about a **7x** minification. `linear` reads four texels out of a footprint covering dozens, so a `t` crossbar two texels tall lands on roughly a quarter of a pixel and survives or not depending on where the sample falls. A player reported it as `Step-In Uppercut` reading `Slep-In Uppercul` — and because the sample point moves with the glyph's position, the same letter survived in one word and not the next, which made the line look unevenly spaced as well as misread.\n\nNeither workaround was worth having: baking nearer the drawn size blurs the headings, because one atlas cannot serve the 8x range between body copy and a display size, and a second atlas for small text doubles the pages and the uploads and moves a sampling decision into the game where it has to be re-tuned whenever a size changes.\n\n**Measured rather than asserted.** `demo/dev/glyphMip.ts` draws one glyph twelve times at deliberately different subpixel offsets, through a plain sampler and through a chain, because the reported symptom is not missing ink but *inconsistent* ink — a build that samples well by luck passes an ink threshold and fails this. The copies' ink has a standard deviation of **0.0130** plain and **0.0020** mipmapped: **6.6x less** disagreement, and the two backends land on the same number to four decimal places. `scripts/glyph-mip-check.mjs` is that measurement as a gate, and it reddens with four failures when the chain is disabled.\n\nWith `filter: 'linear'` this is trilinear; with `filter: 'nearest'` the levels are still blended, because that is minification and `filter` is about magnification, which is the split `SurfaceTexture` already makes. Padding cells against bleed stays the caller's problem, since how much depends on how far down the sheet is ever sampled.\n\n**WebGPU has no `generateMipmap`**, so `generateMipChain` and `mipLevelCount` are public now: the per-level blit that `SurfaceTexture` has always carried is a free function rather than a private method, and `ui2d` calls it. Writing the blit a second time there would have been a second shader to keep in step, and the copy that quietly disagrees about the colour space is the one that ships. It is the first runtime import `ui2d` takes from the engine and it was measured before it was taken: bundled alone, **11,777 against 11,163 gzipped bytes**, so 614 for the blit and its shader."
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "version": "3.62.0",
15
+ "date": "2026-09-13",
16
+ "entries": [
17
+ {
18
+ "kind": "added",
19
+ "short": "An Android build can ask for the permissions it needs. Without this an APK could not open a socket at all, and there was no way for a game to say otherwise short of vendoring three directories.",
20
+ "text": "**The Android manifest template declares one activity and no permissions, and that default is right**: a permission is visible in a store listing, and a packager that granted the network to every game it ever built would be asking on behalf of games that never use it. The difficulty was that a consumer had no way to depart from it. The template is copied wholesale, none of the eight `-Pdrift*` properties Gradle takes reaches the manifest, there is no `manifestPlaceholders` and no source-set overlay, and `--resources` moves the desktop shell and the iOS spec along with the Gradle project — so changing one line of one manifest meant vendoring all three.\n\nSo an online game shipped an APK whose process is not permitted to open a socket. Every connection failed, the game reported that it could not connect, and the relay logged nothing at all because no packet left the phone: the same silence at both ends, indistinguishable from a wrong address. Measured on the built artifact, `aapt2 dump permissions` printed the package name and nothing else.\n\n```json\n\"android\": { \"permissions\": [\"INTERNET\"], \"cleartextTraffic\": false }\n```\n\n`\"INTERNET\"` and `\"android.permission.INTERNET\"` are both accepted; a name that would break out of the XML attribute it is written into is refused at parse time. The elements are written into the copied Gradle project before Gradle runs, which is where the icon is already written, so nothing in the engine's own tree is touched by a consumer's build. `doctor` prints what was asked for beside the signing mode, and says `none — the APK cannot open a socket` when nothing was.\n\n**`cleartextTraffic` is the second half, and it is the one only the engine could answer.** The game is served from `https://appassets.androidplatform.net`, which is a secure context, and a WebView defaults to `MIXED_CONTENT_NEVER_ALLOW` — stricter than a browser tab, where the same insecure WebSocket connects with a deprecation warning rather than being refused, measured on Chrome 151 against a LAN address. Setting it true writes `android:usesCleartextTraffic=\"true\"` **and** is what `MainActivity` reads back through `NetworkSecurityPolicy` to allow mixed content in the WebView. One switch drives both, so the platform and the renderer cannot disagree about it, and a network security config a consumer adds later is picked up without the Java learning about it. A relay on a LAN cannot hold a certificate, which is the case it exists for; a public relay should be `wss://` and this should stay false.\n\nVerified on real APKs with `aapt2`: unchanged, the dump is the package name alone; asking, it carries both permissions and the cleartext flag. Held by twenty-nine cases, and seven mutations each redden at least one."
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "3.61.4",
4
26
  "date": "2026-09-13",
package/dist/index.d.ts CHANGED
@@ -64,6 +64,7 @@ export { createVisitResult, visitVisible } from './scene/visit.ts';
64
64
  export type { VisitResult } from './scene/visit.ts';
65
65
  export type { PassContext, PassDefinition, PassDevice, PassHandle, PrepareContext, } from './render/pass.ts';
66
66
  export type { PassAttachmentOptions, PassTarget } from './render/passTarget.ts';
67
+ export { generateMipChain, mipLevelCount, type MipPipelines, } from './render/backend/webgpu/surfaceTexturePass.ts';
67
68
  export { createPassAttachment } from './render/passTarget.ts';
68
69
  export type { FrameResource } from './render/frame/index.ts';
69
70
  export type { OcclusionOptions } from './render/occlusion.ts';
package/dist/index.js CHANGED
@@ -67,6 +67,17 @@ export { apparentSize, lodForBounds } from './render/lod.js';
67
67
  export { SceneNode } from './scene/node.js';
68
68
  export { MAX_JOINTS, MAX_MORPH_TARGETS, SKIN_PALETTE_TEXTURE_UNIT, morphTexelIndex, paletteTextureWidth, } from './render/skinPalette.js';
69
69
  export { createVisitResult, visitVisible } from './scene/visit.js';
70
+ /*
71
+ * The mip chain a pass has to build for itself on WebGPU, and the level count it needs to ask for.
72
+ *
73
+ * **Public because `gl.generateMipmap` has no WebGPU equivalent and every contributed pass that
74
+ * uploads a texture meets that the moment its texture is ever minified.** `ui2d` met it with a
75
+ * glyph atlas baked at 96 px and drawn at 11; writing the blit a second time there would have been
76
+ * a second WGSL shader to keep in step with this one, and the copy that quietly disagrees about
77
+ * the colour space is the one that would ship. `MipPipelines` is an interface rather than the
78
+ * renderer's cache so a pass can hand over a `Map` and nothing more.
79
+ */
80
+ export { generateMipChain, mipLevelCount, } from './render/backend/webgpu/surfaceTexturePass.js';
70
81
  export { createPassAttachment } from './render/passTarget.js';
71
82
  export { OcclusionBuffer } from './render/occlusion.js';
72
83
  /*
@@ -2,6 +2,34 @@ import type { SurfaceTextureOptions } from '../../surfaceTexture.ts';
2
2
  import type { PipelineCache } from './pipelineCache.ts';
3
3
  /** How many levels an image of this size has, counting the one it arrives with. */
4
4
  export declare function mipLevelCount(width: number, height: number): number;
5
+ /**
6
+ * Somewhere to keep one pipeline per format. `PipelineCache` satisfies it; so does a `Map`.
7
+ *
8
+ * Taken as an interface rather than as the class, because the other caller of the chain below is
9
+ * `@driftengine/ui2d`, which builds its own pipeline from the device and has no cache of the
10
+ * renderer's to hand.
11
+ */
12
+ export interface MipPipelines {
13
+ get(key: string, describe: () => GPURenderPipelineDescriptor): GPURenderPipeline;
14
+ }
15
+ /**
16
+ * Fill every level of a texture from the one above it, which is what WebGPU asks for instead of a
17
+ * call.
18
+ *
19
+ * **`gl.generateMipmap` is one line and WebGPU has no equivalent at all**, so this is the whole of
20
+ * the difference between the two backends for any texture that wants a chain. One encoder, one
21
+ * render pass per level, submitted together, once, at upload.
22
+ *
23
+ * The source view is a **single level**, so the sample cannot read the level being written — that
24
+ * would be one texture bound as an attachment and as a resource at once, which is rejected, and
25
+ * rejected quietly enough to matter.
26
+ *
27
+ * **A free function because there are two callers now.** A surface texture on a mesh has wanted a
28
+ * chain since it existed; a sprite sheet wanted one the day a consumer baked type into an atlas and
29
+ * drew it at a seventh of its authored size. Writing the blit twice would be two statements of one
30
+ * thing, and the second copy is the one that would quietly disagree about the format.
31
+ */
32
+ export declare function generateMipChain(device: GPUDevice, pipelines: MipPipelines, texture: GPUTexture, format: GPUTextureFormat, levels: number): void;
5
33
  /** An image uploaded once and bound per material. */
6
34
  export declare class GpuSurfaceTexture {
7
35
  private readonly device;
@@ -33,9 +61,4 @@ export declare class GpuSurfaceTexture {
33
61
  * an attachment and a resource at once — rejected, and rejected silently enough to matter.
34
62
  */
35
63
  private generateMips;
36
- /**
37
- * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting
38
- * a linear chain through a pipeline declared for the other one is a gamma error per level.
39
- */
40
- private mipPipeline;
41
64
  }
@@ -48,6 +48,75 @@ fn fragmentMain(in: Varying) -> @location(0) vec4<f32> {
48
48
  export function mipLevelCount(width, height) {
49
49
  return Math.floor(Math.log2(Math.max(width, height))) + 1;
50
50
  }
51
+ /**
52
+ * Fill every level of a texture from the one above it, which is what WebGPU asks for instead of a
53
+ * call.
54
+ *
55
+ * **`gl.generateMipmap` is one line and WebGPU has no equivalent at all**, so this is the whole of
56
+ * the difference between the two backends for any texture that wants a chain. One encoder, one
57
+ * render pass per level, submitted together, once, at upload.
58
+ *
59
+ * The source view is a **single level**, so the sample cannot read the level being written — that
60
+ * would be one texture bound as an attachment and as a resource at once, which is rejected, and
61
+ * rejected quietly enough to matter.
62
+ *
63
+ * **A free function because there are two callers now.** A surface texture on a mesh has wanted a
64
+ * chain since it existed; a sprite sheet wanted one the day a consumer baked type into an atlas and
65
+ * drew it at a seventh of its authored size. Writing the blit twice would be two statements of one
66
+ * thing, and the second copy is the one that would quietly disagree about the format.
67
+ */
68
+ export function generateMipChain(device, pipelines, texture, format, levels) {
69
+ if (levels < 2)
70
+ return;
71
+ const pipeline = mipBlitPipeline(device, pipelines, format);
72
+ const sampler = device.createSampler({
73
+ label: 'mip.sampler',
74
+ magFilter: 'linear',
75
+ minFilter: 'linear',
76
+ });
77
+ const encoder = device.createCommandEncoder({ label: 'mip.chain' });
78
+ for (let level = 1; level < levels; level++) {
79
+ const source = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
80
+ const target = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
81
+ const pass = encoder.beginRenderPass({
82
+ label: `mip.level${level}`,
83
+ colorAttachments: [
84
+ { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
85
+ ],
86
+ });
87
+ pass.setPipeline(pipeline);
88
+ pass.setBindGroup(0, device.createBindGroup({
89
+ layout: pipeline.getBindGroupLayout(0),
90
+ entries: [
91
+ { binding: 0, resource: source },
92
+ { binding: 1, resource: sampler },
93
+ ],
94
+ }));
95
+ pass.draw(3);
96
+ pass.end();
97
+ }
98
+ device.queue.submit([encoder.finish()]);
99
+ }
100
+ /**
101
+ * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting a
102
+ * linear chain through a pipeline declared for the other one is a gamma error per level.
103
+ */
104
+ function mipBlitPipeline(device, pipelines, format) {
105
+ return pipelines.get(`surface.mip:${format}`, () => ({
106
+ label: `surface.mip:${format}`,
107
+ layout: 'auto',
108
+ vertex: {
109
+ module: shaderModule(device, { label: 'surface.mip', code: MIP_WGSL }),
110
+ entryPoint: 'vertexMain',
111
+ },
112
+ fragment: {
113
+ module: shaderModule(device, { label: 'surface.mip', code: MIP_WGSL }),
114
+ entryPoint: 'fragmentMain',
115
+ targets: [{ format }],
116
+ },
117
+ primitive: { topology: 'triangle-list' },
118
+ }));
119
+ }
51
120
  function sourceSize(source) {
52
121
  const candidate = source;
53
122
  const width = candidate.videoWidth ?? candidate.width ?? 0;
@@ -176,53 +245,6 @@ export class GpuSurfaceTexture {
176
245
  * an attachment and a resource at once — rejected, and rejected silently enough to matter.
177
246
  */
178
247
  generateMips(texture) {
179
- const pipeline = this.mipPipeline();
180
- const sampler = this.device.createSampler({
181
- label: 'surface.mipSampler',
182
- magFilter: 'linear',
183
- minFilter: 'linear',
184
- });
185
- const encoder = this.device.createCommandEncoder({ label: 'surface.mips' });
186
- for (let level = 1; level < this.levels; level++) {
187
- const source = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
188
- const target = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
189
- const pass = encoder.beginRenderPass({
190
- label: `surface.mip${level}`,
191
- colorAttachments: [
192
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
193
- ],
194
- });
195
- pass.setPipeline(pipeline);
196
- pass.setBindGroup(0, this.device.createBindGroup({
197
- layout: pipeline.getBindGroupLayout(0),
198
- entries: [
199
- { binding: 0, resource: source },
200
- { binding: 1, resource: sampler },
201
- ],
202
- }));
203
- pass.draw(3);
204
- pass.end();
205
- }
206
- this.device.queue.submit([encoder.finish()]);
207
- }
208
- /**
209
- * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting
210
- * a linear chain through a pipeline declared for the other one is a gamma error per level.
211
- */
212
- mipPipeline() {
213
- return this.pipelines.get(`surface.mip:${this.format}`, () => ({
214
- label: `surface.mip:${this.format}`,
215
- layout: 'auto',
216
- vertex: {
217
- module: shaderModule(this.device, { label: 'surface.mip', code: MIP_WGSL }),
218
- entryPoint: 'vertexMain',
219
- },
220
- fragment: {
221
- module: shaderModule(this.device, { label: 'surface.mip', code: MIP_WGSL }),
222
- entryPoint: 'fragmentMain',
223
- targets: [{ format: this.format }],
224
- },
225
- primitive: { topology: 'triangle-list' },
226
- }));
248
+ generateMipChain(this.device, this.pipelines, texture, this.format, this.levels);
227
249
  }
228
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "3.61.4",
3
+ "version": "3.63.0",
4
4
  "description": "DriftEngine reusable browser 3D runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "3.61.4",
33
- "@driftengine/physics": "3.61.4",
32
+ "@driftengine/drft": "3.63.0",
33
+ "@driftengine/physics": "3.63.0",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
package/src/index.ts CHANGED
@@ -158,6 +158,21 @@ export type {
158
158
  * write to the frame stays refused while a write to a pass-owned target does not is written out.
159
159
  */
160
160
  export type { PassAttachmentOptions, PassTarget } from './render/passTarget.ts';
161
+ /*
162
+ * The mip chain a pass has to build for itself on WebGPU, and the level count it needs to ask for.
163
+ *
164
+ * **Public because `gl.generateMipmap` has no WebGPU equivalent and every contributed pass that
165
+ * uploads a texture meets that the moment its texture is ever minified.** `ui2d` met it with a
166
+ * glyph atlas baked at 96 px and drawn at 11; writing the blit a second time there would have been
167
+ * a second WGSL shader to keep in step with this one, and the copy that quietly disagrees about
168
+ * the colour space is the one that would ship. `MipPipelines` is an interface rather than the
169
+ * renderer's cache so a pass can hand over a `Map` and nothing more.
170
+ */
171
+ export {
172
+ generateMipChain,
173
+ mipLevelCount,
174
+ type MipPipelines,
175
+ } from './render/backend/webgpu/surfaceTexturePass.ts';
161
176
  export { createPassAttachment } from './render/passTarget.ts';
162
177
  export type { FrameResource } from './render/frame/index.ts';
163
178
  /*
@@ -55,6 +55,100 @@ export function mipLevelCount(width: number, height: number): number {
55
55
  return Math.floor(Math.log2(Math.max(width, height))) + 1;
56
56
  }
57
57
 
58
+ /**
59
+ * Somewhere to keep one pipeline per format. `PipelineCache` satisfies it; so does a `Map`.
60
+ *
61
+ * Taken as an interface rather than as the class, because the other caller of the chain below is
62
+ * `@driftengine/ui2d`, which builds its own pipeline from the device and has no cache of the
63
+ * renderer's to hand.
64
+ */
65
+ export interface MipPipelines {
66
+ get(key: string, describe: () => GPURenderPipelineDescriptor): GPURenderPipeline;
67
+ }
68
+
69
+ /**
70
+ * Fill every level of a texture from the one above it, which is what WebGPU asks for instead of a
71
+ * call.
72
+ *
73
+ * **`gl.generateMipmap` is one line and WebGPU has no equivalent at all**, so this is the whole of
74
+ * the difference between the two backends for any texture that wants a chain. One encoder, one
75
+ * render pass per level, submitted together, once, at upload.
76
+ *
77
+ * The source view is a **single level**, so the sample cannot read the level being written — that
78
+ * would be one texture bound as an attachment and as a resource at once, which is rejected, and
79
+ * rejected quietly enough to matter.
80
+ *
81
+ * **A free function because there are two callers now.** A surface texture on a mesh has wanted a
82
+ * chain since it existed; a sprite sheet wanted one the day a consumer baked type into an atlas and
83
+ * drew it at a seventh of its authored size. Writing the blit twice would be two statements of one
84
+ * thing, and the second copy is the one that would quietly disagree about the format.
85
+ */
86
+ export function generateMipChain(
87
+ device: GPUDevice,
88
+ pipelines: MipPipelines,
89
+ texture: GPUTexture,
90
+ format: GPUTextureFormat,
91
+ levels: number,
92
+ ): void {
93
+ if (levels < 2) return;
94
+ const pipeline = mipBlitPipeline(device, pipelines, format);
95
+ const sampler = device.createSampler({
96
+ label: 'mip.sampler',
97
+ magFilter: 'linear',
98
+ minFilter: 'linear',
99
+ });
100
+ const encoder = device.createCommandEncoder({ label: 'mip.chain' });
101
+ for (let level = 1; level < levels; level++) {
102
+ const source = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
103
+ const target = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
104
+ const pass = encoder.beginRenderPass({
105
+ label: `mip.level${level}`,
106
+ colorAttachments: [
107
+ { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
108
+ ],
109
+ });
110
+ pass.setPipeline(pipeline);
111
+ pass.setBindGroup(
112
+ 0,
113
+ device.createBindGroup({
114
+ layout: pipeline.getBindGroupLayout(0),
115
+ entries: [
116
+ { binding: 0, resource: source },
117
+ { binding: 1, resource: sampler },
118
+ ],
119
+ }),
120
+ );
121
+ pass.draw(3);
122
+ pass.end();
123
+ }
124
+ device.queue.submit([encoder.finish()]);
125
+ }
126
+
127
+ /**
128
+ * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting a
129
+ * linear chain through a pipeline declared for the other one is a gamma error per level.
130
+ */
131
+ function mipBlitPipeline(
132
+ device: GPUDevice,
133
+ pipelines: MipPipelines,
134
+ format: GPUTextureFormat,
135
+ ): GPURenderPipeline {
136
+ return pipelines.get(`surface.mip:${format}`, () => ({
137
+ label: `surface.mip:${format}`,
138
+ layout: 'auto' as const,
139
+ vertex: {
140
+ module: shaderModule(device, { label: 'surface.mip', code: MIP_WGSL }),
141
+ entryPoint: 'vertexMain',
142
+ },
143
+ fragment: {
144
+ module: shaderModule(device, { label: 'surface.mip', code: MIP_WGSL }),
145
+ entryPoint: 'fragmentMain',
146
+ targets: [{ format }],
147
+ },
148
+ primitive: { topology: 'triangle-list' as const },
149
+ }));
150
+ }
151
+
58
152
  function sourceSize(source: TexImageSource): { width: number; height: number } {
59
153
  const candidate = source as {
60
154
  width?: number;
@@ -201,57 +295,6 @@ export class GpuSurfaceTexture {
201
295
  * an attachment and a resource at once — rejected, and rejected silently enough to matter.
202
296
  */
203
297
  private generateMips(texture: GPUTexture): void {
204
- const pipeline = this.mipPipeline();
205
- const sampler = this.device.createSampler({
206
- label: 'surface.mipSampler',
207
- magFilter: 'linear',
208
- minFilter: 'linear',
209
- });
210
- const encoder = this.device.createCommandEncoder({ label: 'surface.mips' });
211
- for (let level = 1; level < this.levels; level++) {
212
- const source = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
213
- const target = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
214
- const pass = encoder.beginRenderPass({
215
- label: `surface.mip${level}`,
216
- colorAttachments: [
217
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
218
- ],
219
- });
220
- pass.setPipeline(pipeline);
221
- pass.setBindGroup(
222
- 0,
223
- this.device.createBindGroup({
224
- layout: pipeline.getBindGroupLayout(0),
225
- entries: [
226
- { binding: 0, resource: source },
227
- { binding: 1, resource: sampler },
228
- ],
229
- }),
230
- );
231
- pass.draw(3);
232
- pass.end();
233
- }
234
- this.device.queue.submit([encoder.finish()]);
235
- }
236
-
237
- /**
238
- * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting
239
- * a linear chain through a pipeline declared for the other one is a gamma error per level.
240
- */
241
- private mipPipeline(): GPURenderPipeline {
242
- return this.pipelines.get(`surface.mip:${this.format}`, () => ({
243
- label: `surface.mip:${this.format}`,
244
- layout: 'auto' as const,
245
- vertex: {
246
- module: shaderModule(this.device, { label: 'surface.mip', code: MIP_WGSL }),
247
- entryPoint: 'vertexMain',
248
- },
249
- fragment: {
250
- module: shaderModule(this.device, { label: 'surface.mip', code: MIP_WGSL }),
251
- entryPoint: 'fragmentMain',
252
- targets: [{ format: this.format }],
253
- },
254
- primitive: { topology: 'triangle-list' as const },
255
- }));
298
+ generateMipChain(this.device, this.pipelines, texture, this.format, this.levels);
256
299
  }
257
300
  }