'use strict'; // Thin browser shim for TaskForge. All app logic and rendering is in Jai (taskforge.wasm); this file only // 1. forwards WebGL2 calls (same names as OpenGL, see src/web_gl.jai), // 2. forwards input events to the exported web_* procedures, // 3. provides storage (localStorage), clipboard and file import/export. // Pointers and s64 values arrive as BigInt (WASM64), everything else as Number. const canvas = document.getElementById('app'); const message = document.getElementById('message'); const importInput = document.getElementById('import'); const gl = canvas.getContext('webgl2', { antialias: false, alpha: false, premultipliedAlpha: false }); let memory = null; let X = null; // WASM exports. const decoder = new TextDecoder(); const encoder = new TextEncoder(); const u8 = () => new Uint8Array(memory.buffer); const dv = () => new DataView(memory.buffer); const str = (ptr, len) => decoder.decode(u8().subarray(Number(ptr), Number(ptr) + Number(len))); const cstr = (ptr) => { const m = u8(); const start = Number(ptr); let end = start; while (m[end]) end++; return decoder.decode(m.subarray(start, end)); }; // WebGL objects are referenced from Jai by integer handles. const objects = [null]; const handle = (o) => { objects.push(o); return objects.length - 1; }; function writeInfoLog(log, maxLength, lengthPtr, bufferPtr) { const bytes = encoder.encode(log || '').subarray(0, Math.max(0, maxLength - 1)); u8().set(bytes, Number(bufferPtr)); u8()[Number(bufferPtr) + bytes.length] = 0; if (lengthPtr) dv().setInt32(Number(lengthPtr), bytes.length, true); } function genObjects(n, out, create) { for (let i = 0; i < n; i++) dv().setUint32(Number(out) + i * 4, handle(create()), true); } let pendingLoad = null; let consoleLine = ''; const env = { // Jai runtime. wasm_write_string(count, data, toStderr) { consoleLine += str(data, count); const lines = consoleLine.split('\n'); consoleLine = lines.pop(); for (const line of lines) (toStderr ? console.error : console.log)(line); }, wasm_debug_break() { debugger; }, memcmp(a, b, n) { const m = u8(); const pa = Number(a), pb = Number(b); for (let i = 0; i < Number(n); i++) { const d = m[pa + i] - m[pb + i]; if (d) return d; } return 0; }, // OpenGL ES 3.0 -> WebGL2. glCreateShader: (type) => handle(gl.createShader(type)), glShaderSource(shader, count, strings, lengths) { const d = dv(); let source = ''; for (let i = 0; i < count; i++) { const p = d.getBigUint64(Number(strings) + i * 8, true); const len = lengths ? d.getInt32(Number(lengths) + i * 4, true) : -1; source += len >= 0 ? str(p, len) : cstr(p); } gl.shaderSource(objects[shader], source); }, glCompileShader: (shader) => gl.compileShader(objects[shader]), glGetShaderiv(shader, pname, out) { const value = pname === 0x8B84 ? (gl.getShaderInfoLog(objects[shader]) || '').length + 1 : Number(gl.getShaderParameter(objects[shader], pname)); dv().setInt32(Number(out), value, true); }, glGetShaderInfoLog: (shader, max, len, buf) => writeInfoLog(gl.getShaderInfoLog(objects[shader]), max, len, buf), glCreateProgram: () => handle(gl.createProgram()), glAttachShader: (program, shader) => gl.attachShader(objects[program], objects[shader]), glBindAttribLocation: (program, index, name) => gl.bindAttribLocation(objects[program], index, cstr(name)), glLinkProgram: (program) => gl.linkProgram(objects[program]), glGetProgramiv(program, pname, out) { const value = pname === 0x8B84 ? (gl.getProgramInfoLog(objects[program]) || '').length + 1 : Number(gl.getProgramParameter(objects[program], pname)); dv().setInt32(Number(out), value, true); }, glGetProgramInfoLog: (program, max, len, buf) => writeInfoLog(gl.getProgramInfoLog(objects[program]), max, len, buf), glUseProgram: (program) => gl.useProgram(objects[program]), glGetUniformLocation: (program, name) => handle(gl.getUniformLocation(objects[program], cstr(name))), glUniform1i: (loc, v) => gl.uniform1i(objects[loc], v), glUniform1f: (loc, v) => gl.uniform1f(objects[loc], v), glUniform2f: (loc, a, b) => gl.uniform2f(objects[loc], a, b), glGenVertexArrays: (n, out) => genObjects(n, out, () => gl.createVertexArray()), glBindVertexArray: (vao) => gl.bindVertexArray(objects[vao]), glGenBuffers: (n, out) => genObjects(n, out, () => gl.createBuffer()), glBindBuffer: (target, buffer) => gl.bindBuffer(target, objects[buffer]), glBufferData(target, size, data, usage) { if (data) gl.bufferData(target, u8().subarray(Number(data), Number(data) + Number(size)), usage); else gl.bufferData(target, Number(size), usage); }, glVertexAttribPointer: (index, size, type, normalized, stride, offset) => gl.vertexAttribPointer(index, size, type, !!normalized, stride, Number(offset)), glEnableVertexAttribArray: (index) => gl.enableVertexAttribArray(index), glGenTextures: (n, out) => genObjects(n, out, () => gl.createTexture()), glBindTexture: (target, texture) => gl.bindTexture(target, objects[texture]), glActiveTexture: (unit) => gl.activeTexture(unit), glTexImage2D(target, level, internalFormat, w, h, border, format, type, pixels) { const bytesPerPixel = format === gl.RED ? 1 : 4; const data = pixels ? u8().subarray(Number(pixels), Number(pixels) + w * h * bytesPerPixel) : null; gl.texImage2D(target, level, internalFormat, w, h, border, format, type, data); }, glTexParameteri: (target, pname, param) => gl.texParameteri(target, pname, param), glTexSubImage2D(target, level, x, y, w, h, format, type, pixels) { gl.texSubImage2D(target, level, x, y, w, h, format, type, u8().subarray(Number(pixels), Number(pixels) + w * h * 4)); }, glDeleteTextures(n, ptr) { for (let i = 0; i < n; i++) { const h = dv().getUint32(Number(ptr) + i * 4, true); if (objects[h]) gl.deleteTexture(objects[h]); objects[h] = null; } }, glGenerateMipmap: (target) => gl.generateMipmap(target), glPixelStorei: (pname, param) => gl.pixelStorei(pname, param), glEnable: (cap) => gl.enable(cap), glDisable: (cap) => gl.disable(cap), glBlendFunc: (s, d) => gl.blendFunc(s, d), glScissor: (x, y, w, h) => gl.scissor(x, y, w, h), glViewport: (x, y, w, h) => gl.viewport(x, y, w, h), glClearColor: (r, g, b, a) => gl.clearColor(r, g, b, a), glClear: (mask) => gl.clear(mask), glDrawArrays: (mode, first, count) => gl.drawArrays(mode, first, count), // Platform services. js_storage_save(key, keyCount, data, count) { try { localStorage.setItem(str(key, keyCount), str(data, count)); } catch (e) { console.error('Saving failed:', e); } }, js_storage_load_size(key, keyCount) { const text = localStorage.getItem(str(key, keyCount)); pendingLoad = text ? encoder.encode(text) : null; return BigInt(pendingLoad ? pendingLoad.length : 0); }, js_storage_load(dest) { if (pendingLoad) u8().set(pendingLoad, Number(dest)); pendingLoad = null; }, js_clipboard_write(data, count) { navigator.clipboard?.writeText(str(data, count)).catch(() => {}); }, js_set_cursor(kind) { canvas.style.cursor = ['default', 'pointer', 'text', 'grabbing'][kind] || 'default'; }, js_download(data, count) { const blob = new Blob([u8().slice(Number(data), Number(data) + Number(count))], { type: 'text/plain' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'project.taskforge'; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }, js_request_import() { importInput.click(); }, js_http(tag, method, methodCount, path, pathCount, body, bodyCount, token, tokenCount, key, keyCount) { const verb = str(method, methodCount); const headers = {}; if (verb !== 'GET') headers['Content-Type'] = 'text/plain; charset=utf-8'; if (tokenCount) headers['Authorization'] = 'Bearer ' + str(token, tokenCount); const keyText = str(key, keyCount); const options = { method: verb, headers }; if (verb !== 'GET') options.body = u8().slice(Number(body), Number(body) + Number(bodyCount)); fetch(str(path, pathCount), options) .then(async (response) => [response.status, new Uint8Array(await response.arrayBuffer())]) .catch(() => [0, new Uint8Array(0)]) .then(([status, bytes]) => { const [kp, kn] = toWasm(encoder.encode(keyText)); const [bp, bn] = toWasm(bytes); X.web_http_response(tag, status, bp, bn, kp, kn); requestFrame(); }); }, // Images are decoded by the browser, scaled down to at most 2048 px, and handed back as RGBA. js_decode_image(key, keyCount, data, count) { const keyText = str(key, keyCount); const blob = new Blob([u8().slice(Number(data), Number(data) + Number(count))]); decodeImage(blob).then((image) => { const [kp, kn] = toWasm(encoder.encode(keyText)); if (!image) { X.web_image_decoded(kp, kn, 0, 0, 0, 0n); requestFrame(); return; } const [pp] = toWasm(image.pixels); X.web_image_decoded(kp, kn, 1, image.width, image.height, pp); requestFrame(); }); }, // Size, duration and a PNG poster (first frame) of a video. js_probe_video(key, keyCount, data, count) { const keyText = str(key, keyCount); const url = URL.createObjectURL(new Blob([u8().slice(Number(data), Number(data) + Number(count))])); probeVideo(url).then((info) => { URL.revokeObjectURL(url); const [kp, kn] = toWasm(encoder.encode(keyText)); if (!info) { X.web_video_probed(kp, kn, 0, 0, 0, 0, 0n, 0n); requestFrame(); return; } const [pp, pn] = toWasm(info.png); X.web_video_probed(kp, kn, 1, info.width, info.height, info.duration, pp, pn); requestFrame(); }); }, // Local projects keep attachments in IndexedDB (localStorage is far too small). js_blob_save(key, keyCount, data, count) { const keyText = str(key, keyCount); const bytes = u8().slice(Number(data), Number(data) + Number(count)); idb('readwrite', (store) => store.put(bytes, keyText)); }, js_blob_load(key, keyCount) { const keyText = str(key, keyCount); idb('readonly', (store) => store.get(keyText)).then((bytes) => { const [kp, kn] = toWasm(encoder.encode(keyText)); if (!bytes) { X.web_blob_loaded(kp, kn, 0, 0n, 0n); requestFrame(); return; } const [dp, dn] = toWasm(bytes); X.web_blob_loaded(kp, kn, 1, dp, dn); requestFrame(); }).catch(() => { const [kp, kn] = toWasm(encoder.encode(keyText)); X.web_blob_loaded(kp, kn, 0, 0n, 0n); requestFrame(); }); }, js_pick_files() { mediaInput.click(); }, // Grabs one frame of a screen, window or tab the user picks. js_capture_screenshot() { captureScreen().then((blob) => { if (blob) addFile(screenshotName(), blob); }) .catch((e) => console.warn('Screen capture cancelled:', e)); }, // One video at a time plays in an off-screen