1078 lines
32 KiB
C++
1078 lines
32 KiB
C++
/*
|
|
* 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)
|
|
{
|
|
/* 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);
|
|
demo_buffer_add_text (buffer, 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;
|
|
}
|