@swf2es/render 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/README.md +17 -2
- package/dist/commands.d.ts +67 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +93 -0
- package/dist/commands.js.map +1 -0
- package/dist/filter-plan.d.ts +110 -0
- package/dist/filter-plan.d.ts.map +1 -0
- package/dist/filter-plan.js +480 -0
- package/dist/filter-plan.js.map +1 -0
- package/dist/filter-shaders.d.ts +63 -0
- package/dist/filter-shaders.d.ts.map +1 -0
- package/dist/filter-shaders.js +350 -0
- package/dist/filter-shaders.js.map +1 -0
- package/dist/filters.d.ts +107 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +187 -0
- package/dist/filters.js.map +1 -0
- package/dist/geometry.d.ts +83 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +93 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/keeps.d.ts +17 -0
- package/dist/keeps.d.ts.map +1 -0
- package/dist/keeps.js +17 -0
- package/dist/keeps.js.map +1 -0
- package/dist/kernel-shaders.d.ts +74 -0
- package/dist/kernel-shaders.d.ts.map +1 -0
- package/dist/kernel-shaders.js +323 -0
- package/dist/kernel-shaders.js.map +1 -0
- package/dist/measure.d.ts +56 -0
- package/dist/measure.d.ts.map +1 -0
- package/dist/measure.js +287 -0
- package/dist/measure.js.map +1 -0
- package/dist/plan.d.ts +14 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +247 -0
- package/dist/plan.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +87 -0
- package/dist/registry.js.map +1 -0
- package/dist/rows.d.ts +39 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +60 -0
- package/dist/rows.js.map +1 -0
- package/dist/scene.d.ts +179 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +265 -0
- package/dist/scene.js.map +1 -0
- package/dist/shelves.d.ts +23 -0
- package/dist/shelves.d.ts.map +1 -0
- package/dist/shelves.js +63 -0
- package/dist/shelves.js.map +1 -0
- package/dist/textures.d.ts +72 -0
- package/dist/textures.d.ts.map +1 -0
- package/dist/textures.js +160 -0
- package/dist/textures.js.map +1 -0
- package/dist/webgl/atlas.d.ts +63 -0
- package/dist/webgl/atlas.d.ts.map +1 -0
- package/dist/webgl/atlas.js +328 -0
- package/dist/webgl/atlas.js.map +1 -0
- package/dist/webgl/backend.d.ts +141 -0
- package/dist/webgl/backend.d.ts.map +1 -0
- package/dist/webgl/backend.js +659 -0
- package/dist/webgl/backend.js.map +1 -0
- package/dist/webgl/composite.d.ts +101 -0
- package/dist/webgl/composite.d.ts.map +1 -0
- package/dist/webgl/composite.js +293 -0
- package/dist/webgl/composite.js.map +1 -0
- package/dist/webgl/draws.d.ts +213 -0
- package/dist/webgl/draws.d.ts.map +1 -0
- package/dist/webgl/draws.js +714 -0
- package/dist/webgl/draws.js.map +1 -0
- package/dist/webgl/filters.d.ts +107 -0
- package/dist/webgl/filters.d.ts.map +1 -0
- package/dist/webgl/filters.js +351 -0
- package/dist/webgl/filters.js.map +1 -0
- package/dist/webgl/gpu-textures.d.ts +44 -0
- package/dist/webgl/gpu-textures.d.ts.map +1 -0
- package/dist/webgl/gpu-textures.js +178 -0
- package/dist/webgl/gpu-textures.js.map +1 -0
- package/dist/webgl/holds.d.ts +97 -0
- package/dist/webgl/holds.d.ts.map +1 -0
- package/dist/webgl/holds.js +453 -0
- package/dist/webgl/holds.js.map +1 -0
- package/dist/webgl/index.d.ts +2 -0
- package/dist/webgl/index.d.ts.map +1 -0
- package/dist/webgl/index.js +4 -0
- package/dist/webgl/index.js.map +1 -0
- package/dist/webgl/kernels.d.ts +64 -0
- package/dist/webgl/kernels.d.ts.map +1 -0
- package/dist/webgl/kernels.js +160 -0
- package/dist/webgl/kernels.js.map +1 -0
- package/dist/webgl/pool.d.ts +71 -0
- package/dist/webgl/pool.d.ts.map +1 -0
- package/dist/webgl/pool.js +192 -0
- package/dist/webgl/pool.js.map +1 -0
- package/dist/webgl/programs.d.ts +130 -0
- package/dist/webgl/programs.d.ts.map +1 -0
- package/dist/webgl/programs.js +582 -0
- package/dist/webgl/programs.js.map +1 -0
- package/dist/webgl/stage.d.ts +67 -0
- package/dist/webgl/stage.d.ts.map +1 -0
- package/dist/webgl/stage.js +173 -0
- package/dist/webgl/stage.js.map +1 -0
- package/dist/webgl/targets.d.ts +172 -0
- package/dist/webgl/targets.d.ts.map +1 -0
- package/dist/webgl/targets.js +323 -0
- package/dist/webgl/targets.js.map +1 -0
- package/package.json +35 -3
- package/src/commands.ts +98 -0
- package/src/filter-plan.ts +755 -0
- package/src/filter-shaders.ts +360 -0
- package/src/filters.ts +220 -0
- package/src/geometry.ts +143 -0
- package/src/index.ts +87 -0
- package/src/keeps.ts +21 -0
- package/src/kernel-shaders.ts +375 -0
- package/src/measure.ts +400 -0
- package/src/plan.ts +331 -0
- package/src/registry.ts +105 -0
- package/src/rows.ts +93 -0
- package/src/scene.ts +297 -0
- package/src/shelves.ts +70 -0
- package/src/textures.ts +198 -0
- package/src/webgl/atlas.ts +370 -0
- package/src/webgl/backend.ts +973 -0
- package/src/webgl/composite.ts +372 -0
- package/src/webgl/draws.ts +902 -0
- package/src/webgl/filters.ts +502 -0
- package/src/webgl/gpu-textures.ts +253 -0
- package/src/webgl/holds.ts +624 -0
- package/src/webgl/index.ts +3 -0
- package/src/webgl/kernels.ts +205 -0
- package/src/webgl/pool.ts +245 -0
- package/src/webgl/programs.ts +620 -0
- package/src/webgl/stage.ts +214 -0
- package/src/webgl/targets.ts +464 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { GlState } from "@swf2es/context3d/device";
|
|
2
|
+
import { LAYER_UNIT } from "./programs.js";
|
|
3
|
+
import {
|
|
4
|
+
fittingSamples,
|
|
5
|
+
LAYER_IDLE_FRAMES,
|
|
6
|
+
type Layer,
|
|
7
|
+
type Multisampled,
|
|
8
|
+
type TargetPool,
|
|
9
|
+
} from "./targets.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The stage's own targets and their way onto the canvas: multisampled
|
|
13
|
+
* buffers the canvas's size and the texture they resolve into, the layer a
|
|
14
|
+
* frame that clears the stage is drawn into before an opaque canvas, and
|
|
15
|
+
* the copies onto the canvas.
|
|
16
|
+
*/
|
|
17
|
+
export class Stage {
|
|
18
|
+
private readonly gl: WebGL2RenderingContext;
|
|
19
|
+
private readonly state: GlState;
|
|
20
|
+
private readonly pool: TargetPool;
|
|
21
|
+
/** The most samples the GPU takes, and whether the canvas has alpha, asked once. */
|
|
22
|
+
private maxSamples = 1;
|
|
23
|
+
private hasAlpha = false;
|
|
24
|
+
/**
|
|
25
|
+
* The stage's multisampled buffers, and the texture they resolve into before a canvas they may
|
|
26
|
+
* not be resolved onto (present), made as first needed.
|
|
27
|
+
*/
|
|
28
|
+
private stageMsaa: Multisampled | null = null;
|
|
29
|
+
private stageResolved: { framebuffer: WebGLFramebuffer; texture: WebGLTexture } | null = null;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the driver resolves the stage straight onto a canvas with alpha, found as it first does
|
|
32
|
+
* (present): null till then. SwiftShader refuses it.
|
|
33
|
+
*/
|
|
34
|
+
private directResolve: boolean | null = null;
|
|
35
|
+
/** The layer a frame that clears the stage is drawn into before an opaque canvas (stageAlpha). */
|
|
36
|
+
private stageLayer: Layer | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(gl: WebGL2RenderingContext, state: GlState, pool: TargetPool) {
|
|
39
|
+
this.gl = gl;
|
|
40
|
+
this.state = state;
|
|
41
|
+
this.pool = pool;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether the canvas has alpha, as asked of the context in setUp. */
|
|
45
|
+
get canvasAlpha(): boolean {
|
|
46
|
+
return this.hasAlpha;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Nothing made yet, and the GPU and the canvas asked again, as the context comes or comes back. */
|
|
50
|
+
setUp(): void {
|
|
51
|
+
const gl = this.gl;
|
|
52
|
+
this.stageMsaa = null;
|
|
53
|
+
this.stageResolved = null;
|
|
54
|
+
this.directResolve = null;
|
|
55
|
+
this.stageLayer = null;
|
|
56
|
+
this.maxSamples = gl.getParameter(gl.MAX_SAMPLES) as number;
|
|
57
|
+
this.hasAlpha = gl.getContextAttributes()?.alpha ?? false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The stage's multisampled pixels onto the canvas: resolved straight onto one with alpha where
|
|
62
|
+
* the driver lets a resolve go there, else into a texture first, as an opaque canvas's must be, a
|
|
63
|
+
* resolve keeping its format. Whether the driver lets it is found once, by the error it raises.
|
|
64
|
+
*/
|
|
65
|
+
present(msaa: Multisampled): void {
|
|
66
|
+
const gl = this.gl;
|
|
67
|
+
const { width, height } = msaa;
|
|
68
|
+
if (this.hasAlpha && this.directResolve !== false) {
|
|
69
|
+
const finding = this.directResolve === null;
|
|
70
|
+
if (finding) {
|
|
71
|
+
gl.getError();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.toCanvas(msaa.framebuffer, width, height, false);
|
|
75
|
+
if (finding) {
|
|
76
|
+
this.directResolve = gl.getError() !== gl.INVALID_OPERATION;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.directResolve) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolved = this.resolveTarget(width, height);
|
|
85
|
+
this.state.bindFramebuffer(msaa.framebuffer);
|
|
86
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, resolved.framebuffer);
|
|
87
|
+
gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
88
|
+
this.toCanvas(resolved.framebuffer, width, height, false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The texture the stage resolves into before the canvas (present), made as first needed. */
|
|
92
|
+
private resolveTarget(
|
|
93
|
+
width: number,
|
|
94
|
+
height: number,
|
|
95
|
+
): { framebuffer: WebGLFramebuffer; texture: WebGLTexture } {
|
|
96
|
+
if (this.stageResolved) {
|
|
97
|
+
return this.stageResolved;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const gl = this.gl;
|
|
101
|
+
const texture = gl.createTexture() as WebGLTexture;
|
|
102
|
+
this.state.bindTexture(LAYER_UNIT, texture);
|
|
103
|
+
this.state.activeTexture(LAYER_UNIT);
|
|
104
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
105
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
106
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
107
|
+
const framebuffer = gl.createFramebuffer() as WebGLFramebuffer;
|
|
108
|
+
this.state.bindFramebuffer(framebuffer);
|
|
109
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
110
|
+
this.stageResolved = { framebuffer, texture };
|
|
111
|
+
return this.stageResolved;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The first `width` by `height` pixels of `source` copied onto the canvas,
|
|
116
|
+
* upside down where `flip`. Bound through the state cache first and as
|
|
117
|
+
* it was after, so the cache holds what is bound, whatever it held.
|
|
118
|
+
*/
|
|
119
|
+
toCanvas(source: WebGLFramebuffer, width: number, height: number, flip: boolean): void {
|
|
120
|
+
const gl = this.gl;
|
|
121
|
+
this.state.bindFramebuffer(source);
|
|
122
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
123
|
+
const top = flip ? 0 : height;
|
|
124
|
+
gl.blitFramebuffer(
|
|
125
|
+
0,
|
|
126
|
+
0,
|
|
127
|
+
width,
|
|
128
|
+
height,
|
|
129
|
+
0,
|
|
130
|
+
height - top,
|
|
131
|
+
width,
|
|
132
|
+
top,
|
|
133
|
+
gl.COLOR_BUFFER_BIT,
|
|
134
|
+
gl.NEAREST,
|
|
135
|
+
);
|
|
136
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, source);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The layer the canvas' size a frame that clears the stage is drawn into
|
|
141
|
+
* before an opaque canvas (execute): its own, kept apart from the free
|
|
142
|
+
* layers so that none smaller takes it, made again for another size, let
|
|
143
|
+
* go of once idle as they are.
|
|
144
|
+
*/
|
|
145
|
+
stageAlpha(width: number, height: number): Layer {
|
|
146
|
+
const known = this.stageLayer;
|
|
147
|
+
if (known && known.width === width && known.height === height) {
|
|
148
|
+
known.usedAt = this.pool.frame;
|
|
149
|
+
return known;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (known) {
|
|
153
|
+
this.pool.deleteLayer(known);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const layer = this.pool.makeLayer(width, height);
|
|
157
|
+
this.stageLayer = layer;
|
|
158
|
+
return layer;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The stage's multisampled buffers, made again for another size or samples, their resolve's
|
|
163
|
+
* texture let go of; none where it takes no samples.
|
|
164
|
+
*/
|
|
165
|
+
stageBuffers(width: number, height: number, wanted: number): Multisampled | null {
|
|
166
|
+
const samples = fittingSamples(width, height, Math.min(wanted, this.maxSamples), 4);
|
|
167
|
+
const known = this.stageMsaa;
|
|
168
|
+
if (known && known.width === width && known.height === height && known.samples === samples) {
|
|
169
|
+
return known;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.dropStageBuffers();
|
|
173
|
+
const msaa = samples > 1 ? this.pool.multisampled(width, height, samples) : null;
|
|
174
|
+
if (!msaa) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.stageMsaa = msaa;
|
|
179
|
+
return msaa;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The stage's multisampled buffers and their resolve let go of, as samples or size change. */
|
|
183
|
+
dropStageBuffers(): void {
|
|
184
|
+
const gl = this.gl;
|
|
185
|
+
if (this.stageMsaa) {
|
|
186
|
+
this.pool.deleteMultisampled(this.stageMsaa);
|
|
187
|
+
this.stageMsaa = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.stageResolved) {
|
|
191
|
+
this.state.dropTexture(this.stageResolved.texture);
|
|
192
|
+
gl.deleteTexture(this.stageResolved.texture);
|
|
193
|
+
gl.deleteFramebuffer(this.stageResolved.framebuffer);
|
|
194
|
+
this.stageResolved = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The stage's layer (stageAlpha) let go of, undrawn into for LAYER_IDLE_FRAMES. */
|
|
199
|
+
dropIdleLayer(): void {
|
|
200
|
+
const stage = this.stageLayer;
|
|
201
|
+
if (stage && this.pool.frame - stage.usedAt > LAYER_IDLE_FRAMES) {
|
|
202
|
+
this.pool.deleteLayer(stage);
|
|
203
|
+
this.stageLayer = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** What the stage made on the context, deleted. */
|
|
208
|
+
destroy(): void {
|
|
209
|
+
this.dropStageBuffers();
|
|
210
|
+
if (this.stageLayer) {
|
|
211
|
+
this.pool.deleteLayer(this.stageLayer);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import type { GlState } from "@swf2es/context3d/device";
|
|
2
|
+
import type { KernelObject } from "../filter-plan.js";
|
|
3
|
+
import { SizedPool, trimTogether } from "./pool.js";
|
|
4
|
+
import { LAYER_UNIT } from "./programs.js";
|
|
5
|
+
|
|
6
|
+
/** Where a frame is drawn: the target's size in pixels, and how stage pixels map onto it. */
|
|
7
|
+
export interface Target {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
/** Target pixels a stage pixel, and where the stage's origin lies on the target. */
|
|
11
|
+
scale: number;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
/** Target pixels a unit of the target's own, which filters read their input's size in. */
|
|
15
|
+
resolution: number;
|
|
16
|
+
/**
|
|
17
|
+
* Target pixels a side of a pixel of Flash's, which a mesh's texture is
|
|
18
|
+
* read once in: a pixel of the screen, or a BitmapData's draw's sample.
|
|
19
|
+
*/
|
|
20
|
+
pixel: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A target a layer is drawn into: a texture at least its rectangle's size,
|
|
25
|
+
* with a stencil of its own for masks within it. Its rows run down, the
|
|
26
|
+
* first the rectangle's top, as filters read them.
|
|
27
|
+
*/
|
|
28
|
+
export interface Layer {
|
|
29
|
+
framebuffer: WebGLFramebuffer;
|
|
30
|
+
texture: WebGLTexture;
|
|
31
|
+
/** Its stencil, made as it is first drawn into unmultisampled; filters' targets take none. */
|
|
32
|
+
stencil: WebGLRenderbuffer | null;
|
|
33
|
+
/**
|
|
34
|
+
* The size it is used as, which filters read it as a texture of: its
|
|
35
|
+
* texture's, or less where a filter's target takes a larger one (takeLayer).
|
|
36
|
+
*/
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
/** Its texture's size, and its stencil's. */
|
|
40
|
+
textureWidth: number;
|
|
41
|
+
textureHeight: number;
|
|
42
|
+
/** The frame it was last drawn into. */
|
|
43
|
+
usedAt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Multisampled colour and depth-stencil a target is drawn into, its edges
|
|
48
|
+
* covered by `samples` a pixel, then resolved into a texture to be read.
|
|
49
|
+
*/
|
|
50
|
+
export interface Multisampled {
|
|
51
|
+
framebuffer: WebGLFramebuffer;
|
|
52
|
+
color: WebGLRenderbuffer;
|
|
53
|
+
depthStencil: WebGLRenderbuffer;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
samples: number;
|
|
57
|
+
/** The frame it was last drawn into. */
|
|
58
|
+
usedAt: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The most a multisampled target may take, colour and depth-stencil
|
|
63
|
+
* together, 8 bytes a sample, as web's fittingSamples has for the
|
|
64
|
+
* canvas.
|
|
65
|
+
*/
|
|
66
|
+
export const MSAA_BYTES = 256 * 1024 * 1024;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The samples a target of `width` by `height` takes for `samples`, a power
|
|
70
|
+
* of two, as a driver rounds others for colour and depth-stencil alike
|
|
71
|
+
* or not: halved till its buffers fit MSAA_BYTES, none (1) where two would
|
|
72
|
+
* not. A layer goes so far, as one padded as far as a texture goes would
|
|
73
|
+
* take gigabytes; the stage, as web's canvas, keeps 4 at least.
|
|
74
|
+
*/
|
|
75
|
+
export function fittingSamples(width: number, height: number, samples: number, least = 1): number {
|
|
76
|
+
let fitting = 1;
|
|
77
|
+
while (fitting * 2 <= samples) {
|
|
78
|
+
fitting *= 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const floor = Math.min(fitting, least);
|
|
82
|
+
while (fitting > floor && width * height * fitting * 8 > MSAA_BYTES) {
|
|
83
|
+
fitting /= 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return fitting;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The sizes a layer's texture is made in: rounded up to a multiple, so that one serves many. */
|
|
90
|
+
export const LAYER_STEP = 128;
|
|
91
|
+
/**
|
|
92
|
+
* The sizes a small layer's side is made in, under LAYER_STEP: a glow of a
|
|
93
|
+
* few dozen pixels took a texture of 128 a side, and a crowd keeps
|
|
94
|
+
* thousands of them as their filters' outputs.
|
|
95
|
+
*/
|
|
96
|
+
const SMALL_STEP = 16;
|
|
97
|
+
/**
|
|
98
|
+
* The free layers and multisampled buffers kept between frames past what
|
|
99
|
+
* the last frame drew with, the least lately freed going first as a frame
|
|
100
|
+
* ends; and the most kept in all. A stretch of frames each drawing
|
|
101
|
+
* full-screen multisampled layers drew with 150 to 300 MiB a frame, and
|
|
102
|
+
* trimmed to 128 MiB in all, made seven targets a frame again, as many as
|
|
103
|
+
* it let go of.
|
|
104
|
+
*/
|
|
105
|
+
const FREE_BYTES = 128 * 1024 * 1024;
|
|
106
|
+
const WORKING_BYTES = 512 * 1024 * 1024;
|
|
107
|
+
|
|
108
|
+
/** A side of a layer made for `n` pixels: rounded up to SMALL_STEP, past LAYER_STEP to that. */
|
|
109
|
+
export function layerSide(n: number): number {
|
|
110
|
+
return n <= LAYER_STEP
|
|
111
|
+
? Math.max(SMALL_STEP, Math.ceil(n / SMALL_STEP) * SMALL_STEP)
|
|
112
|
+
: Math.ceil(n / LAYER_STEP) * LAYER_STEP;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The bytes multisampled buffers take: colour and depth-stencil, 8 a sample. */
|
|
116
|
+
function msaaBytes(msaa: { width: number; height: number; samples: number }): number {
|
|
117
|
+
return msaa.width * msaa.height * msaa.samples * 8;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The bytes a layer takes: its colour, and its stencil where it has one. */
|
|
121
|
+
function layerBytes(layer: Layer): number {
|
|
122
|
+
return layer.textureWidth * layer.textureHeight * (layer.stencil ? 8 : 4);
|
|
123
|
+
}
|
|
124
|
+
/** Frames a free layer is kept undrawn into before it is let go of. */
|
|
125
|
+
export const LAYER_IDLE_FRAMES = 60;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A target drawn into: its rectangle of the canvas, in pixels from the
|
|
129
|
+
* top left, and the layer it is drawn into, or none for the canvas itself.
|
|
130
|
+
*/
|
|
131
|
+
export interface Open {
|
|
132
|
+
layer: Layer | null;
|
|
133
|
+
/** What it is drawn into while it is multisampled, resolved before it is read. */
|
|
134
|
+
msaa: Multisampled | null;
|
|
135
|
+
x: number;
|
|
136
|
+
y: number;
|
|
137
|
+
width: number;
|
|
138
|
+
height: number;
|
|
139
|
+
/** Where its rectangle's corner is in its framebuffer, a page's (HOLD_PUSH); else 0, 0. */
|
|
140
|
+
ox?: number;
|
|
141
|
+
oy?: number;
|
|
142
|
+
/** Where a whole layer's object lies in it, which its ShaderFilters read (filter-plan.ts). */
|
|
143
|
+
object?: KernelObject | null;
|
|
144
|
+
/** Whether it is opaque throughout, as the stage is (Commands.opaqueStage). */
|
|
145
|
+
opaque?: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A whole layer's object from params `p` from `at` (plan.ts, layerParams): null for none. */
|
|
149
|
+
export function objectOf(p: Float32Array, at: number): KernelObject | null {
|
|
150
|
+
return p[at + 2] < 0 ? null : { x: p[at], y: p[at + 1], width: p[at + 2], height: p[at + 3] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The framebuffer `open` is drawn into: its multisampled one while it has one, else its own. */
|
|
154
|
+
export function framebufferOf(open: Open): WebGLFramebuffer | null {
|
|
155
|
+
return open.msaa?.framebuffer ?? open.layer?.framebuffer ?? null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The backend's render targets: layers and the multisampled buffers they
|
|
160
|
+
* are drawn through, pooled by their size and samples, taken for a frame's
|
|
161
|
+
* layers, given back and let go of once idle; and the layers kept from
|
|
162
|
+
* frame to frame.
|
|
163
|
+
*/
|
|
164
|
+
export class TargetPool {
|
|
165
|
+
private readonly gl: WebGL2RenderingContext;
|
|
166
|
+
private readonly state: GlState;
|
|
167
|
+
/** Frames executed, which a layer's idleness is counted in. */
|
|
168
|
+
private frameCount = 0;
|
|
169
|
+
/**
|
|
170
|
+
* The samples the layers of the frame drawing take: the stage's, which a
|
|
171
|
+
* BitmapData's frame draws with none of.
|
|
172
|
+
*/
|
|
173
|
+
private layerSamples = 1;
|
|
174
|
+
/** Layers free to draw into, by their size. */
|
|
175
|
+
private freeLayers = new SizedPool<Layer>(layerBytes);
|
|
176
|
+
/** Multisampled buffers free to draw a layer into, kept while drawn into often enough. */
|
|
177
|
+
private freeMsaa = new SizedPool<Multisampled>(msaaBytes);
|
|
178
|
+
/** The layers kept from frame to frame, by their ids in Keeps. */
|
|
179
|
+
private kept: (Layer | null)[] = [];
|
|
180
|
+
/** The widest and the tallest multisampled buffers found complete, by their samples. */
|
|
181
|
+
private completeMsaa = new Map<number, { width: number; height: number }>();
|
|
182
|
+
|
|
183
|
+
constructor(gl: WebGL2RenderingContext, state: GlState) {
|
|
184
|
+
this.gl = gl;
|
|
185
|
+
this.state = state;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The pools and the kept layers emptied, as the context comes or comes back. */
|
|
189
|
+
setUp(): void {
|
|
190
|
+
this.freeLayers = new SizedPool<Layer>(layerBytes);
|
|
191
|
+
this.kept = [];
|
|
192
|
+
this.freeMsaa = new SizedPool<Multisampled>(msaaBytes);
|
|
193
|
+
this.completeMsaa = new Map();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get frame(): number {
|
|
197
|
+
return this.frameCount;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get frameSamples(): number {
|
|
201
|
+
return this.layerSamples;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** A frame begun, its layers taking `samples`: the stage's, 1 for a BitmapData's frame. */
|
|
205
|
+
beginFrame(samples: number): void {
|
|
206
|
+
this.layerSamples = samples;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A frame of the canvas' ended: the free layers and multisampled buffers
|
|
211
|
+
* undrawn into for LAYER_IDLE_FRAMES let go of, and the frame counted.
|
|
212
|
+
*/
|
|
213
|
+
endFrame(): void {
|
|
214
|
+
const drop = (l: Layer) => this.deleteLayer(l);
|
|
215
|
+
const dropMsaa = (m: Multisampled) => this.deleteMultisampled(m);
|
|
216
|
+
this.freeLayers.trimIdle(this.frameCount, LAYER_IDLE_FRAMES, drop);
|
|
217
|
+
this.freeMsaa.trimIdle(this.frameCount, LAYER_IDLE_FRAMES, dropMsaa);
|
|
218
|
+
trimTogether(
|
|
219
|
+
[
|
|
220
|
+
{ pool: this.freeLayers, drop },
|
|
221
|
+
{ pool: this.freeMsaa, drop: dropMsaa },
|
|
222
|
+
],
|
|
223
|
+
// What this frame drew with stays for the next, which likely draws alike: holding it adds
|
|
224
|
+
// nothing to what the frame itself took.
|
|
225
|
+
Math.min(
|
|
226
|
+
WORKING_BYTES,
|
|
227
|
+
FREE_BYTES +
|
|
228
|
+
this.freeLayers.bytesFreedIn(this.frameCount) +
|
|
229
|
+
this.freeMsaa.bytesFreedIn(this.frameCount),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
this.frameCount++;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The layer kept as output `id` (Keeps), or none. */
|
|
236
|
+
keptLayer(id: number): Layer | null {
|
|
237
|
+
return this.kept[id] ?? null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** `layer` kept as output `id` (Keeps) for the frames after, the one kept before given back. */
|
|
241
|
+
keep(id: number, layer: Layer): void {
|
|
242
|
+
const before = this.kept[id];
|
|
243
|
+
if (before && before !== layer) {
|
|
244
|
+
this.giveLayer(before);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.kept[id] = layer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The layer kept as output `id` given back, as Keeps let go of it. */
|
|
251
|
+
release(id: number): void {
|
|
252
|
+
const layer = this.kept[id];
|
|
253
|
+
if (layer) {
|
|
254
|
+
this.giveLayer(layer);
|
|
255
|
+
this.kept[id] = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `open`'s multisampled pixels resolved into its layer's texture, its
|
|
261
|
+
* first `width` by `height`, once: what reads it after reads them.
|
|
262
|
+
*/
|
|
263
|
+
resolve(open: Open, width: number, height: number): void {
|
|
264
|
+
const msaa = open.msaa;
|
|
265
|
+
const layer = open.layer;
|
|
266
|
+
if (!msaa || !layer) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const gl = this.gl;
|
|
271
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, msaa.framebuffer);
|
|
272
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, layer.framebuffer);
|
|
273
|
+
// The blit writes the whole rectangle, so what the layer held there goes:
|
|
274
|
+
// said so, as ANGLE's Vulkan back end, SwiftShader's and a GPU's alike,
|
|
275
|
+
// otherwise left a reused layer's old pixels where the blit had written
|
|
276
|
+
// (tests/player/packed-filters.ts, once the pool keeps layers a frame).
|
|
277
|
+
gl.invalidateSubFramebuffer(gl.DRAW_FRAMEBUFFER, [gl.COLOR_ATTACHMENT0], 0, 0, width, height);
|
|
278
|
+
gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
279
|
+
// Bound as the state cache has it.
|
|
280
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, msaa.framebuffer);
|
|
281
|
+
open.msaa = null;
|
|
282
|
+
// Free for the next layer: only those drawn into now hold multisampled buffers.
|
|
283
|
+
this.freeMsaa.give(msaa, this.frameCount, msaa.samples);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Multisampled buffers to draw `layer` into at `wanted` samples, as many
|
|
288
|
+
* as fit, reusing its size or a slightly larger buffer; none where it
|
|
289
|
+
* takes none, its own stencil then made if it has none.
|
|
290
|
+
*/
|
|
291
|
+
layerMsaa(layer: Layer, wanted: number): Multisampled | null {
|
|
292
|
+
const samples = fittingSamples(layer.width, layer.height, wanted);
|
|
293
|
+
// The resolve copies only the layer's rectangle. A larger MSAA buffer can serve it,
|
|
294
|
+
// but bounding its area avoids clearing a large page for a small layer.
|
|
295
|
+
const kept =
|
|
296
|
+
samples > 1
|
|
297
|
+
? (this.freeMsaa.take(layer.width, layer.height, samples) ??
|
|
298
|
+
this.freeMsaa.takeFitting(
|
|
299
|
+
layer.width,
|
|
300
|
+
layer.height,
|
|
301
|
+
samples,
|
|
302
|
+
layer.width * layer.height * 2,
|
|
303
|
+
))
|
|
304
|
+
: null;
|
|
305
|
+
if (kept) {
|
|
306
|
+
return kept;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const msaa = samples > 1 ? this.multisampled(layer.width, layer.height, samples) : null;
|
|
310
|
+
if (!msaa && !layer.stencil) {
|
|
311
|
+
const gl = this.gl;
|
|
312
|
+
const stencil = gl.createRenderbuffer() as WebGLRenderbuffer;
|
|
313
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, stencil);
|
|
314
|
+
gl.renderbufferStorage(
|
|
315
|
+
gl.RENDERBUFFER,
|
|
316
|
+
gl.DEPTH24_STENCIL8,
|
|
317
|
+
layer.textureWidth,
|
|
318
|
+
layer.textureHeight,
|
|
319
|
+
);
|
|
320
|
+
this.state.bindFramebuffer(layer.framebuffer);
|
|
321
|
+
gl.framebufferRenderbuffer(
|
|
322
|
+
gl.FRAMEBUFFER,
|
|
323
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
324
|
+
gl.RENDERBUFFER,
|
|
325
|
+
stencil,
|
|
326
|
+
);
|
|
327
|
+
layer.stencil = stencil;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return msaa;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Multisampled buffers, or none where the driver leaves their framebuffer incomplete. */
|
|
334
|
+
multisampled(width: number, height: number, samples: number): Multisampled | null {
|
|
335
|
+
const gl = this.gl;
|
|
336
|
+
const color = gl.createRenderbuffer() as WebGLRenderbuffer;
|
|
337
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, color);
|
|
338
|
+
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, width, height);
|
|
339
|
+
const depthStencil = gl.createRenderbuffer() as WebGLRenderbuffer;
|
|
340
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencil);
|
|
341
|
+
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.DEPTH24_STENCIL8, width, height);
|
|
342
|
+
const framebuffer = gl.createFramebuffer() as WebGLFramebuffer;
|
|
343
|
+
this.state.bindFramebuffer(framebuffer);
|
|
344
|
+
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color);
|
|
345
|
+
gl.framebufferRenderbuffer(
|
|
346
|
+
gl.FRAMEBUFFER,
|
|
347
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
348
|
+
gl.RENDERBUFFER,
|
|
349
|
+
depthStencil,
|
|
350
|
+
);
|
|
351
|
+
const msaa = {
|
|
352
|
+
framebuffer,
|
|
353
|
+
color,
|
|
354
|
+
depthStencil,
|
|
355
|
+
width,
|
|
356
|
+
height,
|
|
357
|
+
samples,
|
|
358
|
+
usedAt: this.frameCount,
|
|
359
|
+
};
|
|
360
|
+
// Asking waits for the GPU process, milliseconds at a time, and the
|
|
361
|
+
// answer hangs on the formats and the samples, which are always these,
|
|
362
|
+
// and on each side against the largest a renderbuffer may have: one no
|
|
363
|
+
// wider than the widest found complete of its samples, and no taller
|
|
364
|
+
// than the tallest, which may be another, is not asked about.
|
|
365
|
+
const complete = this.completeMsaa.get(samples);
|
|
366
|
+
if (complete && width <= complete.width && height <= complete.height) {
|
|
367
|
+
return msaa;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
|
371
|
+
this.deleteMultisampled(msaa);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.completeMsaa.set(samples, {
|
|
376
|
+
width: Math.max(width, complete?.width ?? 0),
|
|
377
|
+
height: Math.max(height, complete?.height ?? 0),
|
|
378
|
+
});
|
|
379
|
+
return msaa;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
deleteMultisampled(msaa: Multisampled): void {
|
|
383
|
+
this.gl.deleteFramebuffer(msaa.framebuffer);
|
|
384
|
+
this.gl.deleteRenderbuffer(msaa.color);
|
|
385
|
+
this.gl.deleteRenderbuffer(msaa.depthStencil);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* A layer of at least `width` by `height`, the smallest of those free
|
|
390
|
+
* that is, or one made, its sides rounded as layerSide rounds them;
|
|
391
|
+
* `exact`, used as just the rounded size, which filters take so that a
|
|
392
|
+
* frame's targets are alike: a larger texture serves it as one of that
|
|
393
|
+
* size, as filters read it within its size (FilterTarget), so that a
|
|
394
|
+
* layer whose size changes as its content moves finds one free.
|
|
395
|
+
*/
|
|
396
|
+
takeLayer(width: number, height: number, exact = false): Layer {
|
|
397
|
+
width = layerSide(width);
|
|
398
|
+
height = layerSide(height);
|
|
399
|
+
// Bound excess area: oversized texture reuse also enlarges the MSAA target and clears. An
|
|
400
|
+
// exact one takes its multisampled buffers and clears at its own size, so may go further.
|
|
401
|
+
const found =
|
|
402
|
+
this.freeLayers.take(width, height) ??
|
|
403
|
+
this.freeLayers.takeFitting(width, height, 0, width * height * (exact ? 4 : 2));
|
|
404
|
+
if (found) {
|
|
405
|
+
found.usedAt = this.frameCount;
|
|
406
|
+
found.width = exact ? width : found.textureWidth;
|
|
407
|
+
found.height = exact ? height : found.textureHeight;
|
|
408
|
+
return found;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return this.makeLayer(width, height);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** A layer just `width` by `height`, made. */
|
|
415
|
+
makeLayer(width: number, height: number): Layer {
|
|
416
|
+
const gl = this.gl;
|
|
417
|
+
const texture = gl.createTexture() as WebGLTexture;
|
|
418
|
+
this.state.bindTexture(LAYER_UNIT, texture);
|
|
419
|
+
this.state.activeTexture(LAYER_UNIT);
|
|
420
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
421
|
+
// Read texel by texel: with no mipmaps, the default filter would leave it incomplete, reading 0.
|
|
422
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
423
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
424
|
+
const framebuffer = gl.createFramebuffer() as WebGLFramebuffer;
|
|
425
|
+
this.state.bindFramebuffer(framebuffer);
|
|
426
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
427
|
+
return {
|
|
428
|
+
framebuffer,
|
|
429
|
+
texture,
|
|
430
|
+
stencil: null,
|
|
431
|
+
width,
|
|
432
|
+
height,
|
|
433
|
+
textureWidth: width,
|
|
434
|
+
textureHeight: height,
|
|
435
|
+
usedAt: this.frameCount,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** A layer free again, kept for the layers after it while it is drawn into often enough. */
|
|
440
|
+
giveLayer(layer: Layer): void {
|
|
441
|
+
this.freeLayers.give(layer, this.frameCount);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
deleteLayer(layer: Layer): void {
|
|
445
|
+
this.state.dropTexture(layer.texture);
|
|
446
|
+
this.gl.deleteFramebuffer(layer.framebuffer);
|
|
447
|
+
this.gl.deleteTexture(layer.texture);
|
|
448
|
+
if (layer.stencil) {
|
|
449
|
+
this.gl.deleteRenderbuffer(layer.stencil);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The free and the kept layers, and the free multisampled buffers, deleted. */
|
|
454
|
+
destroy(): void {
|
|
455
|
+
this.freeLayers.clear((l) => this.deleteLayer(l));
|
|
456
|
+
for (const layer of this.kept) {
|
|
457
|
+
if (layer) {
|
|
458
|
+
this.deleteLayer(layer);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.freeMsaa.clear((m) => this.deleteMultisampled(m));
|
|
463
|
+
}
|
|
464
|
+
}
|