@driftengine/core 3.62.0 → 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 +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +28 -5
- package/dist/render/backend/webgpu/surfaceTexturePass.js +70 -48
- package/package.json +3 -3
- package/src/index.ts +15 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +95 -52
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
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
|
+
},
|
|
2
13
|
{
|
|
3
14
|
"version": "3.62.0",
|
|
4
15
|
"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
|
-
|
|
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.
|
|
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.
|
|
33
|
-
"@driftengine/physics": "3.
|
|
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
|
-
|
|
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
|
}
|