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.
@@ -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;
@@ -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;
@@ -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 stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType
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 sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale
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;
@@ -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
- if (cur && cur.source === source && cur.w === w && cur.h === h)
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.14",
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)",