@gjsify/webgl 0.1.8 → 0.1.9

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.
@@ -11,6 +11,7 @@ const CanvasWebGLWidget = GObject.registerClass(
11
11
  super(params);
12
12
  this._canvas = null;
13
13
  this._readyCallbacks = [];
14
+ this._resizeCallbacks = [];
14
15
  this._renderTag = null;
15
16
  this._tickCallbackId = null;
16
17
  this._frameCallback = null;
@@ -22,7 +23,7 @@ const CanvasWebGLWidget = GObject.registerClass(
22
23
  this.set_required_version(3, 2);
23
24
  this.set_has_depth_buffer(true);
24
25
  this.set_has_stencil_buffer(true);
25
- attachEventControllers(this, () => this._canvas);
26
+ attachEventControllers(this, () => this._canvas, { captureKeys: true });
26
27
  const initId = this.connect("render", () => {
27
28
  this.disconnect(initId);
28
29
  this.make_current();
@@ -30,6 +31,7 @@ const CanvasWebGLWidget = GObject.registerClass(
30
31
  if (globalThis.document?.body) {
31
32
  globalThis.document.body.appendChild(this._canvas);
32
33
  }
34
+ this._canvas.getContext("webgl2");
33
35
  const gl = this._canvas.getContext("webgl");
34
36
  if (gl) {
35
37
  for (const cb of this._readyCallbacks) {
@@ -40,9 +42,14 @@ const CanvasWebGLWidget = GObject.registerClass(
40
42
  return true;
41
43
  });
42
44
  this.connect("resize", () => {
45
+ const width = this.get_allocated_width();
46
+ const height = this.get_allocated_height();
43
47
  if (this._canvas) {
44
48
  this._canvas.dispatchEvent(new Event("resize"));
45
49
  }
50
+ for (const cb of this._resizeCallbacks) {
51
+ cb(width, height);
52
+ }
46
53
  if (this._frameCallback) {
47
54
  this.requestAnimationFrame(this._frameCallback);
48
55
  }
@@ -83,6 +90,16 @@ const CanvasWebGLWidget = GObject.registerClass(
83
90
  onWebGLReady(cb) {
84
91
  this.onReady(cb);
85
92
  }
93
+ /**
94
+ * Register a callback invoked whenever the GTK widget is resized.
95
+ * The callback fires alongside the native 'resize' GObject signal and
96
+ * after the DOM 'resize' event has been dispatched on the canvas.
97
+ * Canvas buffer dimensions are NOT automatically updated — consumers
98
+ * should set `canvas.width`/`canvas.height` themselves if desired.
99
+ */
100
+ onResize(cb) {
101
+ this._resizeCallbacks.push(cb);
102
+ }
86
103
  /**
87
104
  * Schedules a single animation frame callback, matching the browser `requestAnimationFrame` API.
88
105
  * Backed by GTK frame clock (vsync-synced) + the GLArea render signal.
@@ -94,10 +111,13 @@ const CanvasWebGLWidget = GObject.registerClass(
94
111
  this._tickCallbackId = this.add_tick_callback((_widget, _frameClock) => {
95
112
  this._tickCallbackId = null;
96
113
  if (this._renderTag === null) {
97
- this._renderTag = this.connect("render", () => {
114
+ this._renderTag = this.connect("render", (_widget2) => {
98
115
  this.disconnect(this._renderTag);
99
116
  this._renderTag = null;
100
117
  const time = (GLib.get_monotonic_time() - this._timeOrigin) / 1e3;
118
+ if (globalThis.__GJSIFY_DEBUG_RAF === true) {
119
+ console.log(`[rAF] frame callback fires t=${time.toFixed(1)}`);
120
+ }
101
121
  this._frameCallback?.(time);
102
122
  return true;
103
123
  });
@@ -115,6 +135,9 @@ const CanvasWebGLWidget = GObject.registerClass(
115
135
  */
116
136
  installGlobals() {
117
137
  globalThis.requestAnimationFrame = (cb) => this.requestAnimationFrame(cb);
138
+ globalThis.cancelAnimationFrame = (_id) => {
139
+ this._frameCallback = null;
140
+ };
118
141
  const timeOrigin = this._timeOrigin;
119
142
  globalThis.performance = {
120
143
  now: () => (GLib.get_monotonic_time() - timeOrigin) / 1e3,
@@ -253,6 +253,29 @@ var uniforms_spec_default = async () => {
253
253
  ]);
254
254
  expect(gl.getError()).toBe(gl.NO_ERROR);
255
255
  });
256
+ await it("getUniformLocation resolves bare array name (without [0] suffix)", async () => {
257
+ const prog = makeProgram(gl, VS, FS_ARR);
258
+ gl.useProgram(prog);
259
+ const locBare = gl.getUniformLocation(prog, "u_arr");
260
+ expect(locBare).not.toBeNull();
261
+ expect(gl.getError()).toBe(gl.NO_ERROR);
262
+ gl.uniform1fv(locBare, [0.7, 0.8, 0.9]);
263
+ expect(gl.getError()).toBe(gl.NO_ERROR);
264
+ const val = gl.getUniform(prog, locBare);
265
+ expect(Math.abs(val - 0.7) < 1e-3).toBe(true);
266
+ });
267
+ await it("bare array name and indexed [0] name resolve to equivalent locations", async () => {
268
+ const prog = makeProgram(gl, VS, FS_ARR);
269
+ gl.useProgram(prog);
270
+ const locBare = gl.getUniformLocation(prog, "u_arr");
271
+ const locIndexed = gl.getUniformLocation(prog, "u_arr[0]");
272
+ expect(locBare).not.toBeNull();
273
+ expect(locIndexed).not.toBeNull();
274
+ gl.uniform1fv(locBare, [0.42, 0.43, 0.44]);
275
+ expect(gl.getError()).toBe(gl.NO_ERROR);
276
+ const val = gl.getUniform(prog, locIndexed);
277
+ expect(Math.abs(val - 0.42) < 1e-3).toBe(true);
278
+ });
256
279
  await it("calling uniform* before useProgram generates INVALID_OPERATION", async () => {
257
280
  const prog = makeProgram(gl, VS, FS_ARR);
258
281
  gl.useProgram(null);
@@ -24,6 +24,14 @@ class HTMLCanvasElement extends BaseHTMLCanvasElement {
24
24
  get clientHeight() {
25
25
  return this.height;
26
26
  }
27
+ /** CSS layout width — same as the GTK-allocated pixel width for a full-window canvas. */
28
+ get offsetWidth() {
29
+ return this.width;
30
+ }
31
+ /** CSS layout height — same as the GTK-allocated pixel height for a full-window canvas. */
32
+ get offsetHeight() {
33
+ return this.height;
34
+ }
27
35
  /** Returns the underlying Gtk.GLArea. Used by WebGLRenderingContext for GLSL version detection. */
28
36
  getGlArea() {
29
37
  return this.gtkGlArea;
@@ -36,11 +44,17 @@ class HTMLCanvasElement extends BaseHTMLCanvasElement {
36
44
  */
37
45
  getContext(contextId, options) {
38
46
  if (contextId === "webgl" || contextId === "experimental-webgl") {
39
- this._webgl ??= new OurWebGLRenderingContext(this, options);
47
+ if (!this._webgl) {
48
+ this.gtkGlArea.make_current();
49
+ this._webgl = new OurWebGLRenderingContext(this, options);
50
+ }
40
51
  return this._webgl;
41
52
  }
42
53
  if (contextId === "webgl2") {
43
- this._webgl2 ??= new OurWebGL2RenderingContext(this, options);
54
+ if (!this._webgl2) {
55
+ this.gtkGlArea.make_current();
56
+ this._webgl2 = new OurWebGL2RenderingContext(this, options);
57
+ }
44
58
  return this._webgl2;
45
59
  }
46
60
  return super.getContext(contextId, options);
package/lib/esm/utils.js CHANGED
@@ -133,7 +133,7 @@ const extractImageData = (pixels) => {
133
133
  let context = null;
134
134
  if (typeof pixels.getContext === "function") {
135
135
  context = pixels.getContext("2d");
136
- } else if (typeof pixels.isPixbuf()) {
136
+ } else if (typeof pixels.isPixbuf === "function" && pixels.isPixbuf()) {
137
137
  return pixels.getImageData();
138
138
  } else if (typeof pixels.src !== "undefined" && typeof document === "object" && typeof document.createElement === "function") {
139
139
  const canvas = document.createElement("canvas");
@@ -190,6 +190,17 @@ function flag(options, name, dflt) {
190
190
  }
191
191
  return !!options[name];
192
192
  }
193
+ function premultiplyAlpha(data) {
194
+ const out = new Uint8Array(data.length);
195
+ for (let i = 0; i < data.length; i += 4) {
196
+ const a = data[i + 3] / 255;
197
+ out[i] = Math.round(data[i] * a);
198
+ out[i + 1] = Math.round(data[i + 1] * a);
199
+ out[i + 2] = Math.round(data[i + 2] * a);
200
+ out[i + 3] = data[i + 3];
201
+ }
202
+ return out;
203
+ }
193
204
  export {
194
205
  Uint8ArrayToVariant,
195
206
  arrayToUint8Array,
@@ -205,6 +216,7 @@ export {
205
216
  isTypedArray,
206
217
  isValidString,
207
218
  listToArray,
219
+ premultiplyAlpha,
208
220
  typeSize,
209
221
  uniformTypeSize,
210
222
  validCubeTarget,
@@ -20,7 +20,8 @@ import {
20
20
  uniformTypeSize,
21
21
  vertexCount,
22
22
  typeSize,
23
- Uint8ArrayToVariant
23
+ Uint8ArrayToVariant,
24
+ premultiplyAlpha
24
25
  } from "./utils.js";
25
26
  import { getOESElementIndexUint } from "./extensions/oes-element-index-unit.js";
26
27
  import { getOESStandardDerivatives } from "./extensions/oes-standard-derivatives.js";
@@ -97,6 +98,7 @@ class WebGLContextBase {
97
98
  this._unpackAlignment = 4;
98
99
  this._packAlignment = 4;
99
100
  this._unpackFlipY = false;
101
+ this._unpackPremultAlpha = false;
100
102
  // Viewport and scissor — tracked in JS to avoid crashing native getParameterx for array returns
101
103
  this._viewport = new Int32Array([0, 0, 0, 0]);
102
104
  this._scissorBox = new Int32Array([0, 0, 0, 0]);
@@ -161,6 +163,7 @@ class WebGLContextBase {
161
163
  this._unpackAlignment = 4;
162
164
  this._packAlignment = 4;
163
165
  this._unpackFlipY = false;
166
+ this._unpackPremultAlpha = false;
164
167
  this.bindBuffer(this.ARRAY_BUFFER, null);
165
168
  this.bindBuffer(this.ELEMENT_ARRAY_BUFFER, null);
166
169
  this.bindFramebuffer(this.FRAMEBUFFER, null);
@@ -1166,6 +1169,9 @@ class WebGLContextBase {
1166
1169
  this.setError(this.INVALID_VALUE);
1167
1170
  return;
1168
1171
  }
1172
+ if (this._unpackPremultAlpha && data && format === this.RGBA) {
1173
+ data = premultiplyAlpha(data);
1174
+ }
1169
1175
  if (this._unpackFlipY && data && width > 0 && height > 0) {
1170
1176
  const flipped = new Uint8Array(data.length);
1171
1177
  for (let row = 0; row < height; row++) {
@@ -1266,6 +1272,9 @@ class WebGLContextBase {
1266
1272
  this.setError(this.INVALID_OPERATION);
1267
1273
  return;
1268
1274
  }
1275
+ if (this._unpackPremultAlpha && data && format === this.RGBA) {
1276
+ data = premultiplyAlpha(data);
1277
+ }
1269
1278
  if (this._unpackFlipY && data && width > 0 && height > 0) {
1270
1279
  const flipped = new Uint8Array(data.length);
1271
1280
  for (let row = 0; row < height; row++) {
@@ -2817,27 +2826,32 @@ class WebGLContextBase {
2817
2826
  if (/\[\d+\]$/.test(name)) {
2818
2827
  searchName = name.replace(/\[\d+\]$/, "[0]");
2819
2828
  }
2829
+ const arraySearchName = searchName + "[0]";
2820
2830
  let info = null;
2821
2831
  for (let i = 0; i < program._uniforms.length; ++i) {
2822
2832
  const infoItem = program._uniforms[i];
2823
- if (infoItem.name === searchName) {
2833
+ if (infoItem.name === searchName || infoItem.name === arraySearchName) {
2824
2834
  info = {
2825
2835
  size: infoItem.size,
2826
2836
  type: infoItem.type,
2827
2837
  name: infoItem.name
2828
2838
  };
2839
+ break;
2829
2840
  }
2830
2841
  }
2831
2842
  if (!info) {
2832
- return null;
2843
+ info = { name: searchName, type: 0, size: 1 };
2833
2844
  }
2834
2845
  const result = new WebGLUniformLocation(
2835
2846
  loc,
2836
2847
  program,
2837
2848
  info
2838
2849
  );
2839
- if (/\[0\]$/.test(name)) {
2840
- const baseName = name.replace(/\[0\]$/, "");
2850
+ const callerBracketMatch = name.match(/\[(\d+)\]$/);
2851
+ const callerIndex = callerBracketMatch ? +callerBracketMatch[1] : -1;
2852
+ const infoIsArray = /\[0\]$/.test(info.name);
2853
+ if (infoIsArray && (callerIndex === -1 || callerIndex === 0)) {
2854
+ const baseName = info.name.replace(/\[0\]$/, "");
2841
2855
  const arrayLocs = [];
2842
2856
  this._saveError();
2843
2857
  for (let i = 0; this.getError() === this.NO_ERROR; ++i) {
@@ -2852,13 +2866,8 @@ class WebGLContextBase {
2852
2866
  }
2853
2867
  this._restoreError(this.NO_ERROR);
2854
2868
  result._array = arrayLocs;
2855
- } else if (name && /\[(\d+)\]$/.test(name)) {
2856
- const _regexExec = /\[(\d+)\]$/.exec(name);
2857
- if (!_regexExec || _regexExec.length <= 0) {
2858
- return null;
2859
- }
2860
- const offset = +_regexExec[1];
2861
- if (offset < 0 || offset >= info.size) {
2869
+ } else if (callerIndex > 0) {
2870
+ if (callerIndex >= info.size) {
2862
2871
  return null;
2863
2872
  }
2864
2873
  }
@@ -3011,6 +3020,7 @@ class WebGLContextBase {
3011
3020
  this._unpackFlipY = !!param;
3012
3021
  return;
3013
3022
  } else if (pname === this.UNPACK_PREMULTIPLY_ALPHA_WEBGL) {
3023
+ this._unpackPremultAlpha = !!param;
3014
3024
  return;
3015
3025
  }
3016
3026
  return this._gl.pixelStorei(pname, param);
@@ -10,7 +10,7 @@ import { WebGLActiveInfo } from "./webgl-active-info.js";
10
10
  import { WebGLTexture } from "./webgl-texture.js";
11
11
  import { WebGLRenderbuffer } from "./webgl-renderbuffer.js";
12
12
  import { WebGLFramebuffer } from "./webgl-framebuffer.js";
13
- import { Uint8ArrayToVariant, arrayToUint8Array, vertexCount, convertPixels, extractImageData, checkObject } from "./utils.js";
13
+ import { Uint8ArrayToVariant, arrayToUint8Array, vertexCount, convertPixels, extractImageData, checkObject, premultiplyAlpha } from "./utils.js";
14
14
  import { warnNotImplemented } from "@gjsify/utils";
15
15
  class WebGL2RenderingContext extends WebGLContextBase {
16
16
  constructor(canvas, options = {}) {
@@ -618,6 +618,9 @@ class WebGL2RenderingContext extends WebGLContextBase {
618
618
  return;
619
619
  }
620
620
  let data = convertPixels(pixels);
621
+ if (this._unpackPremultAlpha && data && format === this.RGBA) {
622
+ data = premultiplyAlpha(data);
623
+ }
621
624
  if (this._unpackFlipY && data && width > 0 && height > 0) {
622
625
  const pixelSize = this._computePixelSize(type, format);
623
626
  if (pixelSize > 0) {
@@ -691,6 +694,9 @@ class WebGL2RenderingContext extends WebGLContextBase {
691
694
  this.setError(this.INVALID_OPERATION);
692
695
  return;
693
696
  }
697
+ if (this._unpackPremultAlpha && data && format === this.RGBA) {
698
+ data = premultiplyAlpha(data);
699
+ }
694
700
  if (this._unpackFlipY && data && width > 0 && height > 0) {
695
701
  const pixelSize = this._computePixelSize(type, format);
696
702
  if (pixelSize > 0) {
@@ -724,6 +730,10 @@ class WebGL2RenderingContext extends WebGLContextBase {
724
730
  if (!this._framebufferOk()) return;
725
731
  if (count === 0 || instanceCount === 0) return;
726
732
  if (!this._checkVertexAttribState(count + first - 1 >>> 0)) return;
733
+ if (globalThis.__GJSIFY_DEBUG_GL) {
734
+ const n = this.__drawInstCount = (this.__drawInstCount | 0) + 1;
735
+ if (n <= 5 || n % 100 === 0) console.log(`[WebGL] drawArraysInstanced #${n} count=${rc} instances=${instanceCount} fbo=${this._activeFramebuffer?._ ?? "_gtkFbo"}`);
736
+ }
727
737
  this._native2.drawArraysInstanced(mode, first, rc, instanceCount);
728
738
  }
729
739
  drawElementsInstanced(mode, count, type, offset, instanceCount) {
@@ -826,7 +836,87 @@ class WebGL2RenderingContext extends WebGLContextBase {
826
836
  this.drawElements(mode, count, type, offset);
827
837
  }
828
838
  blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) {
839
+ if (globalThis.__GJSIFY_DEBUG_GL) {
840
+ const errBefore = this._gl.getError();
841
+ if (errBefore !== 0) console.log(`[WebGL] blitFramebuffer PRE-ERROR 0x${errBefore.toString(16)}`);
842
+ }
829
843
  this._native2.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
844
+ if (globalThis.__GJSIFY_DEBUG_GL) {
845
+ const err = this._gl.getError();
846
+ const n = this.__blitCount = (this.__blitCount | 0) + 1;
847
+ if (n <= 5) console.log(`[WebGL] blitFramebuffer #${n} src=(${srcX0},${srcY0},${srcX1},${srcY1}) readFbo=${this._activeReadFramebuffer?._ ?? "_gtkFbo"} err=${err === 0 ? "OK" : "0x" + err.toString(16)}`);
848
+ }
849
+ }
850
+ // clearBuffer{fv,iv,uiv,fi} — WebGL2 methods for clearing specific
851
+ // framebuffer attachments. The native Vala binding does not expose the
852
+ // glClearBuffer* entry points yet, so we emulate the common cases via
853
+ // glClearColor/glClearDepth/glClearStencil + glClear. This is equivalent
854
+ // when the DRAW_FRAMEBUFFER has a single attachment per buffer type,
855
+ // which matches Excalibur's ExcaliburGraphicsContextWebGL.blitToScreen.
856
+ //
857
+ // Buffer target constants per WebGL2 spec (not on our class):
858
+ // COLOR = 0x1800
859
+ // DEPTH = 0x1801
860
+ // STENCIL = 0x1802
861
+ // DEPTH_STENCIL = 0x84F9
862
+ clearBufferfv(buffer, drawbuffer, values, _srcOffset) {
863
+ const n2 = this._native2;
864
+ if (typeof n2.clearBufferfv === "function") {
865
+ n2.clearBufferfv(buffer, drawbuffer, Array.from(values));
866
+ return;
867
+ }
868
+ const v = values;
869
+ if (buffer === 6144) {
870
+ const prev = this.getParameter(this.COLOR_CLEAR_VALUE);
871
+ this.clearColor(v[0] ?? 0, v[1] ?? 0, v[2] ?? 0, v[3] ?? 0);
872
+ this.clear(this.COLOR_BUFFER_BIT);
873
+ if (prev) this.clearColor(prev[0], prev[1], prev[2], prev[3]);
874
+ } else if (buffer === 6145) {
875
+ const prev = this.getParameter(this.DEPTH_CLEAR_VALUE);
876
+ this.clearDepth(v[0] ?? 1);
877
+ this.clear(this.DEPTH_BUFFER_BIT);
878
+ if (prev !== null) this.clearDepth(prev);
879
+ }
880
+ }
881
+ clearBufferiv(buffer, drawbuffer, values, _srcOffset) {
882
+ const n2 = this._native2;
883
+ if (typeof n2.clearBufferiv === "function") {
884
+ n2.clearBufferiv(buffer, drawbuffer, Array.from(values));
885
+ return;
886
+ }
887
+ if (buffer === 6146) {
888
+ const v = values;
889
+ const prev = this.getParameter(this.STENCIL_CLEAR_VALUE);
890
+ this.clearStencil(v[0] ?? 0);
891
+ this.clear(this.STENCIL_BUFFER_BIT);
892
+ if (prev !== null) this.clearStencil(prev);
893
+ }
894
+ }
895
+ clearBufferuiv(buffer, drawbuffer, values, _srcOffset) {
896
+ const n2 = this._native2;
897
+ if (typeof n2.clearBufferuiv === "function") {
898
+ n2.clearBufferuiv(buffer, drawbuffer, Array.from(values));
899
+ return;
900
+ }
901
+ void buffer;
902
+ void drawbuffer;
903
+ }
904
+ clearBufferfi(buffer, drawbuffer, depth, stencil) {
905
+ const n2 = this._native2;
906
+ if (typeof n2.clearBufferfi === "function") {
907
+ n2.clearBufferfi(buffer, drawbuffer, depth, stencil);
908
+ return;
909
+ }
910
+ if (buffer === 34041) {
911
+ const prevDepth = this.getParameter(this.DEPTH_CLEAR_VALUE);
912
+ const prevStencil = this.getParameter(this.STENCIL_CLEAR_VALUE);
913
+ this.clearDepth(depth);
914
+ this.clearStencil(stencil);
915
+ this.clear(this.DEPTH_BUFFER_BIT | this.STENCIL_BUFFER_BIT);
916
+ if (prevDepth !== null) this.clearDepth(prevDepth);
917
+ if (prevStencil !== null) this.clearStencil(prevStencil);
918
+ }
919
+ void drawbuffer;
830
920
  }
831
921
  invalidateFramebuffer(target, attachments) {
832
922
  this._native2.invalidateFramebuffer(target, Array.from(attachments));
@@ -1126,6 +1126,112 @@ void main(){c0=vec4(1.,0.,0.,1.);c1=vec4(0.,0.,1.,1.);}`;
1126
1126
  gl2.deleteProgram(prog);
1127
1127
  });
1128
1128
  });
1129
+ await describe("WebGL2 Excalibur-style VAO draw + READ/DRAW blitToScreen", async () => {
1130
+ beforeEach(async () => {
1131
+ glArea.make_current();
1132
+ });
1133
+ await it("VAO draw to source FBO survives clearBufferfv+blit pipeline", async () => {
1134
+ const W = 4;
1135
+ const H = 4;
1136
+ const gl = gl2;
1137
+ const srcFbo = gl2.createFramebuffer();
1138
+ const srcTex = gl2.createTexture();
1139
+ gl2.bindTexture(gl2.TEXTURE_2D, srcTex);
1140
+ gl2.texImage2D(gl2.TEXTURE_2D, 0, gl2.RGBA, W, H, 0, gl2.RGBA, gl2.UNSIGNED_BYTE, null);
1141
+ gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MIN_FILTER, gl2.NEAREST);
1142
+ gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MAG_FILTER, gl2.NEAREST);
1143
+ gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_S, gl2.CLAMP_TO_EDGE);
1144
+ gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_T, gl2.CLAMP_TO_EDGE);
1145
+ gl2.bindTexture(gl2.TEXTURE_2D, null);
1146
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, srcFbo);
1147
+ gl2.framebufferTexture2D(gl2.FRAMEBUFFER, gl2.COLOR_ATTACHMENT0, gl2.TEXTURE_2D, srcTex, 0);
1148
+ expect(gl2.checkFramebufferStatus(gl2.FRAMEBUFFER)).toBe(gl2.FRAMEBUFFER_COMPLETE);
1149
+ const dstFbo = gl2.createFramebuffer();
1150
+ const dstTex = gl2.createTexture();
1151
+ gl2.bindTexture(gl2.TEXTURE_2D, dstTex);
1152
+ gl2.texImage2D(gl2.TEXTURE_2D, 0, gl2.RGBA, W, H, 0, gl2.RGBA, gl2.UNSIGNED_BYTE, null);
1153
+ gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MIN_FILTER, gl2.NEAREST);
1154
+ gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MAG_FILTER, gl2.NEAREST);
1155
+ gl2.bindTexture(gl2.TEXTURE_2D, null);
1156
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, dstFbo);
1157
+ gl2.framebufferTexture2D(gl2.FRAMEBUFFER, gl2.COLOR_ATTACHMENT0, gl2.TEXTURE_2D, dstTex, 0);
1158
+ expect(gl2.checkFramebufferStatus(gl2.FRAMEBUFFER)).toBe(gl2.FRAMEBUFFER_COMPLETE);
1159
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, null);
1160
+ const vs300 = `#version 300 es
1161
+ in vec2 a_pos;
1162
+ void main(){gl_Position=vec4(a_pos,0.,1.);}`;
1163
+ const fs300 = `#version 300 es
1164
+ precision mediump float;
1165
+ out vec4 c;
1166
+ void main(){c=vec4(0.,1.,0.,1.);}`;
1167
+ const prog = makeProgram(gl, vs300, fs300);
1168
+ expect(gl2.getProgramParameter(prog, gl2.LINK_STATUS)).toBeTruthy();
1169
+ const vao = gl2.createVertexArray();
1170
+ gl2.bindVertexArray(vao);
1171
+ const vbo = gl2.createBuffer();
1172
+ gl2.bindBuffer(gl2.ARRAY_BUFFER, vbo);
1173
+ gl2.bufferData(gl2.ARRAY_BUFFER, new Float32Array([
1174
+ -1,
1175
+ -1,
1176
+ 3,
1177
+ -1,
1178
+ -1,
1179
+ 3
1180
+ // large triangle covering clip space
1181
+ ]), gl2.STATIC_DRAW);
1182
+ const aPos = gl2.getAttribLocation(prog, "a_pos");
1183
+ gl2.enableVertexAttribArray(aPos);
1184
+ gl2.vertexAttribPointer(aPos, 2, gl2.FLOAT, false, 0, 0);
1185
+ gl2.bindVertexArray(null);
1186
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, srcFbo);
1187
+ gl2.viewport(0, 0, W, H);
1188
+ gl2.clearColor(0, 0, 1, 1);
1189
+ gl2.clear(gl2.COLOR_BUFFER_BIT);
1190
+ gl2.useProgram(prog);
1191
+ gl2.bindVertexArray(vao);
1192
+ gl2.drawArrays(gl2.TRIANGLES, 0, 3);
1193
+ gl2.bindVertexArray(null);
1194
+ expect(gl2.getError()).toBe(gl2.NO_ERROR);
1195
+ const READ_FRAMEBUFFER = 36008;
1196
+ const DRAW_FRAMEBUFFER = 36009;
1197
+ gl2.bindFramebuffer(READ_FRAMEBUFFER, srcFbo);
1198
+ gl2.bindFramebuffer(DRAW_FRAMEBUFFER, dstFbo);
1199
+ expect(gl2.getError()).toBe(gl2.NO_ERROR);
1200
+ gl2.clearBufferfv(gl2.COLOR, 0, [0, 0, 1, 1]);
1201
+ expect(gl2.getError()).toBe(gl2.NO_ERROR);
1202
+ gl2.blitFramebuffer(0, 0, W, H, 0, 0, W, H, gl2.COLOR_BUFFER_BIT, gl2.LINEAR);
1203
+ expect(gl2.getError()).toBe(gl2.NO_ERROR);
1204
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, dstFbo);
1205
+ const pixels = new Uint8Array(4);
1206
+ gl2.readPixels(0, 0, 1, 1, gl2.RGBA, gl2.UNSIGNED_BYTE, pixels);
1207
+ expect(gl2.getError()).toBe(gl2.NO_ERROR);
1208
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, srcFbo);
1209
+ const srcPixels = new Uint8Array(4);
1210
+ gl2.readPixels(0, 0, 1, 1, gl2.RGBA, gl2.UNSIGNED_BYTE, srcPixels);
1211
+ expect(gl2.getError()).toBe(gl2.NO_ERROR);
1212
+ gl2.bindVertexArray(null);
1213
+ gl2.deleteVertexArray(vao);
1214
+ gl2.deleteBuffer(vbo);
1215
+ gl2.deleteProgram(prog);
1216
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, srcFbo);
1217
+ gl2.framebufferTexture2D(gl2.FRAMEBUFFER, gl2.COLOR_ATTACHMENT0, gl2.TEXTURE_2D, null, 0);
1218
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, dstFbo);
1219
+ gl2.framebufferTexture2D(gl2.FRAMEBUFFER, gl2.COLOR_ATTACHMENT0, gl2.TEXTURE_2D, null, 0);
1220
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, null);
1221
+ gl2.deleteTexture(srcTex);
1222
+ gl2.deleteTexture(dstTex);
1223
+ gl2.deleteFramebuffer(srcFbo);
1224
+ gl2.deleteFramebuffer(dstFbo);
1225
+ expect(pixels[0]).toBe(0);
1226
+ expect(pixels[1]).toBe(255);
1227
+ expect(pixels[2]).toBe(0);
1228
+ expect(pixels[3]).toBe(255);
1229
+ expect(srcPixels[0]).toBe(0);
1230
+ expect(srcPixels[1]).toBe(255);
1231
+ expect(srcPixels[2]).toBe(0);
1232
+ expect(srcPixels[3]).toBe(255);
1233
+ });
1234
+ });
1129
1235
  await describe("WebGL2 bufferSubData with UNIFORM_BUFFER", async () => {
1130
1236
  beforeEach(async () => {
1131
1237
  glArea.make_current();
@@ -27,6 +27,7 @@ export declare const CanvasWebGLWidget: {
27
27
  new (params?: Partial<Gtk.GLArea.ConstructorProps>): {
28
28
  _canvas: OurHTMLCanvasElement | null;
29
29
  _readyCallbacks: WebGLReadyCallback[];
30
+ _resizeCallbacks: ((width: number, height: number) => void)[];
30
31
  _renderTag: number | null;
31
32
  _tickCallbackId: number | null;
32
33
  _frameCallback: FrameRequestCallback | null;
@@ -42,6 +43,14 @@ export declare const CanvasWebGLWidget: {
42
43
  * @deprecated Use `onReady()` instead.
43
44
  */
44
45
  onWebGLReady(cb: WebGLReadyCallback): void;
46
+ /**
47
+ * Register a callback invoked whenever the GTK widget is resized.
48
+ * The callback fires alongside the native 'resize' GObject signal and
49
+ * after the DOM 'resize' event has been dispatched on the canvas.
50
+ * Canvas buffer dimensions are NOT automatically updated — consumers
51
+ * should set `canvas.width`/`canvas.height` themselves if desired.
52
+ */
53
+ onResize(cb: (width: number, height: number) => void): void;
45
54
  /**
46
55
  * Schedules a single animation frame callback, matching the browser `requestAnimationFrame` API.
47
56
  * Backed by GTK frame clock (vsync-synced) + the GLArea render signal.
@@ -141,7 +150,7 @@ export declare const CanvasWebGLWidget: {
141
150
  vfunc_get_id(): string;
142
151
  vfunc_get_internal_child<T = GObject.Object>(builder: Gtk.Builder, childname: string): T;
143
152
  vfunc_parser_finished(builder: Gtk.Builder): void;
144
- vfunc_set_buildable_property(builder: Gtk.Builder, name: string, value: GObject.Value | any): void;
153
+ vfunc_set_buildable_property(builder: Gtk.Builder, name: string, value: unknown): void;
145
154
  vfunc_set_id(id: string): void;
146
155
  bind_property(source_property: string, target: GObject.Object, target_property: string, flags: GObject.BindingFlags | null): GObject.Binding;
147
156
  bind_property_full(source_property: string, target: GObject.Object, target_property: string, flags: GObject.BindingFlags | null, transform_to?: GObject.BindingTransformFunc | null, transform_from?: GObject.BindingTransformFunc | null, notify?: GLib.DestroyNotify | null): GObject.Binding;
@@ -169,9 +178,9 @@ export declare const CanvasWebGLWidget: {
169
178
  vfunc_dispatch_properties_changed(n_pspecs: number, pspecs: GObject.ParamSpec): void;
170
179
  vfunc_dispose(): void;
171
180
  vfunc_finalize(): void;
172
- vfunc_get_property(property_id: number, value: GObject.Value | any, pspec: GObject.ParamSpec): void;
181
+ vfunc_get_property(property_id: number, value: unknown, pspec: GObject.ParamSpec): void;
173
182
  vfunc_notify(pspec: GObject.ParamSpec): void;
174
- vfunc_set_property(property_id: number, value: GObject.Value | any, pspec: GObject.ParamSpec): void;
183
+ vfunc_set_property(property_id: number, value: unknown, pspec: GObject.ParamSpec): void;
175
184
  disconnect(id: number): void;
176
185
  set(properties: {
177
186
  [key: string]: any;
@@ -477,7 +486,7 @@ export declare const CanvasWebGLWidget: {
477
486
  set_default_direction(dir: Gtk.TextDirection): void;
478
487
  add_shortcut(shortcut: Gtk.Shortcut): void;
479
488
  bind_template_callback_full(callback_name: string, callback_symbol: GObject.Callback): void;
480
- bind_template_child_full(name: string, internal_child: boolean, struct_offset: number): void;
489
+ bind_template_child_full(name: string, internal_child: boolean, struct_offset: bigint | number): void;
481
490
  get_accessible_role(): Gtk.AccessibleRole;
482
491
  get_activate_signal(): number;
483
492
  get_css_name(): string;
@@ -494,7 +503,7 @@ export declare const CanvasWebGLWidget: {
494
503
  set_template_from_resource(resource_name: string): void;
495
504
  set_template_scope(scope: Gtk.BuilderScope): void;
496
505
  newv(object_type: GObject.GType, parameters: GObject.Parameter[]): GObject.Object;
497
- compat_control(what: number, data?: any | null): number;
506
+ compat_control(what: bigint | number, data?: any | null): number;
498
507
  interface_find_property(g_iface: GObject.TypeInterface, property_name: string): GObject.ParamSpec;
499
508
  interface_install_property(g_iface: GObject.TypeInterface, pspec: GObject.ParamSpec): void;
500
509
  interface_list_properties(g_iface: GObject.TypeInterface): GObject.ParamSpec[];
@@ -15,6 +15,10 @@ export declare class HTMLCanvasElement extends BaseHTMLCanvasElement {
15
15
  set height(_height: number);
16
16
  get clientWidth(): number;
17
17
  get clientHeight(): number;
18
+ /** CSS layout width — same as the GTK-allocated pixel width for a full-window canvas. */
19
+ get offsetWidth(): number;
20
+ /** CSS layout height — same as the GTK-allocated pixel height for a full-window canvas. */
21
+ get offsetHeight(): number;
18
22
  /** Returns the underlying Gtk.GLArea. Used by WebGLRenderingContext for GLSL version detection. */
19
23
  getGlArea(): Gtk.GLArea;
20
24
  /**
@@ -28,3 +28,12 @@ export declare function convertPixels(pixels: ArrayBuffer | Uint8Array | Uint16A
28
28
  export declare function checkFormat(gl: WebGLContextBase, format: GLenum): format is 6406 | 6407 | 6408 | 6409 | 6410;
29
29
  export declare function validCubeTarget(gl: WebGLContextBase, target: GLenum): target is 34069 | 34070 | 34071 | 34072 | 34073 | 34074;
30
30
  export declare function flag<T = Record<string, any>>(options: T, name: keyof T, dflt: boolean): boolean;
31
+ /**
32
+ * Premultiply RGB channels by the alpha channel (in-place copy).
33
+ * Required when UNPACK_PREMULTIPLY_ALPHA_WEBGL is set.
34
+ * Excalibur uses blendFunc(ONE, ONE_MINUS_SRC_ALPHA) (premultiplied alpha
35
+ * blending), so textures must have RGB already multiplied by alpha before
36
+ * upload. Without this, transparent-background PNGs (alpha=0 but RGB=255)
37
+ * bleed through as white rectangles.
38
+ */
39
+ export declare function premultiplyAlpha(data: Uint8Array): Uint8Array;
@@ -56,6 +56,7 @@ export declare abstract class WebGLContextBase {
56
56
  _unpackAlignment: number;
57
57
  _packAlignment: number;
58
58
  _unpackFlipY: boolean;
59
+ _unpackPremultAlpha: boolean;
59
60
  _viewport: Int32Array;
60
61
  _scissorBox: Int32Array;
61
62
  _gtkFboId: number;
@@ -152,6 +152,10 @@ export declare class WebGL2RenderingContext extends WebGLContextBase implements
152
152
  drawBuffers(buffers: GLenum[]): void;
153
153
  drawRangeElements(mode: GLenum, start: GLuint, end: GLuint, count: GLsizei, type: GLenum, offset: GLintptr): void;
154
154
  blitFramebuffer(srcX0: GLint, srcY0: GLint, srcX1: GLint, srcY1: GLint, dstX0: GLint, dstY0: GLint, dstX1: GLint, dstY1: GLint, mask: GLbitfield, filter: GLenum): void;
155
+ clearBufferfv(buffer: GLenum, drawbuffer: GLint, values: Float32List, _srcOffset?: GLuint): void;
156
+ clearBufferiv(buffer: GLenum, drawbuffer: GLint, values: Int32List, _srcOffset?: GLuint): void;
157
+ clearBufferuiv(buffer: GLenum, drawbuffer: GLint, values: Uint32List, _srcOffset?: GLuint): void;
158
+ clearBufferfi(buffer: GLenum, drawbuffer: GLint, depth: GLfloat, stencil: GLint): void;
155
159
  invalidateFramebuffer(target: GLenum, attachments: GLenum[]): void;
156
160
  invalidateSubFramebuffer(target: GLenum, attachments: GLenum[], x: GLint, y: GLint, width: GLsizei, height: GLsizei): void;
157
161
  readBuffer(src: GLenum): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/webgl",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "WebGL module for Gjs",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -39,19 +39,19 @@
39
39
  "WebGL"
40
40
  ],
41
41
  "devDependencies": {
42
- "@gjsify/cli": "^0.1.8",
43
- "@gjsify/unit": "^0.1.8",
42
+ "@gjsify/cli": "^0.1.9",
43
+ "@gjsify/unit": "^0.1.9",
44
44
  "@types/bit-twiddle": "^1.0.3",
45
- "@types/node": "^25.5.2",
45
+ "@types/node": "^25.6.0",
46
46
  "typescript": "^6.0.2"
47
47
  },
48
48
  "dependencies": {
49
- "@girs/gjs": "^4.0.0-rc.1",
50
- "@girs/gtk-4.0": "^4.23.0-4.0.0-rc.1",
51
- "@girs/gwebgl-0.1": "^0.1.0-4.0.0-rc.1",
52
- "@gjsify/dom-elements": "^0.1.8",
53
- "@gjsify/event-bridge": "^0.1.8",
54
- "@gjsify/utils": "^0.1.8",
49
+ "@girs/gjs": "^4.0.0-rc.2",
50
+ "@girs/gtk-4.0": "^4.23.0-4.0.0-rc.2",
51
+ "@girs/gwebgl-0.1": "^0.1.0-4.0.0-rc.2",
52
+ "@gjsify/dom-elements": "^0.1.9",
53
+ "@gjsify/event-bridge": "^0.1.9",
54
+ "@gjsify/utils": "^0.1.9",
55
55
  "bit-twiddle": "^1.0.2",
56
56
  "glsl-tokenizer": "^2.1.5"
57
57
  }