dotframe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +57 -0
  4. package/assets/audio/hit.mp3 +0 -0
  5. package/assets/audio/loop.mp3 +0 -0
  6. package/assets/crafter.png +0 -0
  7. package/assets/fonts/archivo-black.json +133 -0
  8. package/assets/fonts/archivo-black.png +0 -0
  9. package/assets/fonts/bangers.json +133 -0
  10. package/assets/fonts/bangers.png +0 -0
  11. package/assets/suzanne.glb +0 -0
  12. package/cli/commands/config.ts +35 -0
  13. package/cli/commands/doctor.ts +71 -0
  14. package/cli/commands/new.ts +73 -0
  15. package/cli/commands/play.ts +244 -0
  16. package/cli/commands/ship.ts +94 -0
  17. package/cli/commands/skills.ts +66 -0
  18. package/cli/commands/snap.ts +88 -0
  19. package/cli/lib.ts +148 -0
  20. package/cli/main.ts +95 -0
  21. package/cli/simkit.ts +152 -0
  22. package/cli/tsconfig.json +8 -0
  23. package/native/df_audio.c +266 -0
  24. package/native/df_native.c +605 -0
  25. package/native/ffi.macos.json +318 -0
  26. package/native/ffi.windows.json +322 -0
  27. package/native/third_party/dr_mp3.h +5430 -0
  28. package/native/third_party/stb_image.h +7988 -0
  29. package/native/third_party/stb_image_write.h +1724 -0
  30. package/native/third_party/stb_truetype.h +5079 -0
  31. package/package.json +47 -0
  32. package/scripts/build-native.sh +33 -0
  33. package/scripts/build-web.sh +10 -0
  34. package/scripts/vendor.sh +36 -0
  35. package/skills/assets/SKILL.md +11 -0
  36. package/skills/core/SKILL.md +51 -0
  37. package/skills/core/references/errors.md +22 -0
  38. package/skills/discord/SKILL.md +12 -0
  39. package/skills/export-web/SKILL.md +20 -0
  40. package/skills/game-design/SKILL.md +27 -0
  41. package/skills/game-design/references/sim-walkthrough.md +32 -0
  42. package/skills/ios/SKILL.md +19 -0
  43. package/skills/macos/SKILL.md +16 -0
  44. package/skills/netplay/SKILL.md +34 -0
  45. package/skills/relay/SKILL.md +18 -0
  46. package/src/audio.ts +19 -0
  47. package/src/draw2d.ts +901 -0
  48. package/src/ecs.ts +30 -0
  49. package/src/gltf.ts +82 -0
  50. package/src/gpu.ts +86 -0
  51. package/src/input.ts +215 -0
  52. package/src/math.ts +113 -0
  53. package/src/native/backend.ts +142 -0
  54. package/src/native/ffi.ts +42 -0
  55. package/src/native/library.ts +65 -0
  56. package/src/native/run.ts +88 -0
  57. package/src/physics.ts +57 -0
  58. package/src/platform.ts +12 -0
  59. package/src/raster2d.ts +348 -0
  60. package/src/render.ts +132 -0
  61. package/src/shapes.ts +37 -0
  62. package/src/sim.ts +56 -0
  63. package/src/storage.ts +6 -0
  64. package/src/web/run.ts +338 -0
  65. package/templates/_base/AGENTS.md +7 -0
  66. package/templates/_base/dotframe.json +15 -0
  67. package/templates/_base/gitignore +5 -0
  68. package/templates/_base/index.html +14 -0
  69. package/templates/_base/main.web.ts +27 -0
  70. package/templates/_base/package.json +12 -0
  71. package/templates/_base/sim.ts +31 -0
  72. package/templates/blank/game.ts +64 -0
  73. package/templates/fighter/game.ts +131 -0
  74. package/templates/platformer/game.ts +120 -0
  75. package/tools/check-audio.ts +57 -0
  76. package/tools/gen-library-glue.ts +83 -0
  77. package/tools/make-sprite.ts +92 -0
@@ -0,0 +1,605 @@
1
+ // Flat C ABI over SDL3 + wgpu-native for scriptc FFI: scalars and byte spans only.
2
+ #include <SDL3/SDL.h>
3
+ #include <stdint.h>
4
+ #include <stdio.h>
5
+ #include <string.h>
6
+ #include <webgpu/webgpu.h>
7
+ #include <webgpu/wgpu.h>
8
+ #if defined(__APPLE__)
9
+ #include <SDL3/SDL_metal.h>
10
+ #include <TargetConditionals.h>
11
+ #endif
12
+
13
+ #define STB_IMAGE_IMPLEMENTATION
14
+ #define STBI_ONLY_PNG
15
+ #define STBI_NO_STDIO
16
+ #include "third_party/stb_image.h"
17
+
18
+ #define DF_MAX_PIPELINES 64
19
+ #define DF_MAX_BUFFERS 1024
20
+ #define DF_MAX_BIND_GROUPS 1024
21
+ #define DF_MAX_TEXTURES 256
22
+
23
+ enum { DF_USAGE_VERTEX = 1, DF_USAGE_INDEX = 2, DF_USAGE_UNIFORM = 4 };
24
+ enum { DF_PIPELINE_DEPTH = 1, DF_PIPELINE_BLEND = 4 };
25
+ enum { DF_FORMAT_FLOAT32X2 = 0, DF_FORMAT_FLOAT32X3 = 1, DF_FORMAT_FLOAT32X4 = 2, DF_FORMAT_FLOAT32 = 3, DF_FORMAT_UNORM8X4 = 4 };
26
+
27
+ static SDL_Window *g_window;
28
+ static WGPUInstance g_instance;
29
+ static WGPUSurface g_surface;
30
+ static WGPUAdapter g_adapter;
31
+ static WGPUDevice g_device;
32
+ static WGPUQueue g_queue;
33
+ static WGPUTextureFormat g_format;
34
+ static WGPURenderPipeline g_pipelines[DF_MAX_PIPELINES];
35
+ static int32_t g_pipeline_count;
36
+ static WGPUBuffer g_buffers[DF_MAX_BUFFERS];
37
+ static int32_t g_buffer_count;
38
+ static WGPUBindGroup g_bind_groups[DF_MAX_BIND_GROUPS];
39
+ static int32_t g_bind_group_count;
40
+ static WGPUTexture g_textures[DF_MAX_TEXTURES];
41
+ static WGPUTextureView g_texture_views[DF_MAX_TEXTURES];
42
+ static int32_t g_texture_sizes[DF_MAX_TEXTURES][2];
43
+ static uint8_t g_texture_smooth[DF_MAX_TEXTURES];
44
+ static int32_t g_texture_count;
45
+ static WGPUSampler g_sampler;
46
+ static WGPUSampler g_sampler_linear;
47
+ static WGPUTexture g_depth_texture;
48
+ static WGPUTextureView g_depth_view;
49
+ static int g_width, g_height;
50
+ static WGPUPresentMode g_present_mode = WGPUPresentMode_Fifo;
51
+ #define DF_MAX_GAMEPADS 4
52
+ static SDL_Gamepad *g_gamepads[DF_MAX_GAMEPADS];
53
+
54
+ // In-flight frame state between df_begin and df_end.
55
+ static WGPUSurfaceTexture g_frame_texture;
56
+ static WGPUTextureView g_frame_view;
57
+ static WGPUCommandEncoder g_frame_encoder;
58
+ static WGPURenderPassEncoder g_frame_pass;
59
+
60
+ static void on_adapter(WGPURequestAdapterStatus status, WGPUAdapter adapter, WGPUStringView message, void *ud1, void *ud2) {
61
+ (void)ud2;
62
+ if (status == WGPURequestAdapterStatus_Success) *(WGPUAdapter *)ud1 = adapter;
63
+ else fprintf(stderr, "dotframe: adapter request failed: %.*s\n", (int)message.length, message.data);
64
+ }
65
+
66
+ static void on_device(WGPURequestDeviceStatus status, WGPUDevice device, WGPUStringView message, void *ud1, void *ud2) {
67
+ (void)ud2;
68
+ if (status == WGPURequestDeviceStatus_Success) *(WGPUDevice *)ud1 = device;
69
+ else fprintf(stderr, "dotframe: device request failed: %.*s\n", (int)message.length, message.data);
70
+ }
71
+
72
+ static WGPUSurface create_surface(void) {
73
+ WGPUSurfaceDescriptor desc = WGPU_SURFACE_DESCRIPTOR_INIT;
74
+ #if defined(__APPLE__)
75
+ SDL_MetalView view = SDL_Metal_CreateView(g_window);
76
+ WGPUSurfaceSourceMetalLayer src = WGPU_SURFACE_SOURCE_METAL_LAYER_INIT;
77
+ src.layer = SDL_Metal_GetLayer(view);
78
+ desc.nextInChain = &src.chain;
79
+ return wgpuInstanceCreateSurface(g_instance, &desc);
80
+ #elif defined(_WIN32)
81
+ SDL_PropertiesID props = SDL_GetWindowProperties(g_window);
82
+ WGPUSurfaceSourceWindowsHWND src = WGPU_SURFACE_SOURCE_WINDOWS_HWND_INIT;
83
+ src.hwnd = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
84
+ src.hinstance = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_INSTANCE_POINTER, NULL);
85
+ desc.nextInChain = &src.chain;
86
+ return wgpuInstanceCreateSurface(g_instance, &desc);
87
+ #else
88
+ return NULL;
89
+ #endif
90
+ }
91
+
92
+ static void configure_surface(void) {
93
+ SDL_GetWindowSizeInPixels(g_window, &g_width, &g_height);
94
+ WGPUSurfaceConfiguration config = WGPU_SURFACE_CONFIGURATION_INIT;
95
+ config.device = g_device;
96
+ config.format = g_format;
97
+ config.usage = WGPUTextureUsage_RenderAttachment;
98
+ config.width = (uint32_t)g_width;
99
+ config.height = (uint32_t)g_height;
100
+ config.presentMode = g_present_mode;
101
+ config.alphaMode = WGPUCompositeAlphaMode_Auto;
102
+ wgpuSurfaceConfigure(g_surface, &config);
103
+
104
+ if (g_depth_view) wgpuTextureViewRelease(g_depth_view);
105
+ if (g_depth_texture) wgpuTextureRelease(g_depth_texture);
106
+ WGPUTextureDescriptor depth = WGPU_TEXTURE_DESCRIPTOR_INIT;
107
+ depth.usage = WGPUTextureUsage_RenderAttachment;
108
+ depth.size = (WGPUExtent3D){(uint32_t)g_width, (uint32_t)g_height, 1};
109
+ depth.format = WGPUTextureFormat_Depth24Plus;
110
+ g_depth_texture = wgpuDeviceCreateTexture(g_device, &depth);
111
+ g_depth_view = wgpuTextureCreateView(g_depth_texture, NULL);
112
+ }
113
+
114
+ int32_t df_open(int32_t width, int32_t height, const uint8_t *title, size_t title_len) {
115
+ char title_buf[256];
116
+ size_t n = title_len < sizeof title_buf - 1 ? title_len : sizeof title_buf - 1;
117
+ memcpy(title_buf, title, n);
118
+ title_buf[n] = 0;
119
+
120
+ if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) return -1;
121
+ SDL_WindowFlags flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE;
122
+ #if defined(__APPLE__)
123
+ flags |= SDL_WINDOW_METAL;
124
+ #endif
125
+ #if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
126
+ flags |= SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS;
127
+ #endif
128
+ g_window = SDL_CreateWindow(title_buf, width, height, flags);
129
+ if (!g_window) return -2;
130
+ // A freshly launched game should take focus, so keyboard and mouse reach it.
131
+ SDL_RaiseWindow(g_window);
132
+
133
+ g_instance = wgpuCreateInstance(NULL);
134
+ g_surface = create_surface();
135
+ if (!g_surface) return -3;
136
+
137
+ WGPURequestAdapterOptions adapter_opts = WGPU_REQUEST_ADAPTER_OPTIONS_INIT;
138
+ adapter_opts.compatibleSurface = g_surface;
139
+ WGPURequestAdapterCallbackInfo adapter_cb = WGPU_REQUEST_ADAPTER_CALLBACK_INFO_INIT;
140
+ adapter_cb.mode = WGPUCallbackMode_AllowSpontaneous;
141
+ adapter_cb.callback = on_adapter;
142
+ adapter_cb.userdata1 = &g_adapter;
143
+ wgpuInstanceRequestAdapter(g_instance, &adapter_opts, adapter_cb);
144
+ if (!g_adapter) {
145
+ // GPU-less machines (CI runners, VMs) may still expose a software adapter such as WARP.
146
+ adapter_opts.forceFallbackAdapter = 1;
147
+ wgpuInstanceRequestAdapter(g_instance, &adapter_opts, adapter_cb);
148
+ }
149
+ if (!g_adapter) return -4;
150
+ WGPUAdapterInfo info = WGPU_ADAPTER_INFO_INIT;
151
+ wgpuAdapterGetInfo(g_adapter, &info);
152
+ fprintf(stderr, "dotframe: adapter %.*s (backend %d, type %d)\n", (int)info.device.length, info.device.data,
153
+ (int)info.backendType, (int)info.adapterType);
154
+ wgpuAdapterInfoFreeMembers(info);
155
+
156
+ WGPURequestDeviceCallbackInfo device_cb = WGPU_REQUEST_DEVICE_CALLBACK_INFO_INIT;
157
+ device_cb.mode = WGPUCallbackMode_AllowSpontaneous;
158
+ device_cb.callback = on_device;
159
+ device_cb.userdata1 = &g_device;
160
+ wgpuAdapterRequestDevice(g_adapter, NULL, device_cb);
161
+ if (!g_device) return -5;
162
+ g_queue = wgpuDeviceGetQueue(g_device);
163
+ // Nearest filtering keeps pixel art crisp.
164
+ WGPUSamplerDescriptor sampler = WGPU_SAMPLER_DESCRIPTOR_INIT;
165
+ g_sampler = wgpuDeviceCreateSampler(g_device, &sampler);
166
+ sampler.magFilter = WGPUFilterMode_Linear;
167
+ sampler.minFilter = WGPUFilterMode_Linear;
168
+ g_sampler_linear = wgpuDeviceCreateSampler(g_device, &sampler);
169
+
170
+ WGPUSurfaceCapabilities caps = WGPU_SURFACE_CAPABILITIES_INIT;
171
+ wgpuSurfaceGetCapabilities(g_surface, g_adapter, &caps);
172
+ // Match the browser's preferred canvas format (non-sRGB) so colors agree across targets.
173
+ // DF_VSYNC=0 uncaps the frame rate for benchmarks when the surface supports it.
174
+ const char *vsync = SDL_getenv("DF_VSYNC");
175
+ if (vsync && vsync[0] == '0') {
176
+ for (size_t i = 0; i < caps.presentModeCount; i++) {
177
+ if (caps.presentModes[i] == WGPUPresentMode_Immediate) g_present_mode = WGPUPresentMode_Immediate;
178
+ else if (caps.presentModes[i] == WGPUPresentMode_Mailbox && g_present_mode != WGPUPresentMode_Immediate)
179
+ g_present_mode = WGPUPresentMode_Mailbox;
180
+ }
181
+ }
182
+ g_format = caps.formats[0];
183
+ for (size_t i = 0; i < caps.formatCount; i++) {
184
+ if (caps.formats[i] == WGPUTextureFormat_BGRA8Unorm || caps.formats[i] == WGPUTextureFormat_RGBA8Unorm) {
185
+ g_format = caps.formats[i];
186
+ break;
187
+ }
188
+ }
189
+ wgpuSurfaceCapabilitiesFreeMembers(caps);
190
+ configure_surface();
191
+ return 0;
192
+ }
193
+
194
+ // SDL scancode, as listed in SDL_scancode.h.
195
+ uint8_t df_key_down(int32_t scancode) {
196
+ int count = 0;
197
+ const bool *state = SDL_GetKeyboardState(&count);
198
+ return scancode >= 0 && scancode < count && state[scancode];
199
+ }
200
+
201
+ static SDL_Gamepad *gamepad_at(int32_t pad) { return pad >= 0 && pad < DF_MAX_GAMEPADS ? g_gamepads[pad] : NULL; }
202
+
203
+ // SDL_GamepadAxis on gamepad slot pad; returns [-1, 1], or 0 when the slot is empty.
204
+ double df_gamepad_axis(int32_t pad, int32_t axis) {
205
+ SDL_Gamepad *gamepad = gamepad_at(pad);
206
+ return gamepad ? SDL_GetGamepadAxis(gamepad, (SDL_GamepadAxis)axis) / 32767.0 : 0.0;
207
+ }
208
+
209
+ // SDL_GamepadButton on gamepad slot pad.
210
+ uint8_t df_gamepad_button(int32_t pad, int32_t button) {
211
+ SDL_Gamepad *gamepad = gamepad_at(pad);
212
+ return gamepad && SDL_GetGamepadButton(gamepad, (SDL_GamepadButton)button);
213
+ }
214
+
215
+ // Mouse position relative to the window's content, normalized to [0, 1] inside it (outside values extend past).
216
+ // Uses global state so the position does not depend on the window holding mouse focus.
217
+ static void mouse_relative(double *nx, double *ny) {
218
+ float gx = 0, gy = 0;
219
+ int wx = 0, wy = 0, w = 1, h = 1;
220
+ SDL_GetGlobalMouseState(&gx, &gy);
221
+ SDL_GetWindowPosition(g_window, &wx, &wy);
222
+ SDL_GetWindowSize(g_window, &w, &h);
223
+ *nx = w > 0 ? (gx - wx) / w : 0.0;
224
+ *ny = h > 0 ? (gy - wy) / h : 0.0;
225
+ }
226
+
227
+ // Field 0 x, 1 y (normalized), 2 buttons (bit 0 left, bit 1 middle, bit 2 right).
228
+ double df_mouse(int32_t field) {
229
+ if (field == 2) {
230
+ SDL_MouseButtonFlags flags = SDL_GetGlobalMouseState(NULL, NULL);
231
+ return (flags & SDL_BUTTON_LMASK ? 1 : 0) | (flags & SDL_BUTTON_MMASK ? 2 : 0) | (flags & SDL_BUTTON_RMASK ? 4 : 0);
232
+ }
233
+ double x, y;
234
+ mouse_relative(&x, &y);
235
+ return field == 0 ? x : y;
236
+ }
237
+
238
+ // Writes the per-user writable directory for org/app (UTF-8, trailing separator) into out; returns its length or -1.
239
+ int32_t df_pref_path(const uint8_t *org, size_t org_len, const uint8_t *app, size_t app_len, uint8_t *out, size_t out_len) {
240
+ char org_buf[128], app_buf[128];
241
+ size_t n = org_len < sizeof org_buf - 1 ? org_len : sizeof org_buf - 1;
242
+ memcpy(org_buf, org, n);
243
+ org_buf[n] = 0;
244
+ n = app_len < sizeof app_buf - 1 ? app_len : sizeof app_buf - 1;
245
+ memcpy(app_buf, app, n);
246
+ app_buf[n] = 0;
247
+ char *path = SDL_GetPrefPath(org_buf, app_buf);
248
+ if (!path) return -1;
249
+ size_t len = strlen(path);
250
+ if (len > out_len) {
251
+ SDL_free(path);
252
+ return -1;
253
+ }
254
+ memcpy(out, path, len);
255
+ SDL_free(path);
256
+ return (int32_t)len;
257
+ }
258
+
259
+ int32_t df_width(void) { return g_width; }
260
+ int32_t df_height(void) { return g_height; }
261
+
262
+ int32_t df_buffer(uint32_t usage, const uint8_t *data, size_t len) {
263
+ if (g_buffer_count >= DF_MAX_BUFFERS) return -1;
264
+ WGPUBufferDescriptor desc = WGPU_BUFFER_DESCRIPTOR_INIT;
265
+ desc.size = (len + 3) & ~(size_t)3;
266
+ desc.usage = WGPUBufferUsage_CopyDst;
267
+ if (usage & DF_USAGE_VERTEX) desc.usage |= WGPUBufferUsage_Vertex;
268
+ if (usage & DF_USAGE_INDEX) desc.usage |= WGPUBufferUsage_Index;
269
+ if (usage & DF_USAGE_UNIFORM) desc.usage |= WGPUBufferUsage_Uniform;
270
+ WGPUBuffer buffer = wgpuDeviceCreateBuffer(g_device, &desc);
271
+ if (!buffer) return -2;
272
+ if (len) wgpuQueueWriteBuffer(g_queue, buffer, 0, data, len);
273
+ g_buffers[g_buffer_count] = buffer;
274
+ return g_buffer_count++;
275
+ }
276
+
277
+ void df_buffer_destroy(int32_t buffer) {
278
+ if (buffer < 0 || buffer >= g_buffer_count || !g_buffers[buffer]) return;
279
+ wgpuBufferRelease(g_buffers[buffer]);
280
+ g_buffers[buffer] = NULL;
281
+ }
282
+
283
+ void df_buffer_write(int32_t buffer, const uint8_t *data, size_t len) {
284
+ if (buffer < 0 || buffer >= g_buffer_count || !g_buffers[buffer]) return;
285
+ wgpuQueueWriteBuffer(g_queue, g_buffers[buffer], 0, data, len);
286
+ }
287
+
288
+ // smooth selects linear filtering (fonts, photos) instead of nearest (pixel art).
289
+ int32_t df_texture(int32_t width, int32_t height, const uint8_t *rgba, size_t len, uint8_t smooth) {
290
+ if (g_texture_count >= DF_MAX_TEXTURES || width <= 0 || height <= 0) return -1;
291
+ if (len < (size_t)width * (size_t)height * 4) return -2;
292
+ WGPUTextureDescriptor desc = WGPU_TEXTURE_DESCRIPTOR_INIT;
293
+ desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst;
294
+ desc.size = (WGPUExtent3D){(uint32_t)width, (uint32_t)height, 1};
295
+ desc.format = WGPUTextureFormat_RGBA8Unorm;
296
+ WGPUTexture texture = wgpuDeviceCreateTexture(g_device, &desc);
297
+ WGPUTexelCopyTextureInfo dst = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT;
298
+ dst.texture = texture;
299
+ WGPUTexelCopyBufferLayout layout = WGPU_TEXEL_COPY_BUFFER_LAYOUT_INIT;
300
+ layout.bytesPerRow = (uint32_t)width * 4;
301
+ layout.rowsPerImage = (uint32_t)height;
302
+ wgpuQueueWriteTexture(g_queue, &dst, rgba, (size_t)width * (size_t)height * 4, &layout, &desc.size);
303
+ int32_t id = g_texture_count++;
304
+ g_textures[id] = texture;
305
+ g_texture_views[id] = wgpuTextureCreateView(texture, NULL);
306
+ g_texture_sizes[id][0] = width;
307
+ g_texture_sizes[id][1] = height;
308
+ g_texture_smooth[id] = smooth;
309
+ return id;
310
+ }
311
+
312
+ // Decodes PNG bytes into a texture.
313
+ int32_t df_image(const uint8_t *png, size_t len, uint8_t smooth) {
314
+ int width, height, channels;
315
+ stbi_uc *pixels = stbi_load_from_memory(png, (int)len, &width, &height, &channels, 4);
316
+ if (!pixels) return -3;
317
+ int32_t id = df_texture(width, height, pixels, (size_t)width * (size_t)height * 4, smooth);
318
+ stbi_image_free(pixels);
319
+ return id;
320
+ }
321
+
322
+ // axis 0 width, 1 height.
323
+ int32_t df_texture_size(int32_t texture, int32_t axis) {
324
+ return texture >= 0 && texture < g_texture_count ? g_texture_sizes[texture][axis ? 1 : 0] : 0;
325
+ }
326
+
327
+ // attrs: little-endian u32 triples (format, offset, shaderLocation).
328
+ int32_t df_pipeline(const uint8_t *wgsl, size_t wgsl_len, uint32_t stride, const uint8_t *attrs, size_t attrs_len,
329
+ uint32_t flags) {
330
+ if (g_pipeline_count >= DF_MAX_PIPELINES) return -1;
331
+ WGPUShaderSourceWGSL src = WGPU_SHADER_SOURCE_WGSL_INIT;
332
+ src.code = (WGPUStringView){(const char *)wgsl, wgsl_len};
333
+ WGPUShaderModuleDescriptor module_desc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
334
+ module_desc.nextInChain = &src.chain;
335
+ WGPUShaderModule module = wgpuDeviceCreateShaderModule(g_device, &module_desc);
336
+
337
+ WGPUVertexAttribute attributes[16];
338
+ size_t attribute_count = attrs_len / 12;
339
+ if (attribute_count > 16) attribute_count = 16;
340
+ for (size_t i = 0; i < attribute_count; i++) {
341
+ const uint32_t *t = (const uint32_t *)(attrs + i * 12);
342
+ static const WGPUVertexFormat formats[] = {WGPUVertexFormat_Float32x2, WGPUVertexFormat_Float32x3,
343
+ WGPUVertexFormat_Float32x4, WGPUVertexFormat_Float32,
344
+ WGPUVertexFormat_Unorm8x4};
345
+ attributes[i] = (WGPUVertexAttribute)WGPU_VERTEX_ATTRIBUTE_INIT;
346
+ attributes[i].format = t[0] <= DF_FORMAT_UNORM8X4 ? formats[t[0]] : WGPUVertexFormat_Float32x3;
347
+ attributes[i].offset = t[1];
348
+ attributes[i].shaderLocation = t[2];
349
+ }
350
+ WGPUVertexBufferLayout layout = WGPU_VERTEX_BUFFER_LAYOUT_INIT;
351
+ layout.arrayStride = stride;
352
+ layout.attributeCount = attribute_count;
353
+ layout.attributes = attributes;
354
+
355
+ WGPUBlendState blend = WGPU_BLEND_STATE_INIT;
356
+ blend.color = (WGPUBlendComponent){WGPUBlendOperation_Add, WGPUBlendFactor_SrcAlpha, WGPUBlendFactor_OneMinusSrcAlpha};
357
+ blend.alpha = (WGPUBlendComponent){WGPUBlendOperation_Add, WGPUBlendFactor_One, WGPUBlendFactor_OneMinusSrcAlpha};
358
+ WGPUColorTargetState target = WGPU_COLOR_TARGET_STATE_INIT;
359
+ target.format = g_format;
360
+ if (flags & DF_PIPELINE_BLEND) target.blend = &blend;
361
+ WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
362
+ fragment.module = module;
363
+ fragment.entryPoint = (WGPUStringView){"fs_main", WGPU_STRLEN};
364
+ fragment.targetCount = 1;
365
+ fragment.targets = &target;
366
+
367
+ WGPUDepthStencilState depth = WGPU_DEPTH_STENCIL_STATE_INIT;
368
+ depth.format = WGPUTextureFormat_Depth24Plus;
369
+ depth.depthWriteEnabled = WGPUOptionalBool_True;
370
+ depth.depthCompare = WGPUCompareFunction_Less;
371
+
372
+ WGPURenderPipelineDescriptor desc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
373
+ desc.vertex.module = module;
374
+ desc.vertex.entryPoint = (WGPUStringView){"vs_main", WGPU_STRLEN};
375
+ if (stride > 0) {
376
+ desc.vertex.bufferCount = 1;
377
+ desc.vertex.buffers = &layout;
378
+ }
379
+ desc.fragment = &fragment;
380
+ if (flags & DF_PIPELINE_DEPTH) {
381
+ desc.depthStencil = &depth;
382
+ desc.primitive.cullMode = WGPUCullMode_Back;
383
+ }
384
+ WGPURenderPipeline pipeline = wgpuDeviceCreateRenderPipeline(g_device, &desc);
385
+ wgpuShaderModuleRelease(module);
386
+ if (!pipeline) return -2;
387
+ g_pipelines[g_pipeline_count] = pipeline;
388
+ return g_pipeline_count++;
389
+ }
390
+
391
+ // Group 0 of the pipeline's auto layout: binding 0 uniform buffer, binding 1 texture, binding 2 sampler.
392
+ // Pass -1 for a resource the shader does not declare.
393
+ int32_t df_bind(int32_t pipeline, int32_t buffer, int32_t texture) {
394
+ if (pipeline < 0 || pipeline >= g_pipeline_count) return -1;
395
+ if (g_bind_group_count >= DF_MAX_BIND_GROUPS) return -2;
396
+ WGPUBindGroupEntry entries[3];
397
+ size_t count = 0;
398
+ if (buffer >= 0 && buffer < g_buffer_count) {
399
+ entries[count] = (WGPUBindGroupEntry)WGPU_BIND_GROUP_ENTRY_INIT;
400
+ entries[count].binding = 0;
401
+ entries[count].buffer = g_buffers[buffer];
402
+ entries[count].size = wgpuBufferGetSize(g_buffers[buffer]);
403
+ count++;
404
+ }
405
+ if (texture >= 0 && texture < g_texture_count) {
406
+ entries[count] = (WGPUBindGroupEntry)WGPU_BIND_GROUP_ENTRY_INIT;
407
+ entries[count].binding = 1;
408
+ entries[count].textureView = g_texture_views[texture];
409
+ count++;
410
+ entries[count] = (WGPUBindGroupEntry)WGPU_BIND_GROUP_ENTRY_INIT;
411
+ entries[count].binding = 2;
412
+ entries[count].sampler = g_texture_smooth[texture] ? g_sampler_linear : g_sampler;
413
+ count++;
414
+ }
415
+ WGPUBindGroupDescriptor desc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
416
+ desc.layout = wgpuRenderPipelineGetBindGroupLayout(g_pipelines[pipeline], 0);
417
+ desc.entryCount = count;
418
+ desc.entries = entries;
419
+ WGPUBindGroup group = wgpuDeviceCreateBindGroup(g_device, &desc);
420
+ wgpuBindGroupLayoutRelease(desc.layout);
421
+ g_bind_groups[g_bind_group_count] = group;
422
+ return g_bind_group_count++;
423
+ }
424
+
425
+ #define DF_MAX_TOUCHES 10
426
+
427
+ typedef struct {
428
+ SDL_FingerID id;
429
+ float x;
430
+ float y;
431
+ } Touch;
432
+
433
+ static Touch g_touches[DF_MAX_TOUCHES];
434
+ static int32_t g_touch_count;
435
+ static uint8_t g_quit;
436
+
437
+ static void touch_set(SDL_FingerID id, float x, float y) {
438
+ for (int32_t i = 0; i < g_touch_count; i++) {
439
+ if (g_touches[i].id == id) {
440
+ g_touches[i].x = x;
441
+ g_touches[i].y = y;
442
+ return;
443
+ }
444
+ }
445
+ if (g_touch_count < DF_MAX_TOUCHES) g_touches[g_touch_count++] = (Touch){id, x, y};
446
+ }
447
+
448
+ static void touch_remove(SDL_FingerID id) {
449
+ for (int32_t i = 0; i < g_touch_count; i++) {
450
+ if (g_touches[i].id == id) {
451
+ g_touches[i] = g_touches[--g_touch_count];
452
+ return;
453
+ }
454
+ }
455
+ }
456
+
457
+ // Applies one SDL event. Desktop builds call it from df_poll; hosts using SDL main callbacks (iOS) call it from
458
+ // SDL_AppEvent.
459
+ void df_handle_event(const SDL_Event *event) {
460
+ switch (event->type) {
461
+ case SDL_EVENT_QUIT:
462
+ case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
463
+ g_quit = 1;
464
+ break;
465
+ case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
466
+ configure_surface();
467
+ break;
468
+ case SDL_EVENT_FINGER_DOWN:
469
+ case SDL_EVENT_FINGER_MOTION:
470
+ touch_set(event->tfinger.fingerID, event->tfinger.x, event->tfinger.y);
471
+ break;
472
+ case SDL_EVENT_FINGER_UP:
473
+ case SDL_EVENT_FINGER_CANCELED:
474
+ touch_remove(event->tfinger.fingerID);
475
+ break;
476
+ case SDL_EVENT_GAMEPAD_ADDED:
477
+ // Gamepads keep the first free slot they get, so player 1 stays player 1 across reconnects of others.
478
+ for (int i = 0; i < DF_MAX_GAMEPADS; i++) {
479
+ if (!g_gamepads[i]) {
480
+ g_gamepads[i] = SDL_OpenGamepad(event->gdevice.which);
481
+ break;
482
+ }
483
+ }
484
+ break;
485
+ case SDL_EVENT_GAMEPAD_REMOVED:
486
+ for (int i = 0; i < DF_MAX_GAMEPADS; i++) {
487
+ if (g_gamepads[i] && SDL_GetGamepadID(g_gamepads[i]) == event->gdevice.which) {
488
+ SDL_CloseGamepad(g_gamepads[i]);
489
+ g_gamepads[i] = NULL;
490
+ }
491
+ }
492
+ break;
493
+ default:
494
+ break;
495
+ }
496
+ }
497
+
498
+ // Drains pending events; returns false once the window is asked to close.
499
+ uint8_t df_poll(void) {
500
+ SDL_Event event;
501
+ while (SDL_PollEvent(&event)) df_handle_event(&event);
502
+ return !g_quit;
503
+ }
504
+
505
+ int32_t df_touch_count(void) { return g_touch_count; }
506
+
507
+ // Field of touch i: 0 stable id while the finger stays down, 1 x and 2 y normalized to the window.
508
+ double df_touch(int32_t i, int32_t field) {
509
+ if (i < 0 || i >= g_touch_count) return field == 0 ? -1.0 : 0.0;
510
+ return field == 0 ? (double)g_touches[i].id : field == 1 ? g_touches[i].x : g_touches[i].y;
511
+ }
512
+
513
+ int32_t df_begin(double r, double g, double b, uint8_t use_depth) {
514
+ g_frame_texture = (WGPUSurfaceTexture)WGPU_SURFACE_TEXTURE_INIT;
515
+ wgpuSurfaceGetCurrentTexture(g_surface, &g_frame_texture);
516
+ if (g_frame_texture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
517
+ g_frame_texture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) {
518
+ if (g_frame_texture.texture) wgpuTextureRelease(g_frame_texture.texture);
519
+ configure_surface();
520
+ // No drawable (window hidden or occluded): the frame is skipped, but buffer writes made for it are already
521
+ // queued and their staging is only reclaimed after a submit. Submit nothing so they apply and recycle;
522
+ // otherwise a hidden window grows by one frame's uploads per frame (gigabytes per second at 50k sprites).
523
+ WGPUSubmissionIndex submitted = wgpuQueueSubmitForIndex(g_queue, 0, NULL);
524
+ wgpuDevicePoll(g_device, 1, &submitted);
525
+ return 1;
526
+ }
527
+ g_frame_view = wgpuTextureCreateView(g_frame_texture.texture, NULL);
528
+ g_frame_encoder = wgpuDeviceCreateCommandEncoder(g_device, NULL);
529
+
530
+ WGPURenderPassColorAttachment color = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
531
+ color.view = g_frame_view;
532
+ color.loadOp = WGPULoadOp_Clear;
533
+ color.storeOp = WGPUStoreOp_Store;
534
+ color.clearValue = (WGPUColor){r, g, b, 1.0};
535
+ WGPURenderPassDepthStencilAttachment depth = WGPU_RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_INIT;
536
+ depth.view = g_depth_view;
537
+ depth.depthLoadOp = WGPULoadOp_Clear;
538
+ depth.depthStoreOp = WGPUStoreOp_Store;
539
+ depth.depthClearValue = 1.0f;
540
+ WGPURenderPassDescriptor pass_desc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
541
+ pass_desc.colorAttachmentCount = 1;
542
+ pass_desc.colorAttachments = &color;
543
+ if (use_depth) pass_desc.depthStencilAttachment = &depth;
544
+ g_frame_pass = wgpuCommandEncoderBeginRenderPass(g_frame_encoder, &pass_desc);
545
+ return 0;
546
+ }
547
+
548
+ // Negative handles mean "none". first/count are indices with an index buffer (uint32), vertices otherwise.
549
+ void df_draw(int32_t pipeline, int32_t bind_group, int32_t vertex_buffer, int32_t index_buffer, uint32_t first,
550
+ uint32_t count) {
551
+ if (!g_frame_pass || pipeline < 0 || pipeline >= g_pipeline_count) return;
552
+ wgpuRenderPassEncoderSetPipeline(g_frame_pass, g_pipelines[pipeline]);
553
+ if (bind_group >= 0 && bind_group < g_bind_group_count)
554
+ wgpuRenderPassEncoderSetBindGroup(g_frame_pass, 0, g_bind_groups[bind_group], 0, NULL);
555
+ if (vertex_buffer >= 0 && vertex_buffer < g_buffer_count)
556
+ wgpuRenderPassEncoderSetVertexBuffer(g_frame_pass, 0, g_buffers[vertex_buffer], 0, WGPU_WHOLE_SIZE);
557
+ if (index_buffer >= 0 && index_buffer < g_buffer_count) {
558
+ wgpuRenderPassEncoderSetIndexBuffer(g_frame_pass, g_buffers[index_buffer], WGPUIndexFormat_Uint32, 0,
559
+ WGPU_WHOLE_SIZE);
560
+ wgpuRenderPassEncoderDrawIndexed(g_frame_pass, count, 1, first, 0, 0);
561
+ } else {
562
+ wgpuRenderPassEncoderDraw(g_frame_pass, count, 1, first, 0);
563
+ }
564
+ }
565
+
566
+ void df_end(void) {
567
+ if (!g_frame_pass) return;
568
+ wgpuRenderPassEncoderEnd(g_frame_pass);
569
+ wgpuRenderPassEncoderRelease(g_frame_pass);
570
+ g_frame_pass = NULL;
571
+ WGPUCommandBuffer commands = wgpuCommandEncoderFinish(g_frame_encoder, NULL);
572
+ wgpuQueueSubmit(g_queue, 1, &commands);
573
+ wgpuSurfacePresent(g_surface);
574
+ wgpuCommandBufferRelease(commands);
575
+ wgpuCommandEncoderRelease(g_frame_encoder);
576
+ wgpuTextureViewRelease(g_frame_view);
577
+ wgpuTextureRelease(g_frame_texture.texture);
578
+ }
579
+
580
+ void df_audio_close(void);
581
+
582
+ void df_close(void) {
583
+ df_audio_close();
584
+ for (int32_t i = 0; i < g_bind_group_count; i++) wgpuBindGroupRelease(g_bind_groups[i]);
585
+ for (int32_t i = 0; i < g_buffer_count; i++)
586
+ if (g_buffers[i]) wgpuBufferRelease(g_buffers[i]);
587
+ for (int32_t i = 0; i < g_texture_count; i++) {
588
+ wgpuTextureViewRelease(g_texture_views[i]);
589
+ wgpuTextureRelease(g_textures[i]);
590
+ }
591
+ if (g_sampler) wgpuSamplerRelease(g_sampler);
592
+ if (g_sampler_linear) wgpuSamplerRelease(g_sampler_linear);
593
+ for (int32_t i = 0; i < g_pipeline_count; i++) wgpuRenderPipelineRelease(g_pipelines[i]);
594
+ if (g_depth_view) wgpuTextureViewRelease(g_depth_view);
595
+ if (g_depth_texture) wgpuTextureRelease(g_depth_texture);
596
+ if (g_queue) wgpuQueueRelease(g_queue);
597
+ if (g_device) wgpuDeviceRelease(g_device);
598
+ if (g_adapter) wgpuAdapterRelease(g_adapter);
599
+ if (g_surface) wgpuSurfaceRelease(g_surface);
600
+ if (g_instance) wgpuInstanceRelease(g_instance);
601
+ for (int i = 0; i < DF_MAX_GAMEPADS; i++)
602
+ if (g_gamepads[i]) SDL_CloseGamepad(g_gamepads[i]);
603
+ if (g_window) SDL_DestroyWindow(g_window);
604
+ SDL_Quit();
605
+ }