diff --git a/readme.md b/readme.md
index df3c883..6b5ff12 100644
--- a/readme.md
+++ b/readme.md
@@ -65,6 +65,27 @@ A classic Minesweeper game implementation. [View source code](Examples/Minesweep

+## Web (WASM + WebGPU)
+
+Thirteen now has an `__EMSCRIPTEN__` backend that uses WebGPU in the browser.
+
+### Build Simple example for web
+
+```bash
+emcc Examples/Simple/main.cpp -std=c++17 -I. \
+ --use-port=emdawnwebgpu \
+ -sASYNCIFY \
+ -sALLOW_MEMORY_GROWTH=1 \
+ -o simple.html
+```
+
+Then run a local web server and open `simple.html` in a WebGPU-enabled browser.
+
+### Notes
+- The web backend expects a `` in the generated page.
+- `SetFullscreen()` is currently a no-op on web.
+- Input uses browser events mapped into the same `GetKey()` / mouse APIs.
+
## API Reference
#### `uint8* Init(uint32 width = 1024, uint32 height = 768, bool fullscreen = false)`
diff --git a/thirteen.h b/thirteen.h
index c0dc7e0..9d579e7 100644
--- a/thirteen.h
+++ b/thirteen.h
@@ -19,6 +19,8 @@ Nikita Lisitsa - Linux/X11+OpenGL
#if defined(_WIN32)
#define THIRTEEN_PLATFORM_WINDOWS
+#elif defined(__EMSCRIPTEN__)
+ #define THIRTEEN_PLATFORM_WEB
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
#define THIRTEEN_PLATFORM_MACOS
#elif defined(__linux__)
@@ -34,14 +36,11 @@ Nikita Lisitsa - Linux/X11+OpenGL
#include
#include
#include
-
- #pragma comment(lib, "d3d12.lib")
- #pragma comment(lib, "dxgi.lib")
-
- #define DX12VALIDATION() (_DEBUG && false)
-#endif
-
-#ifdef THIRTEEN_PLATFORM_MACOS
+#elif defined(THIRTEEN_PLATFORM_WEB)
+ #include
+ #include
+ #include
+#elif defined(THIRTEEN_PLATFORM_MACOS)
#include
#include
#include
@@ -49,6 +48,13 @@ Nikita Lisitsa - Linux/X11+OpenGL
#include
#endif
+#ifdef THIRTEEN_PLATFORM_WINDOWS
+ #pragma comment(lib, "d3d12.lib")
+ #pragma comment(lib, "dxgi.lib")
+
+ #define DX12VALIDATION() (_DEBUG && false)
+#endif
+
#ifdef THIRTEEN_PLATFORM_LINUX
#include
#include
@@ -639,11 +645,367 @@ namespace Thirteen
device->Release();
}
};
+ #elif defined(__EMSCRIPTEN__)
+ using NativeWindowHandle = void*;
+ struct PlatformWeb
+ {
+ static constexpr const char* c_canvasSelector = "#canvas";
- #elif defined(THIRTEEN_PLATFORM_MACOS)
+ inline static PlatformWeb* s_instance = nullptr;
- using NativeWindowHandle = void*;
+ enum BrowserMouseButton : unsigned short
+ {
+ BrowserMouseButtonLeft = 0,
+ BrowserMouseButtonMiddle = 1,
+ BrowserMouseButtonRight = 2
+ };
+
+ enum ThirteenMouseButton : int
+ {
+ ThirteenMouseButtonLeft = 0,
+ ThirteenMouseButtonRight = 1,
+ ThirteenMouseButtonMiddle = 2,
+ ThirteenMouseButtonInvalid = -1
+ };
+
+ static int MapMouseButton(unsigned short button)
+ {
+ // Browser buttons: 0=left, 1=middle, 2=right.
+ if (button == BrowserMouseButtonLeft)
+ return ThirteenMouseButtonLeft;
+ if (button == BrowserMouseButtonRight)
+ return ThirteenMouseButtonRight;
+ if (button == BrowserMouseButtonMiddle)
+ return ThirteenMouseButtonMiddle;
+ return ThirteenMouseButtonInvalid;
+ }
+
+ static void UpdateKeyState(const EmscriptenKeyboardEvent* keyEvent, bool isDown)
+ {
+ if (!keyEvent)
+ return;
+
+ if (keyEvent->keyCode >= 0 && keyEvent->keyCode < 256)
+ keys[keyEvent->keyCode] = isDown;
+
+ if (keyEvent->key[0] != '\0' && keyEvent->key[1] == '\0')
+ keys[(unsigned char)keyEvent->key[0]] = isDown;
+
+ if (std::strcmp(keyEvent->key, "Escape") == 0)
+ keys[VK_ESCAPE] = isDown;
+ if (std::strcmp(keyEvent->key, " ") == 0 || std::strcmp(keyEvent->key, "Spacebar") == 0 || std::strcmp(keyEvent->code, "Space") == 0)
+ keys[VK_SPACE] = isDown;
+ }
+
+ static EM_BOOL OnKeyDown(int, const EmscriptenKeyboardEvent* keyEvent, void*)
+ {
+ UpdateKeyState(keyEvent, true);
+ return EM_TRUE;
+ }
+
+ static EM_BOOL OnKeyUp(int, const EmscriptenKeyboardEvent* keyEvent, void*)
+ {
+ UpdateKeyState(keyEvent, false);
+ return EM_TRUE;
+ }
+
+ static EM_BOOL OnMouseDown(int, const EmscriptenMouseEvent* mouseEvent, void*)
+ {
+ if (!mouseEvent)
+ return EM_FALSE;
+ int button = MapMouseButton(mouseEvent->button);
+ if (button >= 0 && button < 3)
+ mouseButtons[button] = true;
+ mouseX = mouseEvent->targetX;
+ mouseY = mouseEvent->targetY;
+ return EM_TRUE;
+ }
+
+ static EM_BOOL OnMouseUp(int, const EmscriptenMouseEvent* mouseEvent, void*)
+ {
+ if (!mouseEvent)
+ return EM_FALSE;
+ int button = MapMouseButton(mouseEvent->button);
+ if (button >= 0 && button < 3)
+ mouseButtons[button] = false;
+ mouseX = mouseEvent->targetX;
+ mouseY = mouseEvent->targetY;
+ return EM_TRUE;
+ }
+
+ static EM_BOOL OnMouseMove(int, const EmscriptenMouseEvent* mouseEvent, void*)
+ {
+ if (!mouseEvent)
+ return EM_FALSE;
+ mouseX = mouseEvent->targetX;
+ mouseY = mouseEvent->targetY;
+ return EM_TRUE;
+ }
+
+ static EM_BOOL OnCanvasResize(int, const EmscriptenUiEvent*, void*)
+ {
+ if (!s_instance)
+ return EM_FALSE;
+ int canvasWidth = 0;
+ int canvasHeight = 0;
+ if (emscripten_get_canvas_element_size(c_canvasSelector, &canvasWidth, &canvasHeight) == EMSCRIPTEN_RESULT_SUCCESS)
+ {
+ if (canvasWidth > 0 && canvasHeight > 0 && ((uint32)canvasWidth != width || (uint32)canvasHeight != height))
+ SetSize((uint32)canvasWidth, (uint32)canvasHeight);
+ }
+ return EM_TRUE;
+ }
+
+ bool InitWindow(uint32 width, uint32 height)
+ {
+ s_instance = this;
+
+ if (emscripten_set_canvas_element_size(c_canvasSelector, (int)width, (int)height) != EMSCRIPTEN_RESULT_SUCCESS)
+ return false;
+
+ emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, true, OnKeyDown);
+ emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, true, OnKeyUp);
+ emscripten_set_mousedown_callback(c_canvasSelector, nullptr, true, OnMouseDown);
+ emscripten_set_mouseup_callback(c_canvasSelector, nullptr, true, OnMouseUp);
+ emscripten_set_mousemove_callback(c_canvasSelector, nullptr, true, OnMouseMove);
+ emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, false, OnCanvasResize);
+ return true;
+ }
+
+ void PumpMessages() {}
+
+ void SetTitle(const char* title)
+ {
+ emscripten_set_window_title(title ? title : "");
+ }
+
+ void SetFullscreen(bool, uint32, uint32) {}
+
+ void ResizeWindow(uint32 width, uint32 height, bool)
+ {
+ emscripten_set_canvas_element_size(c_canvasSelector, (int)width, (int)height);
+ }
+
+ NativeWindowHandle GetWindowHandle() const { return nullptr; }
+
+ void ShutdownWindow()
+ {
+ emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, true, nullptr);
+ emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, true, nullptr);
+ emscripten_set_mousedown_callback(c_canvasSelector, nullptr, true, nullptr);
+ emscripten_set_mouseup_callback(c_canvasSelector, nullptr, true, nullptr);
+ emscripten_set_mousemove_callback(c_canvasSelector, nullptr, true, nullptr);
+ emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, false, nullptr);
+ s_instance = nullptr;
+ }
+ };
+
+ struct RendererWebGPU
+ {
+ WGPUDevice device = nullptr;
+ WGPUAdapter adapter = nullptr;
+ WGPUQueue queue = nullptr;
+ WGPUInstance instance = nullptr;
+ WGPUSurface surface = nullptr;
+ WGPUTextureFormat surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
+ uint32 configuredWidth = 0;
+ uint32 configuredHeight = 0;
+ bool adapterRequestPending = false;
+ bool deviceRequestPending = false;
+ bool requestFailed = false;
+
+ static void OnRequestDevice(WGPURequestDeviceStatus status, WGPUDevice requestedDevice, WGPUStringView, void* userdata1, void*)
+ {
+ RendererWebGPU* self = (RendererWebGPU*)userdata1;
+ if (!self)
+ return;
+ self->deviceRequestPending = false;
+ if (status != WGPURequestDeviceStatus_Success || !requestedDevice)
+ {
+ self->requestFailed = true;
+ return;
+ }
+
+ self->device = requestedDevice;
+ self->queue = wgpuDeviceGetQueue(self->device);
+ if (!self->queue)
+ {
+ self->requestFailed = true;
+ return;
+ }
+
+ if (!self->ConfigureSurface(self->configuredWidth, self->configuredHeight))
+ self->requestFailed = true;
+ }
+
+ static void OnRequestAdapter(WGPURequestAdapterStatus status, WGPUAdapter requestedAdapter, WGPUStringView, void* userdata1, void*)
+ {
+ RendererWebGPU* self = (RendererWebGPU*)userdata1;
+ if (!self)
+ return;
+
+ self->adapterRequestPending = false;
+ if (status != WGPURequestAdapterStatus_Success || !requestedAdapter)
+ {
+ self->requestFailed = true;
+ return;
+ }
+
+ self->adapter = requestedAdapter;
+
+ WGPURequestDeviceCallbackInfo callbackInfo = {};
+ callbackInfo.mode = WGPUCallbackMode_AllowProcessEvents;
+ callbackInfo.callback = OnRequestDevice;
+ callbackInfo.userdata1 = self;
+
+ WGPUDeviceDescriptor deviceDesc = {};
+ wgpuAdapterRequestDevice(self->adapter, &deviceDesc, callbackInfo);
+ self->deviceRequestPending = true;
+ }
+
+ bool ConfigureSurface(uint32 width, uint32 height)
+ {
+ if (!surface || !device || width == 0 || height == 0)
+ return false;
+
+ WGPUSurfaceConfiguration config = {};
+ config.device = device;
+ config.format = surfaceFormat;
+ config.usage = WGPUTextureUsage_CopyDst | WGPUTextureUsage_RenderAttachment;
+ config.alphaMode = WGPUCompositeAlphaMode_Auto;
+ config.width = width;
+ config.height = height;
+ config.presentMode = WGPUPresentMode_Fifo;
+ wgpuSurfaceConfigure(surface, &config);
+
+ configuredWidth = width;
+ configuredHeight = height;
+ return true;
+ }
+
+ bool Init(NativeWindowHandle, uint32 width, uint32 height)
+ {
+ WGPUInstanceDescriptor instanceDesc = {};
+ instance = wgpuCreateInstance(&instanceDesc);
+ if (!instance)
+ return false;
+
+ WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvasDesc = {};
+ canvasDesc.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
+ canvasDesc.selector.data = PlatformWeb::c_canvasSelector;
+ canvasDesc.selector.length = WGPU_STRLEN;
+
+ WGPUSurfaceDescriptor surfaceDesc = {};
+ surfaceDesc.nextInChain = reinterpret_cast(&canvasDesc);
+ surface = wgpuInstanceCreateSurface(instance, &surfaceDesc);
+ if (!surface)
+ return false;
+
+ configuredWidth = width;
+ configuredHeight = height;
+
+ WGPURequestAdapterOptions options = {};
+ options.compatibleSurface = surface;
+
+ WGPURequestAdapterCallbackInfo callbackInfo = {};
+ callbackInfo.mode = WGPUCallbackMode_AllowProcessEvents;
+ callbackInfo.callback = OnRequestAdapter;
+ callbackInfo.userdata1 = this;
+
+ wgpuInstanceRequestAdapter(instance, &options, callbackInfo);
+ adapterRequestPending = true;
+ deviceRequestPending = false;
+ requestFailed = false;
+ return true;
+ }
+
+ bool Render(const uint8* pixels, uint32 width, uint32 height, bool)
+ {
+ if (!pixels || !surface)
+ return false;
+
+ if (!device)
+ {
+ if (instance && (adapterRequestPending || deviceRequestPending))
+ wgpuInstanceProcessEvents(instance);
+ if (requestFailed)
+ return false;
+ if (!device || !queue)
+ return true;
+ }
+
+ if (width != configuredWidth || height != configuredHeight)
+ {
+ if (!ConfigureSurface(width, height))
+ return false;
+ }
+
+ WGPUSurfaceTexture surfaceTexture = {};
+ wgpuSurfaceGetCurrentTexture(surface, &surfaceTexture);
+
+ const bool surfaceOk =
+ surfaceTexture.status == WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal ||
+ surfaceTexture.status == WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal;
+ if (!surfaceOk || !surfaceTexture.texture)
+ {
+ if (!ConfigureSurface(width, height))
+ return false;
+ return true;
+ }
+
+ WGPUTexelCopyTextureInfo dst = {};
+ dst.texture = surfaceTexture.texture;
+ dst.mipLevel = 0;
+ dst.origin = { 0, 0, 0 };
+ dst.aspect = WGPUTextureAspect_All;
+
+ WGPUTexelCopyBufferLayout layout = {};
+ layout.offset = 0;
+ layout.bytesPerRow = width * 4u;
+ layout.rowsPerImage = height;
+
+ WGPUExtent3D writeSize = { width, height, 1 };
+ wgpuQueueWriteTexture(queue, &dst, pixels, (size_t)width * (size_t)height * 4u, &layout, &writeSize);
+
+ wgpuSurfacePresent(surface);
+ wgpuTextureRelease(surfaceTexture.texture);
+ return true;
+ }
+
+ bool Resize(uint32 width, uint32 height)
+ {
+ return ConfigureSurface(width, height);
+ }
+
+ void Shutdown()
+ {
+ if (surface)
+ {
+ wgpuSurfaceRelease(surface);
+ surface = nullptr;
+ }
+ if (adapter)
+ {
+ wgpuAdapterRelease(adapter);
+ adapter = nullptr;
+ }
+ if (instance)
+ {
+ wgpuInstanceRelease(instance);
+ instance = nullptr;
+ }
+ queue = nullptr;
+ device = nullptr;
+ configuredWidth = 0;
+ configuredHeight = 0;
+ adapterRequestPending = false;
+ deviceRequestPending = false;
+ requestFailed = false;
+ }
+ };
+ #elif defined(THIRTEEN_PLATFORM_MACOS)
+ using NativeWindowHandle = void*;
extern "C" void* MTLCreateSystemDefaultDevice(void);
using NSUInteger = unsigned long;
@@ -1410,6 +1772,9 @@ namespace Thirteen
#if defined(THIRTEEN_PLATFORM_WINDOWS)
using Platform = PlatformWin32;
using Renderer = RendererD3D12;
+ #elif defined(__EMSCRIPTEN__)
+ using Platform = PlatformWeb;
+ using Renderer = RendererWebGPU;
#elif defined(THIRTEEN_PLATFORM_MACOS)
using Platform = PlatformMetal;
using Renderer = RendererMetal;
@@ -1611,6 +1976,11 @@ namespace Thirteen
if (!renderer)
return false;
renderer->Render(Internal::Pixels, width, height, vsyncEnabled);
+ #if defined(__EMSCRIPTEN__)
+ // Browser builds need to yield so JS promises/events (including WebGPU
+ // async setup and canvas presentation) can progress each frame.
+ emscripten_sleep(0);
+ #endif
return !shouldQuit;
}