| /* | |
| * Copyright (C) 2026 Behdad Esfahbod | |
| * | |
| * This is part of HarfBuzz, a text shaping library. | |
| * | |
| * Permission is hereby granted, without written agreement and without | |
| * license or royalty fees, to use, copy, modify, and distribute this | |
| * software and its documentation for any purpose, provided that the | |
| * above copyright notice and the following two paragraphs appear in | |
| * all copies of this software. | |
| * | |
| * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR | |
| * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES | |
| * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN | |
| * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH | |
| * DAMAGE. | |
| * | |
| * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, | |
| * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
| * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS | |
| * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO | |
| * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | |
| * | |
| * Author(s): Behdad Esfahbod | |
| */ | |
| /* | |
| * WebGPU web demo. | |
| * | |
| * Uses Emscripten WebGPU bindings instead of WebGL. | |
| * Uses demo_view for input/view (with HB_GPU_NO_GLFW). | |
| */ | |
| #define HB_NO_PRAGMA_GCC_DIAGNOSTIC | |
| #include <emscripten.h> | |
| #include <emscripten/html5.h> | |
| #include <webgpu/webgpu.h> | |
| #include <hb.h> | |
| #include <hb-gpu.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <stdio.h> | |
| #include <math.h> | |
| #include <string> | |
| /* GL type stubs for headers */ | |
| #include "gl-stub.h" | |
| #include "../demo-common.h" | |
| #include "../demo-font.h" | |
| #include "../demo-buffer.h" | |
| #include "../demo-renderer.h" | |
| #include "../demo-view.h" | |
| #include "../default-text-combined.hh" | |
| #include "../default-text-en.hh" | |
| #include "../default-font.hh" | |
| /* ---- WGSL demo shader ---- */ | |
| #include "../demo-shader-wgsl.hh" | |
| /* ---- Uniform buffer layout (must match WGSL struct) ---- */ | |
| struct Uniforms { | |
| float mvp[16]; | |
| float viewport[2]; | |
| float gamma; | |
| float stem_darkening; | |
| float foreground[4]; | |
| float debug; | |
| float _pad[3]; | |
| }; | |
| /* ---- Atlas (storage buffer) ---- */ | |
| struct atlas_t { | |
| WGPUBuffer buf; | |
| int32_t *shadow; | |
| unsigned capacity; | |
| unsigned cursor; | |
| bool dirty; | |
| }; | |
| static atlas_t atlas; | |
| static unsigned | |
| atlas_alloc_cb (void *ctx, const char *data, unsigned len_bytes) | |
| { | |
| atlas_t *at = (atlas_t *) ctx; | |
| unsigned len_texels = len_bytes / 8; | |
| if (at->cursor + len_texels > at->capacity) | |
| { | |
| fprintf (stderr, "Ran out of atlas memory\n"); | |
| abort (); | |
| } | |
| unsigned offset = at->cursor; | |
| at->cursor += len_texels; | |
| const int16_t *src = (const int16_t *) data; | |
| int32_t *dst = at->shadow + offset * 4; | |
| for (unsigned i = 0; i < len_texels * 4; i++) | |
| dst[i] = (int32_t) src[i]; | |
| at->dirty = true; | |
| return offset; | |
| } | |
| static unsigned | |
| atlas_get_used_cb (void *ctx) { return ((atlas_t *) ctx)->cursor; } | |
| static void | |
| atlas_clear_cb (void *ctx) { ((atlas_t *) ctx)->cursor = 0; ((atlas_t *) ctx)->dirty = true; } | |
| /* ---- WebGPU state ---- */ | |
| static WGPUAdapter g_adapter; | |
| static WGPUDevice g_device; | |
| static WGPUQueue g_queue; | |
| static WGPUSurface g_surface; | |
| static WGPURenderPipeline g_pipeline; | |
| static WGPUBuffer g_uniform_buf; | |
| static WGPUBuffer g_vertex_buf; | |
| static unsigned g_vertex_buf_capacity; | |
| static WGPUBindGroup g_bind_group; | |
| static WGPUTextureFormat g_surface_format; | |
| static int canvas_w, canvas_h; | |
| static int css_w, css_h; | |
| /* ---- WebGPU renderer (implements demo_renderer_t) ---- */ | |
| struct demo_renderer_webgpu_t : demo_renderer_t | |
| { | |
| float bg_r, bg_g, bg_b, bg_a; | |
| float fg_r, fg_g, fg_b, fg_a; | |
| float gamma; | |
| demo_atlas_t *demo_atlas; | |
| demo_renderer_webgpu_t () | |
| : bg_r (1), bg_g (1), bg_b (1), bg_a (1), | |
| fg_r (0), fg_g (0), fg_b (0), fg_a (1), | |
| gamma (1), demo_atlas (nullptr) {} | |
| ~demo_renderer_webgpu_t () override | |
| { | |
| if (demo_atlas) | |
| demo_atlas_destroy (demo_atlas); | |
| } | |
| demo_atlas_t *get_atlas () override { return demo_atlas; } | |
| void setup () override | |
| { | |
| /* Gamma defaults to 2.2 correction */ | |
| gamma = 1.f / 2.2f; | |
| } | |
| void set_gamma (float g) override { gamma = g; } | |
| void set_foreground (float r, float g, float b, float a) override | |
| { fg_r = r; fg_g = g; fg_b = b; fg_a = a; } | |
| void set_background (float r, float g, float b, float a) override | |
| { bg_r = r; bg_g = g; bg_b = b; bg_a = a; } | |
| void set_debug (bool enabled) override { debug_mode = enabled; } | |
| void set_stem_darkening (bool enabled) override { stem_mode = enabled; } | |
| bool debug_mode = false; | |
| bool stem_mode = true; | |
| void toggle_vsync (bool &vsync) override { /* always vsync in browser */ } | |
| void display (glyph_vertex_t *vertices, unsigned int count, | |
| unsigned generation, | |
| int width, int height, float mat[16]) override | |
| { | |
| WGPUSurfaceTexture surfTex; | |
| wgpuSurfaceGetCurrentTexture (g_surface, &surfTex); | |
| if (surfTex.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal && | |
| surfTex.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) | |
| return; | |
| WGPUTextureView view = wgpuTextureCreateView (surfTex.texture, NULL); | |
| /* Update uniforms */ | |
| Uniforms u; | |
| memcpy (u.mvp, mat, sizeof (u.mvp)); | |
| u.viewport[0] = (float) width; | |
| u.viewport[1] = (float) height; | |
| u.gamma = gamma; | |
| u.stem_darkening = stem_mode ? 1.f : 0.f; | |
| u.foreground[0] = fg_r; u.foreground[1] = fg_g; | |
| u.foreground[2] = fg_b; u.foreground[3] = fg_a; | |
| u.debug = debug_mode ? 1.f : 0.f; | |
| wgpuQueueWriteBuffer (g_queue, g_uniform_buf, 0, &u, sizeof (u)); | |
| /* Upload atlas if dirty */ | |
| if (atlas.dirty && atlas.cursor > 0) | |
| { | |
| wgpuQueueWriteBuffer (g_queue, atlas.buf, 0, | |
| atlas.shadow, atlas.cursor * 4 * sizeof (int32_t)); | |
| atlas.dirty = false; | |
| } | |
| /* Upload vertices */ | |
| if (count > 0) | |
| { | |
| unsigned needed = count * sizeof (glyph_vertex_t); | |
| if (needed > g_vertex_buf_capacity) | |
| { | |
| if (g_vertex_buf) | |
| wgpuBufferRelease (g_vertex_buf); | |
| WGPUBufferDescriptor desc = {}; | |
| desc.size = needed; | |
| desc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; | |
| g_vertex_buf = wgpuDeviceCreateBuffer (g_device, &desc); | |
| g_vertex_buf_capacity = needed; | |
| } | |
| wgpuQueueWriteBuffer (g_queue, g_vertex_buf, 0, vertices, needed); | |
| } | |
| /* Render pass */ | |
| WGPURenderPassColorAttachment colorAtt = {}; | |
| colorAtt.view = view; | |
| colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; | |
| colorAtt.loadOp = WGPULoadOp_Clear; | |
| colorAtt.storeOp = WGPUStoreOp_Store; | |
| colorAtt.clearValue = {bg_r, bg_g, bg_b, bg_a}; | |
| WGPURenderPassDescriptor rpDesc = {}; | |
| rpDesc.colorAttachmentCount = 1; | |
| rpDesc.colorAttachments = &colorAtt; | |
| WGPUCommandEncoderDescriptor ceDesc = {}; | |
| WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder (g_device, &ceDesc); | |
| WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass (encoder, &rpDesc); | |
| if (count > 0) | |
| { | |
| wgpuRenderPassEncoderSetPipeline (pass, g_pipeline); | |
| wgpuRenderPassEncoderSetBindGroup (pass, 0, g_bind_group, 0, NULL); | |
| wgpuRenderPassEncoderSetVertexBuffer (pass, 0, g_vertex_buf, 0, | |
| count * sizeof (glyph_vertex_t)); | |
| wgpuRenderPassEncoderDraw (pass, count, 1, 0, 0); | |
| } | |
| wgpuRenderPassEncoderEnd (pass); | |
| wgpuRenderPassEncoderRelease (pass); | |
| WGPUCommandBufferDescriptor cbDesc = {}; | |
| WGPUCommandBuffer cmdBuf = wgpuCommandEncoderFinish (encoder, &cbDesc); | |
| wgpuCommandEncoderRelease (encoder); | |
| wgpuQueueSubmit (g_queue, 1, &cmdBuf); | |
| wgpuCommandBufferRelease (cmdBuf); | |
| wgpuTextureViewRelease (view); | |
| wgpuTextureRelease (surfTex.texture); | |
| } | |
| }; | |
| static demo_renderer_webgpu_t *renderer; | |
| /* ---- Demo state ---- */ | |
| static demo_view_t *vu; | |
| static demo_buffer_t *buffer; | |
| static demo_font_t *current_demo_font; | |
| static hb_blob_t *current_blob; | |
| static hb_face_t *current_face; | |
| static hb_font_t *current_font; | |
| static char *current_text; | |
| static bool custom_text; | |
| static const char *arg_text = nullptr; | |
| static const char *arg_font = nullptr; | |
| /* ---- Buffer rebuild ---- */ | |
| static void | |
| rebuild_buffer (const char *text) | |
| { | |
| if (!text) | |
| text = default_text_en; | |
| if (text != current_text) | |
| { | |
| /* strdup first -- text may alias current_text. */ | |
| char *new_text = strdup (text); | |
| free (current_text); | |
| current_text = new_text; | |
| } | |
| demo_font_clear_cache (current_demo_font); | |
| atlas_clear_cb (&atlas); | |
| demo_buffer_clear (buffer); | |
| demo_point_t top_left = {0, 0}; | |
| demo_buffer_move_to (buffer, &top_left); | |
| if (current_text) | |
| demo_buffer_add_text (buffer, current_text, current_demo_font, 1); | |
| atlas.dirty = true; | |
| demo_view_reset (vu); | |
| } | |
| /* ---- Exported functions for JS ---- */ | |
| extern "C" { | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_reset () | |
| { | |
| demo_view_reset (vu); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_request_redraw () | |
| { | |
| demo_view_request_redraw (vu); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_cancel_gesture () | |
| { | |
| demo_view_cancel_gesture (vu); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_load_font (const char *data, int len) | |
| { | |
| hb_blob_t *blob = hb_blob_create (data, len, | |
| HB_MEMORY_MODE_DUPLICATE, | |
| NULL, NULL); | |
| hb_face_t *face = hb_face_create (blob, 0); | |
| hb_font_t *font = hb_font_create (face); | |
| hb_blob_destroy (current_blob); | |
| hb_face_destroy (current_face); | |
| hb_font_destroy (current_font); | |
| current_blob = blob; | |
| current_face = face; | |
| current_font = font; | |
| demo_font_destroy (current_demo_font); | |
| current_demo_font = demo_font_create (font, renderer->get_atlas (), false); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| demo_font_print_stats (current_demo_font); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_text (const char *utf8) | |
| { | |
| custom_text = true; | |
| rebuild_buffer (utf8); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_variations (const char *settings) | |
| { | |
| if (!current_font) return; | |
| hb_variation_t vars[32]; | |
| unsigned n = 0; | |
| const char *p = settings; | |
| while (p && *p && n < (sizeof vars / sizeof vars[0])) | |
| { | |
| const char *end = strchr (p, ','); | |
| int len = end ? (int) (end - p) : (int) strlen (p); | |
| if (hb_variation_from_string (p, len, &vars[n])) | |
| n++; | |
| p = end ? end + 1 : nullptr; | |
| } | |
| hb_font_set_variations (current_font, vars, n); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_palette (unsigned palette_index) | |
| { | |
| if (!current_demo_font) return; | |
| demo_font_set_palette (current_demo_font, palette_index); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| } | |
| EMSCRIPTEN_KEEPALIVE const char * | |
| web_get_text () | |
| { | |
| return current_text; | |
| } | |
| } /* extern "C" */ | |
| /* ---- Input handling (feeds into demo_view) ---- */ | |
| /* GLFW-compatible button/action constants */ | |
| #define BUTTON_LEFT 0 | |
| #define BUTTON_RIGHT 1 | |
| #define BUTTON_MIDDLE 2 | |
| #define ACTION_RELEASE 0 | |
| #define ACTION_PRESS 1 | |
| static int active_buttons; | |
| static EM_BOOL | |
| on_mousedown (int type, const EmscriptenMouseEvent *e, void *ud) | |
| { | |
| int button = e->button == 2 ? BUTTON_RIGHT : e->button == 1 ? BUTTON_MIDDLE : BUTTON_LEFT; | |
| active_buttons |= (1 << button); | |
| demo_view_motion_func (vu, e->targetX, e->targetY); | |
| demo_view_mouse_func (vu, button, ACTION_PRESS, 0); | |
| return EM_TRUE; | |
| } | |
| static EM_BOOL | |
| on_mouseup (int type, const EmscriptenMouseEvent *e, void *ud) | |
| { | |
| int button = e->button == 2 ? BUTTON_RIGHT : e->button == 1 ? BUTTON_MIDDLE : BUTTON_LEFT; | |
| if (!(active_buttons & (1 << button))) | |
| return EM_FALSE; /* Not our drag — ignore (e.g. UI button click) */ | |
| active_buttons &= ~(1 << button); | |
| /* Don't call motion_func here — it would zero dx/dy needed for | |
| * drag-release velocity detection. Cursor is already up to date | |
| * from the last mousemove event. */ | |
| demo_view_mouse_func (vu, button, ACTION_RELEASE, 0); | |
| return EM_TRUE; | |
| } | |
| static EM_BOOL | |
| on_mousemove (int type, const EmscriptenMouseEvent *e, void *ud) | |
| { | |
| demo_view_motion_func (vu, e->targetX, e->targetY); | |
| return EM_TRUE; | |
| } | |
| static EM_BOOL | |
| on_wheel (int type, const EmscriptenWheelEvent *e, void *ud) | |
| { | |
| double delta = -e->deltaY; | |
| if (e->deltaMode == DOM_DELTA_PIXEL) | |
| delta /= 100.0; | |
| else if (e->deltaMode == DOM_DELTA_LINE) | |
| delta *= 3.0; | |
| demo_view_scroll_func (vu, 0, delta); | |
| return EM_TRUE; | |
| } | |
| /* Key mapping from DOM key strings to GLFW key codes */ | |
| static int | |
| dom_key_to_glfw (const char *key) | |
| { | |
| if (!strcmp (key, " ")) return 32; /* GLFW_KEY_SPACE */ | |
| if (!strcmp (key, "Escape")) return 256; /* GLFW_KEY_ESCAPE */ | |
| if (!strcmp (key, "Backspace")) return 259; /* GLFW_KEY_BACKSPACE */ | |
| if (!strcmp (key, "ArrowUp")) return 265; /* GLFW_KEY_UP */ | |
| if (!strcmp (key, "ArrowDown")) return 264; /* GLFW_KEY_DOWN */ | |
| if (!strcmp (key, "ArrowLeft")) return 263; /* GLFW_KEY_LEFT */ | |
| if (!strcmp (key, "ArrowRight"))return 262; /* GLFW_KEY_RIGHT */ | |
| if (key[0] >= 'a' && key[0] <= 'z' && key[1] == 0) | |
| return key[0] - 'a' + 65; /* GLFW_KEY_A..Z */ | |
| return 0; | |
| } | |
| static EM_BOOL | |
| on_keydown (int type, const EmscriptenKeyboardEvent *e, void *ud) | |
| { | |
| /* Let char_func handle printable characters */ | |
| int key = dom_key_to_glfw (e->key); | |
| if (key) | |
| demo_view_key_func (vu, key, 0, ACTION_PRESS, e->shiftKey ? 1 : 0); | |
| /* Also send as char for =/-/etc */ | |
| if (e->key[0] && !e->key[1]) | |
| demo_view_char_func (vu, (unsigned int) e->key[0]); | |
| return EM_FALSE; | |
| } | |
| /* Touch handling */ | |
| static double pinch_dist; | |
| static double pinch_angle; | |
| static double pinch_cx, pinch_cy; | |
| static double gesture_x, gesture_y; | |
| static bool pinch_active; | |
| static bool single_pan_active; | |
| static bool three_finger_active; | |
| static bool skip_next_pinch_move; | |
| static EM_BOOL | |
| on_touchstart (int type, const EmscriptenTouchEvent *e, void *ud) | |
| { | |
| if (e->numTouches == 1) | |
| { | |
| single_pan_active = false; | |
| demo_view_motion_func (vu, e->touches[0].targetX, e->touches[0].targetY); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_PRESS, 0); | |
| } | |
| else if (e->numTouches == 2) | |
| { | |
| pinch_active = true; | |
| single_pan_active = false; | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_RELEASE, 0); | |
| double dx = e->touches[1].targetX - e->touches[0].targetX; | |
| double dy = e->touches[1].targetY - e->touches[0].targetY; | |
| pinch_dist = sqrt (dx * dx + dy * dy); | |
| pinch_angle = atan2 (dy, dx); | |
| pinch_cx = (e->touches[0].targetX + e->touches[1].targetX) / 2.0; | |
| pinch_cy = (e->touches[0].targetY + e->touches[1].targetY) / 2.0; | |
| gesture_x = pinch_cx; | |
| gesture_y = pinch_cy; | |
| skip_next_pinch_move = false; | |
| } | |
| else if (e->numTouches >= 3) | |
| { | |
| pinch_active = false; | |
| single_pan_active = false; | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_RELEASE, 0); | |
| three_finger_active = true; | |
| skip_next_pinch_move = false; | |
| double cx = (e->touches[0].targetX + e->touches[1].targetX + e->touches[2].targetX) / 3.0; | |
| double cy = (e->touches[0].targetY + e->touches[1].targetY + e->touches[2].targetY) / 3.0; | |
| gesture_x = cx; | |
| gesture_y = cy; | |
| demo_view_motion_func (vu, cx, cy); | |
| demo_view_mouse_func (vu, BUTTON_RIGHT, ACTION_PRESS, 0); | |
| } | |
| return EM_TRUE; | |
| } | |
| static EM_BOOL | |
| on_touchmove (int type, const EmscriptenTouchEvent *e, void *ud) | |
| { | |
| if (e->numTouches >= 3 && three_finger_active) | |
| { | |
| pinch_active = false; | |
| double cx = (e->touches[0].targetX + e->touches[1].targetX + e->touches[2].targetX) / 3.0; | |
| double cy = (e->touches[0].targetY + e->touches[1].targetY + e->touches[2].targetY) / 3.0; | |
| gesture_x = cx; | |
| gesture_y = cy; | |
| demo_view_motion_func (vu, cx, cy); | |
| } | |
| else if (e->numTouches == 1 && single_pan_active) | |
| { | |
| gesture_x = e->touches[0].targetX; | |
| gesture_y = e->touches[0].targetY; | |
| demo_view_motion_func (vu, gesture_x, gesture_y); | |
| } | |
| else if (e->numTouches == 1) | |
| { | |
| demo_view_motion_func (vu, e->touches[0].targetX, e->touches[0].targetY); | |
| } | |
| else if (e->numTouches == 2) | |
| { | |
| pinch_active = true; | |
| double dx = e->touches[1].targetX - e->touches[0].targetX; | |
| double dy = e->touches[1].targetY - e->touches[0].targetY; | |
| double dist = sqrt (dx * dx + dy * dy); | |
| double angle = atan2 (dy, dx); | |
| double cx = (e->touches[0].targetX + e->touches[1].targetX) / 2.0; | |
| double cy = (e->touches[0].targetY + e->touches[1].targetY) / 2.0; | |
| if (skip_next_pinch_move) | |
| { | |
| pinch_dist = dist; | |
| pinch_angle = angle; | |
| pinch_cx = cx; | |
| pinch_cy = cy; | |
| gesture_x = cx; | |
| gesture_y = cy; | |
| skip_next_pinch_move = false; | |
| return EM_TRUE; | |
| } | |
| double dAngle = angle - pinch_angle; | |
| if (dAngle > M_PI) dAngle -= 2 * M_PI; | |
| if (dAngle < -M_PI) dAngle += 2 * M_PI; | |
| double factor = pinch_dist > 0 ? dist / pinch_dist : 1.0; | |
| demo_view_pinch (vu, cx - pinch_cx, cy - pinch_cy, | |
| factor, dAngle, cx, cy, css_w, css_h); | |
| pinch_dist = dist; | |
| pinch_angle = angle; | |
| pinch_cx = cx; | |
| pinch_cy = cy; | |
| gesture_x = cx; | |
| gesture_y = cy; | |
| } | |
| return EM_TRUE; | |
| } | |
| static EM_BOOL | |
| on_touchend (int type, const EmscriptenTouchEvent *e, void *ud) | |
| { | |
| bool pinch_ending = pinch_active && e->numTouches < 2; | |
| bool single_pan_ending = single_pan_active && e->numTouches == 0; | |
| if (three_finger_active && e->numTouches < 3) | |
| { | |
| three_finger_active = false; | |
| double release_x = gesture_x, release_y = gesture_y; | |
| if (e->numTouches == 2) | |
| { | |
| release_x = (e->touches[0].targetX + e->touches[1].targetX) / 2.0; | |
| release_y = (e->touches[0].targetY + e->touches[1].targetY) / 2.0; | |
| } | |
| demo_view_motion_func (vu, release_x, release_y); | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_RIGHT, ACTION_RELEASE, 0); | |
| /* Reinitialize pinch state so 3→2 transition doesn't jump */ | |
| if (e->numTouches == 2) | |
| { | |
| double dx = e->touches[1].targetX - e->touches[0].targetX; | |
| double dy = e->touches[1].targetY - e->touches[0].targetY; | |
| pinch_dist = sqrt (dx * dx + dy * dy); | |
| pinch_angle = atan2 (dy, dx); | |
| pinch_cx = release_x; | |
| pinch_cy = release_y; | |
| gesture_x = release_x; | |
| gesture_y = release_y; | |
| pinch_active = true; | |
| skip_next_pinch_move = true; | |
| } | |
| else | |
| { | |
| pinch_active = false; | |
| skip_next_pinch_move = false; | |
| } | |
| } | |
| else if (pinch_ending) | |
| { | |
| pinch_active = false; | |
| skip_next_pinch_move = false; | |
| if (e->numTouches == 1) | |
| { | |
| single_pan_active = true; | |
| gesture_x = e->touches[0].targetX; | |
| gesture_y = e->touches[0].targetY; | |
| demo_view_motion_func (vu, gesture_x, gesture_y); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_PRESS, 0); | |
| } | |
| } | |
| else if (single_pan_ending) | |
| { | |
| single_pan_active = false; | |
| demo_view_motion_func (vu, gesture_x, gesture_y); | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_RELEASE, 0); | |
| } | |
| else | |
| { | |
| skip_next_pinch_move = false; | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_RELEASE, 0); | |
| } | |
| return EM_TRUE; | |
| } | |
| static EM_BOOL | |
| on_touchcancel (int type, const EmscriptenTouchEvent *e, void *ud) | |
| { | |
| if (three_finger_active) | |
| { | |
| three_finger_active = false; | |
| demo_view_motion_func (vu, gesture_x, gesture_y); | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_RIGHT, ACTION_RELEASE, 0); | |
| } | |
| else if (pinch_active) | |
| { | |
| pinch_active = false; | |
| } | |
| else if (single_pan_active) | |
| { | |
| single_pan_active = false; | |
| demo_view_motion_func (vu, gesture_x, gesture_y); | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_RELEASE, 0); | |
| } | |
| else | |
| { | |
| demo_view_cancel_gesture (vu); | |
| demo_view_mouse_func (vu, BUTTON_LEFT, ACTION_RELEASE, 0); | |
| } | |
| pinch_active = false; | |
| skip_next_pinch_move = false; | |
| return EM_TRUE; | |
| } | |
| /* ---- Main loop ---- */ | |
| static void | |
| check_resize () | |
| { | |
| int w, h; | |
| emscripten_get_canvas_element_size ("#canvas", &w, &h); | |
| if (w != canvas_w || h != canvas_h) | |
| { | |
| canvas_w = w; | |
| canvas_h = h; | |
| double cw, ch; | |
| emscripten_get_element_css_size ("#canvas", &cw, &ch); | |
| css_w = (int) cw; | |
| css_h = (int) ch; | |
| WGPUSurfaceConfiguration config = {}; | |
| config.device = g_device; | |
| config.format = g_surface_format; | |
| config.usage = WGPUTextureUsage_RenderAttachment; | |
| config.width = canvas_w; | |
| config.height = canvas_h; | |
| config.presentMode = WGPUPresentMode_Fifo; | |
| config.alphaMode = WGPUCompositeAlphaMode_Opaque; | |
| wgpuSurfaceConfigure (g_surface, &config); | |
| demo_view_reshape_func (vu, canvas_w, canvas_h); | |
| } | |
| } | |
| static void | |
| main_loop_iter () | |
| { | |
| check_resize (); | |
| if (demo_view_should_redraw (vu)) | |
| demo_view_display (vu, buffer); | |
| } | |
| /* ---- WebGPU initialization ---- */ | |
| static void init_demo (); | |
| static void create_pipeline (); | |
| static void create_bind_group (); | |
| static void | |
| on_device (WGPURequestDeviceStatus status, | |
| WGPUDevice dev, WGPUStringView msg, void *ud1, void *ud2) | |
| { | |
| if (status != WGPURequestDeviceStatus_Success) | |
| { | |
| fprintf (stderr, "Failed to get WebGPU device\n"); | |
| EM_ASM ({ | |
| var el = document.getElementById ('loading'); | |
| if (el) { el.textContent = 'Failed to create WebGPU device.'; el.style.color = '#666'; } | |
| }); | |
| return; | |
| } | |
| g_device = dev; | |
| g_queue = wgpuDeviceGetQueue (g_device); | |
| init_demo (); | |
| } | |
| static void | |
| on_adapter (WGPURequestAdapterStatus status, | |
| WGPUAdapter adpt, WGPUStringView msg, void *ud1, void *ud2) | |
| { | |
| if (status != WGPURequestAdapterStatus_Success) | |
| { | |
| fprintf (stderr, "Failed to get WebGPU adapter\n"); | |
| EM_ASM ({ | |
| window._webgpuError = true; | |
| var el = document.getElementById ('loading'); | |
| if (el) { | |
| el.style.display = ""; | |
| var html = 'WebGPU adapter not available.<br>' + | |
| '<a href="https://github.com/gpuweb/gpuweb/wiki/Implementation-Status" ' + | |
| 'style="color:#88f">Browser support status</a>'; | |
| if (navigator.userAgent.indexOf ('Linux') !== -1 && | |
| navigator.userAgent.indexOf ('Chrome') !== -1) | |
| html += '<br><br>On Linux Chrome, try restarting with:<br>' + | |
| '<code style="font-size:13px">google-chrome --enable-unsafe-webgpu --ozone-platform=x11 ' + | |
| '--use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE</code>'; | |
| el.innerHTML = html; | |
| el.style.font = '18px sans-serif'; | |
| el.style.color = '#666'; | |
| el.style.textAlign = 'center'; | |
| el.style.flexDirection = 'column'; | |
| } | |
| }); | |
| return; | |
| } | |
| g_adapter = adpt; | |
| WGPUDeviceDescriptor devDesc = {}; | |
| WGPURequestDeviceCallbackInfo cbInfo = {}; | |
| cbInfo.mode = WGPUCallbackMode_AllowSpontaneous; | |
| cbInfo.callback = on_device; | |
| wgpuAdapterRequestDevice (g_adapter, &devDesc, cbInfo); | |
| } | |
| static void | |
| create_pipeline () | |
| { | |
| std::string wgsl; | |
| wgsl += hb_gpu_shader_source (HB_GPU_SHADER_STAGE_VERTEX, HB_GPU_SHADER_LANG_WGSL); | |
| wgsl += "\n"; | |
| wgsl += hb_gpu_draw_shader_source (HB_GPU_SHADER_STAGE_VERTEX, HB_GPU_SHADER_LANG_WGSL); | |
| wgsl += "\n"; | |
| wgsl += hb_gpu_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_WGSL); | |
| wgsl += "\n"; | |
| wgsl += hb_gpu_draw_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_WGSL); | |
| wgsl += "\n"; | |
| wgsl += hb_gpu_paint_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_WGSL); | |
| wgsl += "\n"; | |
| wgsl += demo_shader_wgsl; | |
| WGPUShaderSourceWGSL wgslSrc = {}; | |
| wgslSrc.chain.sType = WGPUSType_ShaderSourceWGSL; | |
| wgslSrc.code = {wgsl.c_str (), wgsl.size ()}; | |
| WGPUShaderModuleDescriptor smDesc = {}; | |
| smDesc.nextInChain = &wgslSrc.chain; | |
| WGPUShaderModule module = wgpuDeviceCreateShaderModule (g_device, &smDesc); | |
| WGPUVertexAttribute attrs[5] = {}; | |
| attrs[0].format = WGPUVertexFormat_Float32x2; | |
| attrs[0].offset = offsetof (glyph_vertex_t, x); | |
| attrs[0].shaderLocation = 0; | |
| attrs[1].format = WGPUVertexFormat_Float32x2; | |
| attrs[1].offset = offsetof (glyph_vertex_t, tx); | |
| attrs[1].shaderLocation = 1; | |
| attrs[2].format = WGPUVertexFormat_Float32x2; | |
| attrs[2].offset = offsetof (glyph_vertex_t, nx); | |
| attrs[2].shaderLocation = 2; | |
| attrs[3].format = WGPUVertexFormat_Float32; | |
| attrs[3].offset = offsetof (glyph_vertex_t, emPerPos); | |
| attrs[3].shaderLocation = 3; | |
| attrs[4].format = WGPUVertexFormat_Uint32; | |
| attrs[4].offset = offsetof (glyph_vertex_t, atlas_offset); | |
| attrs[4].shaderLocation = 4; | |
| WGPUVertexBufferLayout vbLayout = {}; | |
| vbLayout.arrayStride = sizeof (glyph_vertex_t); | |
| vbLayout.stepMode = WGPUVertexStepMode_Vertex; | |
| vbLayout.attributeCount = 5; | |
| vbLayout.attributes = attrs; | |
| WGPUBlendState blend = {}; | |
| blend.color.srcFactor = WGPUBlendFactor_SrcAlpha; | |
| blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; | |
| blend.color.operation = WGPUBlendOperation_Add; | |
| blend.alpha.srcFactor = WGPUBlendFactor_One; | |
| blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; | |
| blend.alpha.operation = WGPUBlendOperation_Add; | |
| WGPUColorTargetState colorTarget = {}; | |
| colorTarget.format = g_surface_format; | |
| colorTarget.blend = &blend; | |
| colorTarget.writeMask = WGPUColorWriteMask_All; | |
| WGPUFragmentState fragState = {}; | |
| fragState.module = module; | |
| fragState.entryPoint = {"fs_main", WGPU_STRLEN}; | |
| fragState.targetCount = 1; | |
| fragState.targets = &colorTarget; | |
| WGPURenderPipelineDescriptor pipeDesc = {}; | |
| pipeDesc.vertex.module = module; | |
| pipeDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN}; | |
| pipeDesc.vertex.bufferCount = 1; | |
| pipeDesc.vertex.buffers = &vbLayout; | |
| pipeDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList; | |
| pipeDesc.primitive.cullMode = WGPUCullMode_None; | |
| pipeDesc.multisample.count = 1; | |
| pipeDesc.multisample.mask = ~0u; | |
| pipeDesc.fragment = &fragState; | |
| g_pipeline = wgpuDeviceCreateRenderPipeline (g_device, &pipeDesc); | |
| wgpuShaderModuleRelease (module); | |
| } | |
| static void | |
| create_bind_group () | |
| { | |
| WGPUBindGroupEntry entries[2] = {}; | |
| entries[0].binding = 0; | |
| entries[0].buffer = g_uniform_buf; | |
| entries[0].size = sizeof (Uniforms); | |
| entries[1].binding = 1; | |
| entries[1].buffer = atlas.buf; | |
| entries[1].size = atlas.capacity * 4 * sizeof (int32_t); | |
| WGPUBindGroupDescriptor bgDesc = {}; | |
| bgDesc.layout = wgpuRenderPipelineGetBindGroupLayout (g_pipeline, 0); | |
| bgDesc.entryCount = 2; | |
| bgDesc.entries = entries; | |
| if (g_bind_group) | |
| wgpuBindGroupRelease (g_bind_group); | |
| g_bind_group = wgpuDeviceCreateBindGroup (g_device, &bgDesc); | |
| } | |
| static void | |
| init_demo () | |
| { | |
| /* Configure surface */ | |
| WGPUSurfaceCapabilities caps = {}; | |
| wgpuSurfaceGetCapabilities (g_surface, g_adapter, &caps); | |
| g_surface_format = caps.formats[0]; | |
| WGPUSurfaceConfiguration config = {}; | |
| config.device = g_device; | |
| config.format = g_surface_format; | |
| config.usage = WGPUTextureUsage_RenderAttachment; | |
| config.width = canvas_w; | |
| config.height = canvas_h; | |
| config.presentMode = WGPUPresentMode_Fifo; | |
| config.alphaMode = WGPUCompositeAlphaMode_Opaque; | |
| wgpuSurfaceConfigure (g_surface, &config); | |
| /* Create atlas storage buffer */ | |
| unsigned atlas_capacity = 256 * 1024; | |
| atlas.capacity = atlas_capacity; | |
| atlas.cursor = 0; | |
| atlas.dirty = true; | |
| atlas.shadow = (int32_t *) calloc (atlas_capacity * 4, sizeof (int32_t)); | |
| WGPUBufferDescriptor atlasDesc = {}; | |
| atlasDesc.size = atlas_capacity * 4 * sizeof (int32_t); | |
| atlasDesc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; | |
| atlas.buf = wgpuDeviceCreateBuffer (g_device, &atlasDesc); | |
| /* Create uniform buffer */ | |
| WGPUBufferDescriptor uniDesc = {}; | |
| uniDesc.size = sizeof (Uniforms); | |
| uniDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; | |
| g_uniform_buf = wgpuDeviceCreateBuffer (g_device, &uniDesc); | |
| /* Create pipeline and bind group */ | |
| create_pipeline (); | |
| /* Set up renderer with atlas backend */ | |
| renderer = new demo_renderer_webgpu_t (); | |
| demo_atlas_backend_t backend = {}; | |
| backend.ctx = &atlas; | |
| backend.alloc = atlas_alloc_cb; | |
| backend.get_used = atlas_get_used_cb; | |
| backend.clear = atlas_clear_cb; | |
| renderer->demo_atlas = demo_atlas_create_external (&backend); | |
| /* Create view (headless — no GLFW) */ | |
| vu = demo_view_create_headless (renderer, canvas_w, canvas_h, css_w, css_h); | |
| /* Create font and buffer */ | |
| current_blob = hb_blob_create ((const char *) default_font, | |
| sizeof (default_font), | |
| HB_MEMORY_MODE_READONLY, | |
| NULL, NULL); | |
| current_face = hb_face_create (current_blob, 0); | |
| current_font = hb_font_create (current_face); | |
| current_demo_font = demo_font_create (current_font, renderer->get_atlas (), false); | |
| current_text = strdup (arg_text ? arg_text : default_text_combined); | |
| if (arg_text) | |
| custom_text = true; | |
| buffer = demo_buffer_create (); | |
| demo_point_t top_left = {0, 0}; | |
| demo_buffer_move_to (buffer, &top_left); | |
| /* Skip initial glyph upload if ?font= is pending. Prevents | |
| * wasting atlas capacity on the default font's glyphs. */ | |
| if (!arg_font) | |
| demo_buffer_add_text (buffer, current_text, current_demo_font, 1); | |
| demo_font_print_stats (current_demo_font); | |
| /* Create bind group (after atlas populated) */ | |
| create_bind_group (); | |
| /* Setup renderer (sets initial gamma/colors) */ | |
| demo_view_setup (vu); | |
| /* Register input handlers */ | |
| emscripten_set_mousedown_callback ("#canvas", NULL, EM_TRUE, on_mousedown); | |
| emscripten_set_mouseup_callback ("#canvas", NULL, EM_TRUE, on_mouseup); | |
| /* Catch mouseup outside canvas for drag-release; filtered by active_buttons */ | |
| emscripten_set_mouseup_callback (EMSCRIPTEN_EVENT_TARGET_DOCUMENT, NULL, EM_TRUE, on_mouseup); | |
| emscripten_set_mousemove_callback ("#canvas", NULL, EM_TRUE, on_mousemove); | |
| emscripten_set_wheel_callback ("#canvas", NULL, EM_TRUE, on_wheel); | |
| emscripten_set_keydown_callback (EMSCRIPTEN_EVENT_TARGET_DOCUMENT, NULL, EM_TRUE, on_keydown); | |
| emscripten_set_touchstart_callback ("#canvas", NULL, EM_TRUE, on_touchstart); | |
| emscripten_set_touchmove_callback ("#canvas", NULL, EM_TRUE, on_touchmove); | |
| emscripten_set_touchend_callback ("#canvas", NULL, EM_TRUE, on_touchend); | |
| emscripten_set_touchcancel_callback ("#canvas", NULL, EM_TRUE, on_touchcancel); | |
| /* Hide loading screen */ | |
| EM_ASM ({ | |
| var el = document.getElementById ('loading'); | |
| if (el && !window._webgpuError) { | |
| el.style.display = 'none'; | |
| document.body.style.background = '#222'; | |
| } | |
| }); | |
| printf ("WebGPU demo initialized\n"); | |
| /* Signal JS that async init is complete so URL-param ?font= | |
| * fetches can safely call _web_load_font. */ | |
| EM_ASM ({ | |
| if (typeof window._webDemoReady === 'function') window._webDemoReady (); | |
| }); | |
| /* Start render loop */ | |
| emscripten_set_main_loop (main_loop_iter, 0, 0); | |
| } | |
| int | |
| main (int argc, char **argv) | |
| { | |
| /* argv is stack-allocated by Emscripten; main returns while | |
| * init_demo runs async, so copy to heap to survive the unwind. */ | |
| for (int i = 1; i < argc; i++) | |
| { | |
| if (!strncmp (argv[i], "--text=", 7)) | |
| arg_text = strdup (argv[i] + 7); | |
| else if (!strncmp (argv[i], "--font=", 7)) | |
| arg_font = strdup (argv[i] + 7); | |
| } | |
| emscripten_get_canvas_element_size ("#canvas", &canvas_w, &canvas_h); | |
| double css_w_d, css_h_d; | |
| emscripten_get_element_css_size ("#canvas", &css_w_d, &css_h_d); | |
| css_w = (int) css_w_d; | |
| css_h = (int) css_h_d; | |
| /* Check WebGPU availability before trying the C API. */ | |
| bool has_webgpu = EM_ASM_INT ({ return !!navigator.gpu; }); | |
| if (!has_webgpu) | |
| { | |
| fprintf (stderr, "WebGPU not available in this browser\n"); | |
| EM_ASM ({ | |
| window._webgpuError = true; | |
| var el = document.getElementById ('loading'); | |
| if (el) { | |
| el.style.display = ""; | |
| el.innerHTML = 'WebGPU is not available in this browser.<br>' + | |
| '<a href="index.html" style="color:#88f">Try the WebGL version</a>'; | |
| el.style.font = '18px sans-serif'; | |
| el.style.color = '#666'; | |
| el.style.textAlign = 'center'; | |
| } | |
| }); | |
| return 0; | |
| } | |
| WGPUInstanceDescriptor instDesc = {}; | |
| WGPUInstance instance = wgpuCreateInstance (&instDesc); | |
| WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvasDesc = {}; | |
| canvasDesc.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; | |
| canvasDesc.selector = {"#canvas", WGPU_STRLEN}; | |
| WGPUSurfaceDescriptor surfDesc = {}; | |
| surfDesc.nextInChain = &canvasDesc.chain; | |
| g_surface = wgpuInstanceCreateSurface (instance, &surfDesc); | |
| WGPURequestAdapterOptions opts = {}; | |
| opts.compatibleSurface = g_surface; | |
| opts.powerPreference = WGPUPowerPreference_HighPerformance; | |
| WGPURequestAdapterCallbackInfo cbInfo = {}; | |
| cbInfo.mode = WGPUCallbackMode_AllowSpontaneous; | |
| cbInfo.callback = on_adapter; | |
| wgpuInstanceRequestAdapter (instance, &opts, cbInfo); | |
| return 0; | |
| } |