drawnui-react 0.1.0-preview.14 → 0.1.0-preview.15
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/dist/core/Canvas.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare class Canvas {
|
|
|
47
47
|
private frameId;
|
|
48
48
|
private disposed;
|
|
49
49
|
private readonly observer;
|
|
50
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
51
|
+
private contextLost;
|
|
52
|
+
/**
|
|
53
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
54
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
55
|
+
*/
|
|
56
|
+
GpuEpoch: number;
|
|
50
57
|
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
51
58
|
constructor(Element: HTMLCanvasElement, init?: (canvas: Canvas) => void);
|
|
52
59
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
@@ -61,6 +68,17 @@ export declare class Canvas {
|
|
|
61
68
|
* redrawn content lands in the same paint and no blank frame is ever shown while dragging.
|
|
62
69
|
*/
|
|
63
70
|
private OnResized;
|
|
71
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
72
|
+
private readonly onContextLost;
|
|
73
|
+
/**
|
|
74
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
75
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
76
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
77
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
78
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
79
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
80
|
+
*/
|
|
81
|
+
private readonly onContextRestored;
|
|
64
82
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
65
83
|
private DrawNow;
|
|
66
84
|
private Draw;
|
package/dist/core/Canvas.js
CHANGED
|
@@ -54,19 +54,28 @@ export class Canvas {
|
|
|
54
54
|
frameId = 0;
|
|
55
55
|
disposed = false;
|
|
56
56
|
observer;
|
|
57
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
58
|
+
contextLost = false;
|
|
59
|
+
/**
|
|
60
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
61
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
62
|
+
*/
|
|
63
|
+
GpuEpoch = 0;
|
|
57
64
|
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
58
65
|
constructor(Element, init) {
|
|
59
66
|
this.Element = Element;
|
|
60
67
|
if (!Super.CK)
|
|
61
68
|
throw new Error("DrawnUi: call Super.UseDrawnUi()...BuildAsync() before creating a Canvas");
|
|
62
69
|
init?.(this);
|
|
70
|
+
Element.addEventListener("webglcontextlost", this.onContextLost);
|
|
71
|
+
Element.addEventListener("webglcontextrestored", this.onContextRestored);
|
|
63
72
|
this.observer = new ResizeObserver(() => this.OnResized());
|
|
64
73
|
this.observer.observe(Element);
|
|
65
74
|
this.OnResized();
|
|
66
75
|
}
|
|
67
76
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
68
77
|
Update() {
|
|
69
|
-
if (this.frameId || !this.surface || this.disposed)
|
|
78
|
+
if (this.frameId || !this.surface || this.disposed || this.contextLost)
|
|
70
79
|
return;
|
|
71
80
|
const surface = this.surface;
|
|
72
81
|
this.frameId = surface.requestAnimationFrame((c) => {
|
|
@@ -117,16 +126,53 @@ export class Canvas {
|
|
|
117
126
|
this.RenderingScale = dpr;
|
|
118
127
|
this.Element.width = w;
|
|
119
128
|
this.Element.height = h;
|
|
129
|
+
if (this.contextLost)
|
|
130
|
+
return; // the restore makes the surface at this size
|
|
120
131
|
this.ReleaseSurface();
|
|
121
132
|
this.surface = this.CreateSurface(w, h);
|
|
122
133
|
if (!this.surface)
|
|
123
134
|
throw new Error("DrawnUi: cannot create surface");
|
|
124
135
|
this.DrawNow();
|
|
125
136
|
}
|
|
137
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
138
|
+
onContextLost = (e) => {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
this.contextLost = true;
|
|
141
|
+
if (this.frameId) {
|
|
142
|
+
cancelAnimationFrame(this.frameId);
|
|
143
|
+
this.frameId = 0;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
148
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
149
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
150
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
151
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
152
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
153
|
+
*/
|
|
154
|
+
onContextRestored = () => {
|
|
155
|
+
if (this.disposed)
|
|
156
|
+
return;
|
|
157
|
+
if (this.grContext)
|
|
158
|
+
this.grContext.releaseResourcesAndAbandonContext();
|
|
159
|
+
this.surface?.delete();
|
|
160
|
+
this.surface = undefined;
|
|
161
|
+
this.grContext?.delete();
|
|
162
|
+
this.grContext = undefined;
|
|
163
|
+
if (this.glHandle) {
|
|
164
|
+
Super.CK.deleteContext(this.glHandle);
|
|
165
|
+
this.glHandle = undefined;
|
|
166
|
+
}
|
|
167
|
+
this.contextLost = false;
|
|
168
|
+
this.GpuEpoch++;
|
|
169
|
+
this.surface = this.CreateSurface(this.Element.width, this.Element.height);
|
|
170
|
+
this.DrawNow();
|
|
171
|
+
};
|
|
126
172
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
127
173
|
DrawNow() {
|
|
128
174
|
const surface = this.surface;
|
|
129
|
-
if (!surface || this.disposed)
|
|
175
|
+
if (!surface || this.disposed || this.contextLost)
|
|
130
176
|
return;
|
|
131
177
|
this.Draw(surface.getCanvas());
|
|
132
178
|
surface.flush();
|
|
@@ -260,6 +306,8 @@ export class Canvas {
|
|
|
260
306
|
}
|
|
261
307
|
Dispose() {
|
|
262
308
|
this.disposed = true;
|
|
309
|
+
this.Element.removeEventListener("webglcontextlost", this.onContextLost);
|
|
310
|
+
this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
263
311
|
this.Gestures = "Disabled";
|
|
264
312
|
this.observer.disconnect();
|
|
265
313
|
this.content?.Dispose();
|
|
@@ -36,6 +36,8 @@ export declare class CachedObject {
|
|
|
36
36
|
constructor(Type: SkiaCacheType,
|
|
37
37
|
/** Rect the content was recorded for, canvas pixels at recording time. */
|
|
38
38
|
Bounds: SKRect, Scale: number, Picture?: SkPicture | undefined, Image?: Image | undefined);
|
|
39
|
+
/** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
|
|
40
|
+
Epoch: number;
|
|
39
41
|
/** Replays the cache with its top-left moved to (left, top). */
|
|
40
42
|
Draw(canvas: SkCanvas, left: number, top: number): void;
|
|
41
43
|
Dispose(): void;
|
package/dist/core/SkiaControl.js
CHANGED
|
@@ -22,6 +22,8 @@ export class CachedObject {
|
|
|
22
22
|
this.Picture = Picture;
|
|
23
23
|
this.Image = Image;
|
|
24
24
|
}
|
|
25
|
+
/** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
|
|
26
|
+
Epoch = 0;
|
|
25
27
|
/** Replays the cache with its top-left moved to (left, top). */
|
|
26
28
|
Draw(canvas, left, top) {
|
|
27
29
|
const CK = Super.CK;
|
|
@@ -691,7 +693,10 @@ export class SkiaControl {
|
|
|
691
693
|
}
|
|
692
694
|
else {
|
|
693
695
|
const r = cacheType === "Operations" ? this.ExpandedCacheRect(ctx.Scale) : this.AlignedCacheRect(ctx.Scale);
|
|
694
|
-
const
|
|
696
|
+
const epoch = this.Superview?.GpuEpoch ?? 0;
|
|
697
|
+
if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
|
|
698
|
+
this.DisposePrevious(); // made on a lost GPU context
|
|
699
|
+
const stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType || this.RenderObject.Epoch !== epoch
|
|
695
700
|
|| this.RenderObject.Scale !== ctx.Scale
|
|
696
701
|
|| Math.round(this.RenderObject.Bounds.Width) !== Math.round(r.Width) || Math.round(this.RenderObject.Bounds.Height) !== Math.round(r.Height);
|
|
697
702
|
if (stale)
|
|
@@ -750,6 +755,7 @@ export class SkiaControl {
|
|
|
750
755
|
const picture = recorder.finishRecordingAsPicture();
|
|
751
756
|
recorder.delete();
|
|
752
757
|
this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
|
|
758
|
+
this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
|
|
753
759
|
}
|
|
754
760
|
else if (cacheType === "ImageComposite") {
|
|
755
761
|
this.CreateCompositeRenderingObject(ctx, r, w, h);
|
|
@@ -773,6 +779,7 @@ export class SkiaControl {
|
|
|
773
779
|
const image = offscreen.makeImageSnapshot();
|
|
774
780
|
offscreen.delete();
|
|
775
781
|
this.RenderObject = new CachedObject(cacheType, r, ctx.Scale, undefined, image);
|
|
782
|
+
this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
|
|
776
783
|
}
|
|
777
784
|
this.cacheDirty = false;
|
|
778
785
|
}
|
|
@@ -791,7 +798,9 @@ export class SkiaControl {
|
|
|
791
798
|
}
|
|
792
799
|
let surface = this.compositeSurface;
|
|
793
800
|
const prev = this.RenderObject;
|
|
794
|
-
const
|
|
801
|
+
const epoch = this.Superview?.GpuEpoch ?? 0;
|
|
802
|
+
// the kept surface belongs to the GPU context it was made on: after a context restore it is made again
|
|
803
|
+
const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale && prev.Epoch === epoch
|
|
795
804
|
&& Math.round(prev.Bounds.Width) === w && Math.round(prev.Bounds.Height) === h;
|
|
796
805
|
if (!sameGeometry) {
|
|
797
806
|
surface?.delete();
|
|
@@ -871,6 +880,7 @@ export class SkiaControl {
|
|
|
871
880
|
const image = surface.makeImageSnapshot();
|
|
872
881
|
const old = this.RenderObject;
|
|
873
882
|
this.RenderObject = new CachedObject("ImageComposite", r, ctx.Scale, undefined, image);
|
|
883
|
+
this.RenderObject.Epoch = epoch;
|
|
874
884
|
if (old) {
|
|
875
885
|
const sv = this.Superview;
|
|
876
886
|
if (sv)
|
|
@@ -125,6 +125,9 @@ export declare class SkiaShaderEffect extends SkiaEffect implements IPostRendere
|
|
|
125
125
|
protected ApplyShaderSource(): void;
|
|
126
126
|
/** Snapshot of what the parent painted (C# CreateSnapshot): the surface the pixels are on, mapped through its origin. */
|
|
127
127
|
protected CreateSnapshot(ctx: DrawingContext, destination: SKRect): CachedTexture | undefined;
|
|
128
|
+
private frozenEpoch;
|
|
129
|
+
/** Canvas.GpuEpoch: GPU textures made before a WebGL context restore are made again. */
|
|
130
|
+
protected GpuEpoch(): number;
|
|
128
131
|
/** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
|
|
129
132
|
private frozenAt?;
|
|
130
133
|
private ParentOrigin;
|
package/dist/core/SkiaEffect.js
CHANGED
|
@@ -159,6 +159,9 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
159
159
|
return undefined;
|
|
160
160
|
return { Image: image, Bounds: destination, Origin: { X: ox, Y: oy } };
|
|
161
161
|
}
|
|
162
|
+
frozenEpoch = 0;
|
|
163
|
+
/** Canvas.GpuEpoch: GPU textures made before a WebGL context restore are made again. */
|
|
164
|
+
GpuEpoch() { return this.Parent?.Superview?.GpuEpoch ?? 0; }
|
|
162
165
|
/** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
|
|
163
166
|
frozenAt;
|
|
164
167
|
ParentOrigin() { const r = this.Parent?.DrawingRect; return { X: r?.Left ?? 0, Y: r?.Top ?? 0 }; }
|
|
@@ -176,7 +179,7 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
176
179
|
switch (this.UseBackground) {
|
|
177
180
|
case "Never": return undefined;
|
|
178
181
|
case "Once":
|
|
179
|
-
if (!this.AquiredBackground || !this.frozen) {
|
|
182
|
+
if (!this.AquiredBackground || !this.frozen || this.frozenEpoch !== this.GpuEpoch()) { // a frozen texture of a lost GPU context is taken again
|
|
180
183
|
this.ReleaseFrozenSnapshot();
|
|
181
184
|
let snapshot = this.Parent?.CachedImage;
|
|
182
185
|
let owned = false;
|
|
@@ -189,6 +192,7 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
189
192
|
this.frozenOwned = owned;
|
|
190
193
|
this.AquiredBackground = true;
|
|
191
194
|
this.frozenAt = this.ParentOrigin();
|
|
195
|
+
this.frozenEpoch = this.GpuEpoch();
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
198
|
return this.FollowParent(this.frozen);
|
|
@@ -434,7 +438,8 @@ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
|
|
|
434
438
|
return undefined;
|
|
435
439
|
const w = Math.round(d.Width), h = Math.round(d.Height);
|
|
436
440
|
const cur = slot === "p" ? this.resizedPrimary : this.resizedSecondary;
|
|
437
|
-
|
|
441
|
+
const epoch = this.GpuEpoch();
|
|
442
|
+
if (cur && cur.source === source && cur.w === w && cur.h === h && cur.epoch === epoch)
|
|
438
443
|
return cur.image;
|
|
439
444
|
const main = ctx.Context.Surface;
|
|
440
445
|
if (!main)
|
|
@@ -449,7 +454,7 @@ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
|
|
|
449
454
|
const image = surface.makeImageSnapshot();
|
|
450
455
|
surface.delete();
|
|
451
456
|
cur?.image.delete();
|
|
452
|
-
const entry = { source, w, h, image };
|
|
457
|
+
const entry = { source, w, h, epoch, image };
|
|
453
458
|
if (slot === "p")
|
|
454
459
|
this.resizedPrimary = entry;
|
|
455
460
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drawnui-react",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.15",
|
|
4
4
|
"description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nick Kovalsky (AppoMobi)",
|