Compare commits

..

3 Commits

12 changed files with 1413 additions and 1031 deletions

View File

@@ -1,31 +1,6 @@
# Cartograph
A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses line-strip vector shapes with procedural geometry.
## Features
- **Shapes** — parametric circles, stars, rectangles; freeform pen tool with Catmull-Rom splines
- **Instanced rendering** — shapes with identical local-space geometry share vertex buffers; per-instance transforms (position, scale, rotation) uploaded via SSBO
- **Bezier editing** — double-click any shape to edit its Bezier control points and handles in local space
- **Shape editing** — select, move, rotate, scale individual shapes; rect-select multiple shapes
- **Groups** — group/ungroup shapes with nested hierarchy support, group-level selection and focus mode
- **Clipboard** — copy/paste shapes with deep-copy of geometry, group remapping for pasted items
- **Undo/redo** — property-level history stack (position, scale, rotation, vertex edits, create/delete, group) with batch operations
- **Pen tool** — click to place control points; Enter, double-click, or close-to-start to commit; Escape to cancel
- **Spatial index** — open-addressing hash grid for fast hit testing, rect-selection, and viewport culling on large shape counts
- **Viewport** — zoom (scroll) and pan (right-click drag) with screen↔world coordinate transforms
- **Debug panel** — toggleable log overlay (backtick), FPS meter with 60-frame rolling average, log filtering
## Tech stack
| Layer | Library |
|---|---|
| Language | C (C99) |
| Compiler | [Emscripten](https://emscripten.org/) (emcc) → WebAssembly |
| Graphics | [Sokol](https://github.com/floooh/sokol) with WebGPU backend |
| UI | [Dear ImGui](https://github.com/ocornut/imgui) via [cimgui](https://github.com/cimgui/cimgui) |
| Math | [cglm](https://github.com/recp/cglm) |
| Shaders | WGSL (compiled to C headers via `xxd -i`) |
A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses a hierarchical tile-based SDF rendering.
## Build
@@ -42,66 +17,6 @@ make debug
Output is `app.html`, served by Emscripten's built-in web server or any static server.
## Project structure
```
src/
main.c Entry point, sokol init, render loop, input dispatch, UI panels, debug stats
api.h Central include hub — backend defines, all library headers, ALLOC/FREE macros
types.h Shared type definitions, constants, userdata_t
camera.h Viewport state, zoom/pan, MVP matrix (via glm_ortho), screen↔world transforms
render.h Shape & overlay pipeline init/shutdown, shader definitions
shape.h Shape geometry types, procedural generation, Bezier editing, vertex hash grouping, instanced buffer pool
spatial.h Spatial hash grid with linear probing, AABB queries, viewport culling
history.h Undo/redo stack — property-level tracking, vertex snapshots, batch operations
interact.h Selection AABB, group recursive helpers, resize handle hit-test, group rebuild
overlay.h Selection overlay geometry, rotate/corner handles, edit-mode anchor & handle visualization
draw.h Draw dispatch — frustum culling, instance map sorting, instanced draw calls
input.h Mouse/keyboard event handlers — select, move, rotate, resize, pen, edit mode, clipboard
ui_panels.h ImGui panels — toolbar, shape list tree, properties, debug log
util.h Stripe-based vector_t (dynamic array)
rand.h Xorshift32 PRNG
shaders/ WGSL shader sources (sprite, shape, overlay)
generated/ xxd-generated C headers from shaders
lib/
sokol/ Sokol single-file headers (gfx, app, glue, log, memtrack)
imgui/ Dear ImGui + cimgui
cglm/ C linear math library
util/ Sokol utility headers
```
## Architecture notes
### Instanced rendering with vertex hash grouping
Shapes share vertex buffers when their local-space geometry is identical. Each shape stores a 64-bit FNV-1a hash of its vertex data. The geometry pool groups shapes by `(num_elements, vertex_hash)` — not just vertex count. This means:
- **Parametric shapes** (circles, stars, rectangles from fixed formulas): all instances of the same type naturally produce the same hash, so they share one vertex buffer regardless of count.
- **Freeform paths** (pen tool): each path gets a unique hash, guaranteeing its own vertex buffer and preventing geometry corruption.
- **Bezier edits**: `shape_regenerate_from_ctrl` updates the hash and modifies the group buffer in-place via `sg_update_buffer` rather than destroying/recreating it.
The previous implementation grouped only by vertex count, which caused pen-drawn paths with matching counts to silently share the wrong geometry.
### Lazy group index rebuild
The `g_group_by_id` lookup array is rebuilt lazily. Operations that modify groups set a `g_group_index_dirty` flag; the actual rebuild happens on the first `find_group()` call afterward. This avoids redundant rebuilds when multiple group operations occur within the same frame (e.g., undo then redo, or group then ungroup).
### Hover state optimization
`handle_hover` (called every frame) tracks the previous set of highlighted shapes (up to 64) and only toggles state on shapes entering or leaving the highlight set. The O(n) full-array sweep is only used as a fallback when the highlight set exceeds 64 shapes.
### Pool rebuild granularity
When the geometry pool rebuilds, existing group vertex buffers whose `(num_elements, vertex_hash)` key still exists are preserved rather than destroyed and recreated. Only new keys trigger buffer creation, and only orphaned keys trigger destruction. The shape data SSBO is also preserved when its size hasn't changed.
### Spatial grid memory reuse
The spatial hash grid retains per-slot entry arrays across rebuilds. Only slots whose shape count has grown beyond their current capacity trigger a reallocation.
### Log deduplication
The debug log ring buffer uses a 64-bit message hash for fast deduplication of warnings and errors (levels 0-2). Debug-level messages (level 3) skip dedup entirely to avoid the linear scan cost when verbose logging is active.
## License
MIT

View File

@@ -59,14 +59,14 @@ else
echo " > cglm already present"
fi
# 5. sokol_gp.h
if [ ! -f "$LIB_DIR/sokol/sokol_gp.h" ]; then
echo " > Fetching cglm..."
git clone --depth 1 https://github.com/edubart/sokol_gp.git "$LIB_DIR/sokol_gp_tmp"
cp -r "$LIB_DIR/sokol_gp_tmp/sokol_gp.h" "$LIB_DIR/sokol/sokol_gp.h"
rm -rf "$LIB_DIR/sokol_gp_tmp"
# 5. stb_ds.h
if [ ! -f "$LIB_DIR/util/stb_ds.h" ]; then
echo " > Fetching STB..."
git clone --depth 1 https://github.com/nothings/stb.git "$LIB_DIR/stb_tmp"
cp -r "$LIB_DIR/stb_tmp/stb_ds.h" "$LIB_DIR/util/stb_ds.h"
rm -rf "$LIB_DIR/stb_tmp"
else
echo " > sokol_gp.h already present"
echo " > stb_ds.h already present"
fi
echo "=== Done ==="

View File

@@ -31,8 +31,8 @@ EMCC_FLAGS = --use-port=emdawnwebgpu \
-sALLOW_MEMORY_GROWTH \
-msimd128 \
-sFILESYSTEM=0 \
-flto \
-Rpass=loop-vectorize
-sMALLOC=emmalloc \
-flto
# Shell template
SHELL_FILE = shell.html
@@ -46,6 +46,7 @@ $(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) $(SHE
-o $(TARGET) \
$(EMCC_FLAGS) \
-O3 \
--closure 1 \
-I$(LIB_DIR)/sokol \
-I$(LIB_DIR)/imgui \
-I$(LIB_DIR)/imgui/imgui \
@@ -66,7 +67,7 @@ debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES)
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
-o $(TARGET) \
$(EMCC_FLAGS) \
-g --profiling-funcs -gsource-map=inline \
-g3 --profiling-funcs -gsource-map \
-sASSERTIONS \
-I$(LIB_DIR)/sokol \
-I$(LIB_DIR)/imgui \

View File

@@ -1,25 +1,58 @@
#ifndef API_DEFINITION
#define API_DEFINITION
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#define SOKOL_IMPL
#define SOKOL_WGPU
#define SOKOL_IMGUI_IMPL
#include "sokol_gfx.h"
#include "sokol_app.h"
#include "sokol_glue.h"
#include "cimgui.h"
#include "sokol_imgui.h"
#include <emscripten.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdarg.h>
#include <emscripten/emmalloc.h>
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
static void log_fn(const char* tag, uint32_t log_level, uint32_t log_item_id, const char* message_or_null, uint32_t line_nr, const char* filename_or_null, void* user_data)
{
fprintf(stderr, "[%s - %s]: (%d) %s \tat %s:%d\r\n", tag, log_level == 0 ? "FATAL" : log_level == 1 ? "ERROR" : log_level == 2 ? "WARNING" : "INFO", log_item_id, message_or_null, filename_or_null, line_nr);
}
#define SOKOL_ASSERT(x) ((void)((x) || (log_fn("ASSERT", 1, 1, "Assertion failed", __LINE__, __FILE__, NULL),0)))
#define SOKOL_IMPL
#define SOKOL_WGPU
#define SOKOL_IMGUI_IMPL
#define STB_DS_IMPLEMENTATION
#define STBDS_NO_SHORT_NAMES
#define STBDS_REALLOC(context,ptr,size) emmalloc_realloc(ptr, size)
#define STBDS_FREE(context,ptr) emmalloc_free(ptr)
#define COMPUTE_VIEWIDX_segments 0
#define COMPUTE_VIEWIDX_shapes 1
#define COMPUTE_VIEWIDX_circles 2
#define COMPUTE_VIEWIDX_tiles 3
#define COMPUTE_VIEWIDX_indices 4
#define COMPUTE_VIEWIDX_SDF 5
#define DISPLAY_VIEWIDX_SDF 0
#define DISPLAY_VIEWIDX_Sampler 1
static float clampf(float x, float a, float b)
{
return x < a ? a : (x > b ? b : x);
}
#include "sokol_gfx.h"
#include "sokol_app.h"
#include "sokol_glue.h"
#include "cimgui.h"
#include "sokol_imgui.h"
#include "stb_ds.h"
#include "shape.h"
//#include "cache.h"
#include "pool.h"
#include "generated/compute.h"
#include "generated/display.h"

107
src/cache.h Normal file
View File

@@ -0,0 +1,107 @@
#include "api.h"
tile_slot_t* cache_evict(scene_t* s)
{
tile_slot_t* best = NULL;
uint64_t oldest = UINT64_MAX;
for(uint32_t i = 0; i < TILE_LAYERS; i++)
{
tile_slot_t* slot = &s->cache.slots[i];
if(slot->key.lod == UINT32_MAX) continue; // LOD == UINT32_MAX means the slot is free.
//Found a better candidate
if(slot->last_used < oldest)
{
oldest = slot->last_used;
best = slot;
}
}
if(best == NULL)
return best;
stbds_hmdel(s->cache.map, best->key);
s->cache.free_layers[s->cache.free_count] = best->layer;
s->cache.free_count++;
s->cache.slots[best->layer].key.lod = UINT32_MAX; //Mark the slot as free for the evict scan using lod == UINT32_MAX
return best;
}
uint32_t cache_allocate(scene_t* s)
{
assert(s->cache.free_count > 0);
s->cache.free_count--;
uint32_t layer = s->cache.free_layers[s->cache.free_count];
return layer;
}
tile_slot_t* cache_search(scene_t* s, tile_key_t* key, uint64_t frame_count)
{
uint32_t index = stbds_hmgeti(s->cache.map, *key);
if(index >= 0)
{
uint32_t layer = s->cache.map[index].value;
tile_slot_t* slot = &s->cache.slots[layer];
slot->last_used = frame_count;
return slot;
}
// Evict the least recently used (LRU) slot
if(stbds_hmlen(s->cache.map) >= TILE_LAYERS)
{
tile_slot_t* evict = cache_evict(s);
}
// Allocate a free layer
uint32_t layer = cache_allocate(s);
tile_slot_t* slot = &s->cache.slots[layer];
slot->key.lod = key->lod; slot->key.tx = key->tx; slot->key.ty = key->ty;
slot->layer = layer;
slot->state = TILE_STATE_DIRTY;
slot->last_used = frame_count;
stbds_hmput(s->cache.map, *key, layer);
return slot;
}
// Works in 3 steps, first we select every required tiles and store the mising one from the cache
// Then we evict the LRU cache until we have enough space for the missing tiles
// Finally we allocate the missing tiles
static int cache_query(scene_t* s, uint32_t lod, box_t* box, tile_key_t** buffer)
{
uint64_t frame = sapp_frame_count();
tile_key_t tile_key = { lod, 0, 0 };
for(uint32_t ty = box->min_y; ty < box->max_y; ty++)
{
tile_key.ty = ty;
for(uint32_t tx = box->min_x; tx < box->max_x; tx++)
{
tile_key.tx = tx;
stbds_hmget(s->cache.map, tile_key);
}
}
}
static int cache_cull(scene_t* s, uint32_t tile_count)
{
uint32_t count = 0;
for(uint32_t i = 0; i < tile_count; i++)
{
tile_slot_t slot = s->cache.slots[i];
tile_task_t* tile = &s->cache.tiles[i];
tile->bounds.min_x = slot.key.tx * s->LODs[slot.key.lod].texel_size;
tile->bounds.min_y = slot.key.ty * s->LODs[slot.key.lod].texel_size;
tile->bounds.max_x = (slot.key.tx + 1) * s->LODs[slot.key.lod].texel_size;
tile->bounds.max_y = (slot.key.ty + 1) * s->LODs[slot.key.lod].texel_size;
tile->layer = slot.layer;
uint32_t start = count;
for(uint32_t j = 0; j < s->num_shapes; j++)
{
if(BOX_INTERSECTS(tile->bounds, s->shapes[j].aabb)) s->cache.indices[count++] = j;
}
tile->offset = start; tile->count = count - start;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,47 @@
unsigned char src_shaders_display_wgsl[] = {
0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x55, 0x6e, 0x69, 0x66, 0x6f,
0x72, 0x6d, 0x73, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70,
0x61, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x0d, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x7a, 0x6f, 0x6f, 0x6d, 0x3a, 0x20, 0x66, 0x33,
0x32, 0x2c, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63,
0x74, 0x20, 0x54, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73,
0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x6f, 0x64, 0x3a,
0x20, 0x75, 0x33, 0x32, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74,
0x69, 0x6c, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x66, 0x33,
0x32, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x6f, 0x72, 0x6c,
0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66,
0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
0x5f, 0x6d, 0x61, 0x78, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c,
0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x40, 0x67, 0x72, 0x6f, 0x75,
0x70, 0x28, 0x30, 0x29, 0x20, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e,
0x67, 0x28, 0x30, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69,
0x66, 0x6f, 0x72, 0x6d, 0x3e, 0x20, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72,
0x6d, 0x73, 0x3a, 0x20, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73,
0x3b, 0x0d, 0x0a, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29,
0x20, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, 0x29,
0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d,
0x3e, 0x20, 0x74, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d,
0x73, 0x3a, 0x20, 0x54, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d,
0x73, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, 0x65,
0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e,
0x28, 0x40, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x76, 0x65,
0x72, 0x74, 0x65, 0x78, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x29, 0x20,
0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78,
0x20, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x40,
0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f, 0x73, 0x69,
0x74, 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x20,
0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20, 0x70,
0x6f, 0x73, 0x20, 0x3d, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x28, 0x0d,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x65, 0x63, 0x32, 0x28, 0x20, 0x30,
0x2e, 0x30, 0x2c, 0x20, 0x20, 0x30, 0x2e, 0x35, 0x29, 0x2c, 0x0d, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x76, 0x65, 0x63, 0x32, 0x28, 0x2d, 0x30, 0x2e,
0x35, 0x2c, 0x20, 0x2d, 0x30, 0x2e, 0x35, 0x29, 0x2c, 0x0d, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x76, 0x65, 0x63, 0x32, 0x28, 0x20, 0x30, 0x2e, 0x35,
0x2c, 0x20, 0x2d, 0x30, 0x2e, 0x35, 0x29, 0x0d, 0x0a, 0x20, 0x20, 0x29,
0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72,
0x6e, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x70, 0x6f, 0x73, 0x5b,
0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78,
0x5d, 0x2c, 0x20, 0x30, 0x2c, 0x20, 0x31, 0x29, 0x3b, 0x0d, 0x0a, 0x7d,
0x72, 0x6d, 0x73, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74,
0x69, 0x6d, 0x65, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x0d, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x3a, 0x20, 0x75, 0x33,
0x32, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x76, 0x70, 0x3a,
0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, 0x0d, 0x0a, 0x7d,
0x0d, 0x0a, 0x0d, 0x0a, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30,
0x29, 0x20, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x30,
0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72,
0x6d, 0x3e, 0x20, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x3a,
0x20, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x3b, 0x0d, 0x0a,
0x0d, 0x0a, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x31, 0x29, 0x20,
0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20,
0x76, 0x61, 0x72, 0x20, 0x73, 0x64, 0x66, 0x20, 0x3a, 0x20, 0x74, 0x65,
0x78, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x32, 0x64, 0x3c, 0x66, 0x33, 0x32,
0x3e, 0x3b, 0x0d, 0x0a, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x31,
0x29, 0x20, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31,
0x29, 0x20, 0x76, 0x61, 0x72, 0x20, 0x73, 0x64, 0x66, 0x5f, 0x73, 0x61,
0x6d, 0x70, 0x6c, 0x65, 0x72, 0x20, 0x3a, 0x20, 0x73, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x40, 0x76, 0x65, 0x72,
0x74, 0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61,
0x69, 0x6e, 0x28, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x29, 0x20, 0x2d, 0x3e, 0x20,
0x40, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f, 0x73,
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66,
0x0d, 0x0a, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74,
0x75, 0x72, 0x6e, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x70, 0x6f,
0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c,
0x20, 0x31, 0x2e, 0x30, 0x29, 0x20, 0x2a, 0x20, 0x75, 0x6e, 0x69, 0x66,
0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x3b, 0x0d, 0x0a, 0x7d,
0x0d, 0x0a, 0x0d, 0x0a, 0x40, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e,
0x74, 0x20, 0x66, 0x6e, 0x20, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e,
0x28, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66,
0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e,
0x20, 0x76, 0x65, 0x63, 0x34, 0x28, 0x31, 0x2c, 0x20, 0x30, 0x2c, 0x20,
0x30, 0x2c, 0x20, 0x31, 0x29, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a
0x28, 0x40, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f,
0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x69,
0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x29,
0x20, 0x2d, 0x3e, 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x28, 0x30, 0x29, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x0d, 0x0a,
0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72,
0x6e, 0x20, 0x74, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x53, 0x61, 0x6d,
0x70, 0x6c, 0x65, 0x28, 0x73, 0x64, 0x66, 0x2c, 0x20, 0x73, 0x64, 0x66,
0x5f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x72, 0x2c, 0x20, 0x70, 0x6f,
0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x79, 0x29, 0x3b, 0x0d,
0x0a, 0x7d
};
unsigned int src_shaders_display_wgsl_len = 599;
unsigned int src_shaders_display_wgsl_len = 518;

View File

@@ -1,11 +1,5 @@
#include "api.h"
#define COMPUTE_VIEWIDX_segments 0
#define COMPUTE_VIEWIDX_shapes 1
#define COMPUTE_VIEWIDX_SDF 2
#define DISPLAY_VIEWIDX_SDF 0
typedef struct display_uniforms {
uint32_t frame;
uint32_t time;
@@ -21,19 +15,10 @@ typedef struct renderer_t {
display_uniforms uniforms;
} renderer_t;
typedef struct compute_uniforms {
uint32_t frame;
uint32_t time;
float pan_x, pan_y;
float zoom, rotation;
} compute_uniforms;
typedef struct compute_t {
sg_pipeline pipeline;
sg_bindings bindings;
compute_uniforms uniforms
sg_pass pass;
} compute_t;
typedef struct userdata_t {
@@ -46,16 +31,18 @@ typedef struct userdata_t {
float zoom, rotation;
} userdata_t;
static void log_fn(const char* tag, uint32_t log_level, uint32_t log_item_id, const char* message_or_null, uint32_t line_nr, const char* filename_or_null, void* user_data)
static void* _malloc(size_t size, void* userdata)
{
if(log_level < 2) return;
fprintf(stderr, "[%s - %s]: (%d) %s \nat %s:%d", tag, log_level == 0 ? "FATAL" : log_level == 1 ? "ERROR" : "WARNING", log_item_id, message_or_null, filename_or_null, line_nr);
(void) userdata;
return emmalloc_malloc(size);
}
static void _free(void* ptr, void* userdata)
{
(void) userdata;
emmalloc_free(ptr);
}
//#define _SG_LOG_ITEMS \
_SG_LOGITEM_XMACRO(COMPUTE_INVALID_BUFFER, "invalid buffer");
static void draw_scene(userdata_t* ud)
static void refresh_SDF(userdata_t* ud)
{
//Get visible tiles
float view_world_w = sapp_widthf() * ud->zoom;
@@ -67,51 +54,112 @@ static void draw_scene(userdata_t* ud)
ud->pan_y + view_world_h * 0.5f,
};
sg_buffer seg_buffer = sg_query_view_buffer(ud->compute.bindings.views[0]);
//_SG_VALIDATE(seg_buffer.id == SG_INVALID_ID, COMPUTE_INVALID_BUFFER);
sg_update_buffer(seg_buffer, &(sg_range) { .ptr = ud->scene.segments, .size = sizeof(segment_t) * ud->scene.num_segments });
bool dirty = false;
if(ud->scene.segment_dirty)
{
pool_view_grow(ud->compute.bindings.views[COMPUTE_VIEWIDX_segments], sizeof(segment_t), ud->scene.shapes.num_segments, ud->scene.shapes.segments);
ud->scene.segment_dirty = false;
dirty = true;
}
sg_buffer shape_buffer = sg_query_view_buffer(ud->compute.bindings.views[1]);
//_SG_VALIDATE(shape_buffer.id == SG_INVALID_ID, COMPUTE_INVALID_BUFFER);
sg_update_buffer(shape_buffer, &(sg_range) { .ptr = ud->scene.shapes, .size = sizeof(shape_meta_t) * ud->scene.num_shapes });
if(ud->scene.shape_dirty)
{
pool_view_grow(ud->compute.bindings.views[COMPUTE_VIEWIDX_shapes], sizeof(shape_meta_t), ud->scene.shapes.num_shapes, ud->scene.shapes.shapes);
ud->scene.shape_dirty = false;
dirty = true;
}
sg_begin_pass(&(sg_pass){ .compute = true, .label = "Compute Pass" });
if(ud->scene.circle_dirty)
{
pool_view_grow(ud->compute.bindings.views[COMPUTE_VIEWIDX_circles], sizeof(circle_t), ud->scene.circles.num_circles, ud->scene.circles.circles);
ud->scene.circle_dirty = false;
dirty = true;
}
if(dirty)
{
uint32_t tile_count = scene_process_tiles(&ud->scene, ud->pan_x, ud->pan_y, ud->zoom, &ud->compute.bindings);
if(tile_count > 0)
{
pool_view_grow(ud->compute.bindings.views[COMPUTE_VIEWIDX_tiles], sizeof(tile_task_t), tile_count, ud->scene.cache.tiles);
pool_view_grow(ud->compute.bindings.views[COMPUTE_VIEWIDX_indices], sizeof(uint32_t), ud->scene.cache.num_indices, ud->scene.cache.indices);
sg_begin_pass(&ud->compute.pass);
sg_apply_pipeline(ud->compute.pipeline);
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniforms));
sg_apply_uniforms(1, &SG_RANGE(ud->scene.LODs));
sg_apply_bindings(&ud->compute.bindings);
sg_dispatch(TILE_SIZE / 8, TILE_SIZE / 8, 1);
sg_dispatch(tile_count * 16, 16, 1);
sg_end_pass();
}
dirty = false;
}
}
static sg_pass pass = (sg_pass){
.label = "Render pass",
};
static simgui_frame_desc_t imgui_frame = {0};
static void compute_mvp(userdata_t *ud)
{
const float w = sapp_widthf();
const float h = sapp_heightf();
const float z = ud->zoom;
const float px = ud->pan_x;
const float py = ud->pan_y;
float* mvp = ud->renderer.uniforms.mvp;
mvp[0] = (2.0f / w) * z;
mvp[1] = 0.0f;
mvp[2] = 0.0f;
mvp[3] = (2.0f / w) * px;
mvp[4] = 0.0f;
mvp[5] = (2.0f / h) * z;
mvp[6] = 0.0f;
mvp[7] = (2.0f / h) * py;
mvp[8] = 0.0f;
mvp[9] = 0.0f;
mvp[10] = 0.0f;
mvp[11] = 0.0f;
mvp[12] = 0.0f;
mvp[13] = 0.0f;
mvp[14] = 0.0f;
mvp[15] = 1.0f;
}
static void frame(void* _userdata)
{
userdata_t* ud = (userdata_t*) _userdata;
ud->renderer.uniforms.frame = ud->compute.uniforms.frame = sapp_frame_count();
ud->renderer.uniforms.time = ud->compute.uniforms.time += sapp_frame_duration_unfiltered();
ud->renderer.uniforms.frame = sapp_frame_count();
ud->renderer.uniforms.time += (uint32_t) ceil(sapp_frame_duration_unfiltered() / 1000.0f);
draw_scene(ud);
refresh_SDF(ud);
sg_begin_pass(&(sg_pass){
.action = ud->renderer.clear_pass,
.swapchain = sglue_swapchain(),
});
pass.swapchain = sglue_swapchain();
sg_begin_pass(&pass);
simgui_new_frame(&(simgui_frame_desc_t){
.width = sapp_width(),
.height = sapp_height(),
.delta_time = sapp_frame_duration_unfiltered(),
.dpi_scale = sapp_dpi_scale(),
});
imgui_frame.width = sapp_width(),
imgui_frame.height = sapp_height(),
imgui_frame.delta_time = sapp_frame_duration_unfiltered(),
imgui_frame.dpi_scale = sapp_dpi_scale(),
simgui_new_frame(&imgui_frame);
igBegin("Framerate", (bool*) true, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
igText("%.0f FPS", 1 / sapp_frame_duration_unfiltered());
igText("%.3fms", sapp_frame_duration_unfiltered() * 1000);
igEnd();
sg_apply_pipeline(ud->renderer.pipeline);
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniforms));
sg_apply_bindings(&ud->renderer.bindings);
sg_draw(0, 6, 1);
simgui_render();
sg_end_pass();
sg_commit();
@@ -124,11 +172,20 @@ static void init(void* _userdata)
sg_setup(&(sg_desc){
.environment = sglue_environment(),
.logger.func = log_fn,
.allocator = {
.alloc_fn = _malloc,
.free_fn = _free,
},
});
simgui_setup(&(simgui_desc_t){
.allocator = {
.alloc_fn = _malloc,
.free_fn = _free,
},
});
simgui_setup(&(simgui_desc_t){0});
ud->scene = (scene_t) {0};
if(!scene_init(&ud->scene, 128, 1024, (vec2) { 8192.0f, 8192.0f })) return;
if(!scene_init(&ud->scene, (vec2) { 8192.0f, 8192.0f })) return;
ud->compute = (compute_t) {
.pipeline = sg_make_pipeline(&(sg_pipeline_desc){
@@ -139,39 +196,50 @@ static void init(void* _userdata)
.entry = "main",
},
.views = {
[0] = {
[COMPUTE_VIEWIDX_segments] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_COMPUTE,
.wgsl_group1_binding_n = 0,
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_segments,
.readonly = true,
}
},
[1] = {
[COMPUTE_VIEWIDX_shapes] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_COMPUTE,
.wgsl_group1_binding_n = 1,
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_shapes,
.readonly = true,
}
},
[2] = {
[COMPUTE_VIEWIDX_circles] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_COMPUTE,
.wgsl_group1_binding_n = 2,
.readonly = false,
}
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_circles,
.readonly = true,
}
},
.uniform_blocks = {
[0] = {
.size = sizeof(compute_uniforms),
[COMPUTE_VIEWIDX_tiles] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_COMPUTE,
.wgsl_group0_binding_n = 0,
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_tiles,
.readonly = true,
}
},
[1] = {
.size = sizeof(tile_ID),
[COMPUTE_VIEWIDX_indices] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_COMPUTE,
.wgsl_group0_binding_n = 1,
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_indices,
.readonly = true,
}
},
[COMPUTE_VIEWIDX_SDF] = {
.storage_image = {
.stage = SG_SHADERSTAGE_COMPUTE,
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_SDF,
.access_format = SG_PIXELFORMAT_R16F,
.image_type = SG_IMAGETYPE_2D,
.writeonly = true,
}
}
},
.label = "SDF Compute Shader",
}),
@@ -187,7 +255,7 @@ static void init(void* _userdata)
.storage_buffer = true,
.stream_update = true,
},
.size = sizeof(segment_t),
.size = sizeof(segment_t) * INITIAL_SEGMENTS_SIZE,
}),
},
}),
@@ -200,25 +268,52 @@ static void init(void* _userdata)
.storage_buffer = true,
.stream_update = true,
},
.size = sizeof(shape_meta_t),
.size = sizeof(shape_meta_t) * INITIAL_SHAPE_SIZE,
}),
},
}),
.views[COMPUTE_VIEWIDX_SDF] = sg_make_view(&(sg_view_desc) {
.label = "SDF tiles view",
.storage_image = {
.image = sg_make_image(&(sg_image_desc) {
.height = TILE_SIZE,
.width = TILE_SIZE,
.label = "SDF tiles texture",
.num_slices = TILE_LAYERS,
.pixel_format = SG_PIXELFORMAT_R32F,
.type = SG_IMAGETYPE_ARRAY,
.usage = { .storage_image = true },
.views[COMPUTE_VIEWIDX_circles] = sg_make_view(&(sg_view_desc) {
.label = "Circles view",
.storage_buffer = {
.buffer = sg_make_buffer(&(sg_buffer_desc) {
.label = "Circles buffer",
.usage = {
.storage_buffer = true,
.stream_update = true,
},
.size = sizeof(circle_t) * INITIAL_CIRCLE_SIZE,
}),
},
}),
}
.views[COMPUTE_VIEWIDX_tiles] = sg_make_view(&(sg_view_desc) {
.label = "Tiles rebuild view",
.storage_buffer = {
.buffer = sg_make_buffer(&(sg_buffer_desc) {
.label = "Tiles rebuild buffer",
.usage = {
.storage_buffer = true,
.stream_update = true,
},
.size = sizeof(tile_task_t) * 256,
}),
},
}),
.views[COMPUTE_VIEWIDX_indices] = sg_make_view(&(sg_view_desc) {
.label = "Culling indices view",
.storage_buffer = {
.buffer = sg_make_buffer(&(sg_buffer_desc) {
.label = "Culling indices buffer",
.usage = {
.storage_buffer = true,
.stream_update = true,
},
.size = sizeof(uint32_t) * INITIAL_INDICES_SIZE,
}),
},
}),
.views[COMPUTE_VIEWIDX_SDF] = ud->scene.texture,
},
.pass = { .compute = true, .label = "Compute Pass" }
};
const vec2 quad[4] = {
@@ -232,7 +327,7 @@ static void init(void* _userdata)
};
ud->renderer = (renderer_t) {
.pipeline = sg_make_pipeline(&(sg_pipeline_desc){
.pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
.shader = sg_make_shader(&(sg_shader_desc) {
.vertex_func = {
.source = (const char*) src_shaders_display_wgsl,
@@ -242,14 +337,18 @@ static void init(void* _userdata)
.source = (const char*) src_shaders_display_wgsl,
.entry = "fs_main",
},
.attrs[0] = {
.base_type = SG_SHADERATTRBASETYPE_FLOAT,
},
.views = {
[DISPLAY_VIEWIDX_SDF] = {
.storage_buffer = {
.texture = {
.image_type = SG_IMAGETYPE_2D,
.stage = SG_SHADERSTAGE_FRAGMENT,
.wgsl_group1_binding_n = 1,
.readonly = true,
}
}
.wgsl_group1_binding_n = DISPLAY_VIEWIDX_SDF,
.sample_type = SG_IMAGESAMPLETYPE_FLOAT,
},
},
},
.uniform_blocks = {
[0] = {
@@ -257,46 +356,67 @@ static void init(void* _userdata)
.stage = SG_SHADERSTAGE_VERTEX,
.wgsl_group0_binding_n = 0,
},
[1] = {
.size = sizeof(tile_ID),
.stage = SG_SHADERSTAGE_VERTEX,
.wgsl_group0_binding_n = 1,
},
},
.label = "Display Shader",
.samplers[DISPLAY_VIEWIDX_Sampler] = {
.sampler_type = SG_SAMPLERTYPE_FILTERING,
.stage = SG_SHADERSTAGE_FRAGMENT,
.wgsl_group1_binding_n = DISPLAY_VIEWIDX_Sampler,
},
.texture_sampler_pairs[0] = {
.sampler_slot = DISPLAY_VIEWIDX_Sampler,
.view_slot = DISPLAY_VIEWIDX_SDF,
.stage = SG_SHADERSTAGE_FRAGMENT,
},
}),
.cull_mode = SG_CULLMODE_FRONT,
.cull_mode = SG_CULLMODE_NONE,
.index_type = SG_INDEXTYPE_UINT16,
.layout.attrs = {
[0].format = SG_VERTEXFORMAT_FLOAT2,
},
.label = "Render pipeline",
}),
.clear_pass = (sg_pass_action) {
.colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR }
},
.bindings = {
.index_buffer = sg_make_buffer(&(sg_buffer_desc) {
.label = "Index buffer",
.usage.index_buffer = true,
.size = sizeof(uint16_t),
.size = sizeof(indices),
.data = SG_RANGE(indices),
}),
.vertex_buffers = sg_make_buffer(&(sg_buffer_desc) {
.vertex_buffers = {
[0] = sg_make_buffer(&(sg_buffer_desc) {
.label = "Vertex buffer",
.usage.vertex_buffer = true,
.size = sizeof(vec2),
.size = sizeof(quad),
.data = SG_RANGE(quad),
}),
})
},
.views = {
[DISPLAY_VIEWIDX_SDF] = ud->compute.bindings.views[COMPUTE_VIEWIDX_SDF]
}
[DISPLAY_VIEWIDX_SDF] = sg_make_view(&(sg_view_desc) {
.texture.image = sg_query_view_image(ud->scene.texture),
.label = "SDF texture view",
}),
},
.samplers = {
[DISPLAY_VIEWIDX_Sampler] = sg_make_sampler(&(sg_sampler_desc) {
.label = "SDF sampler",
}),
}
},
.uniforms = {
.frame = 0,
.time = 0,
.mvp = { 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0 },
},
};
compute_mvp(ud);
pass.action = (sg_pass_action) {
.colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR }
};
add_rectangle(&ud->scene, (vec2) { 200.0f, 100.0f }, (vec2) { 100.0f, 150.0f });
add_circle(&ud->scene, (vec2) { 400.0f, 300.0f }, 125.0f);
scene_add_tile(&ud->scene, (tile_ID) { .lod = 0, .tile = TILE_SIZE, .bounds = { 0.0f, 0.0f, 1024.0f, 1024.0f } });
}
static void cleanup(void* _userdata)
@@ -304,7 +424,7 @@ static void cleanup(void* _userdata)
userdata_t* ud = (userdata_t*) _userdata;
scene_shutdown(&ud->scene);
free(ud);
emmalloc_free(ud);
simgui_shutdown();
sg_shutdown();
@@ -344,7 +464,7 @@ static void event(const sapp_event* event, void* _userdata)
sapp_desc sokol_main(int argc, char* argv[])
{
userdata_t* userdata = (userdata_t*) malloc(sizeof(userdata_t));
userdata_t* userdata = (userdata_t*) emmalloc_malloc(sizeof(userdata_t));
(void)argc;
(void)argv;
@@ -355,6 +475,10 @@ sapp_desc sokol_main(int argc, char* argv[])
.cleanup_userdata_cb = cleanup,
.event_userdata_cb = event,
.window_title = "Sokol",
.allocator = {
.alloc_fn = _malloc,
.free_fn = _free,
},
.logger.func = log_fn,
};
}

36
src/pool.h Normal file
View File

@@ -0,0 +1,36 @@
#include "api.h"
#define MAX_BUFFER_SIZE 1024 * 1024 * 1024 // 1GB
static void pool_buffer_grow(sg_buffer buffer, uint32_t stripe, uint32_t count, void* data)
{
size_t size = sg_query_buffer_size(buffer);
if(size == MAX_BUFFER_SIZE) return sg_update_buffer(buffer, &(sg_range) { data, MAX_BUFFER_SIZE });
if(size < stripe * count)
{
sg_buffer_desc desc = sg_query_buffer_desc(buffer);
while(desc.size < stripe * count) desc.size = fmaxf(MAX_BUFFER_SIZE, size * 2);
sg_uninit_buffer(buffer);
sg_init_buffer(buffer, &desc);
}
sg_update_buffer(buffer, &(sg_range) { data, stripe * count });
}
static void pool_view_grow(sg_view view, uint32_t stripe, uint32_t count, void* data)
{
sg_buffer buffer = sg_query_view_buffer(view);
size_t size = sg_query_buffer_size(buffer);
if(size == MAX_BUFFER_SIZE) return sg_update_buffer(buffer, &(sg_range) { data, MAX_BUFFER_SIZE });
if(size < stripe * count)
{
sg_buffer_desc desc = sg_query_buffer_desc(buffer);
while(desc.size < stripe * count) desc.size = fmaxf(MAX_BUFFER_SIZE, size * 2);
sg_uninit_buffer(buffer);
sg_init_buffer(buffer, &desc);
}
sg_update_buffer(buffer, &(sg_range) { data, stripe * count });
}

View File

@@ -1,4 +1,3 @@
// ---------- Bindings (exactly as you defined) ----------
struct Segment {
p0: vec2f,
p1: vec2f,
@@ -6,30 +5,31 @@ struct Segment {
p3: vec2f,
}
struct ShapeMeta {
start_segment: u32,
start_segment: u32, //Offset in the segments buffer
segment_count: u32,
bbox_min: vec2f,
bbox_max: vec2f,
}
struct Circle {
center: vec2f,
radius: f32,
}
struct Tile {
world_min: vec2f,
world_max: vec2f,
offset: u32,
circle_count: u32,
count: u32,
};
@group(1) @binding(0) var<storage> segments: array<Segment>;
@group(1) @binding(1) var<storage> shapes: array<ShapeMeta>;
@group(1) @binding(2) var<storage, read_write> sdf_buffer: array<f32>;
struct Uniforms {
pan: vec2f,
zoom: f32,
};
struct TileParams {
lod: u32,
tile_size: f32,
world_min: vec2f,
world_max: vec2f,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<uniform> tile_params: TileParams;
@group(1) @binding(2) var<storage> circles: array<Circle>;
@group(1) @binding(3) var<storage> tiles: array<Tile>;
@group(1) @binding(4) var<storage> indices: array<u32>;
@group(1) @binding(5) var sdf_buffer: texture_storage_2d<r16float, write>;
override TILE_SIZE: u32 = 256u;
// ---------- Cubic Bézier helpers ----------
fn eval_cubic(t: f32, p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f {
@@ -174,31 +174,48 @@ fn ray_intersections_cubic(p: vec2f, seg: Segment) -> i32 {
// ---------- Main compute shader ----------
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let tile_dim = vec2f(tile_params.tile_size);
if id.x >= u32(tile_dim.x) || id.y >= u32(tile_dim.y) {
fn main(@builtin(workgroup_id) wg: vec3<u32>, // each workgroup is a tile
@builtin(local_invocation_id) local: vec3<u32>) // each local invocation is a pixel
{
let tile_idx = wg.x / 16u;
let tile = tiles[tile_idx];
let px = (wg.x % 16u) * 8u + local.x;
let py = wg.y * 8u + local.y;
if (px >= TILE_SIZE || py >= TILE_SIZE) {
return;
}
// Pixel center UV, then map to world space
let uv = (vec2f(id.xy) + 0.5) / tile_dim;
let world = mix(tile_params.world_min, tile_params.world_max, uv);
let uv = (vec2f(f32(px), f32(py)) + 0.5) / f32(TILE_SIZE);
let world = mix(tile.world_min, tile.world_max, uv);
var minDist: f32 = 1e20;
var sdf: f32 = 1e20;
var winding: i32 = 0;
for (var s = 0u; s < arrayLength(&shapes); s++) {
let shape = shapes[s];
for (var s = 0u; s < tile.count; s++) {
// Use a pre-culled shape range per tile to reduce the per-pixel processing time
let index = indices[tile.offset + s];
// Quick bbox culling in world space
let bbox_min = shape.bbox_min;
// If the index is lower than the circle count, it mean we are processing circles
if(index < tile.circle_count)
{
let c = circles[index];
sdf = min(sdf, length(world - c.center) - c.radius);
}
else
{
let shape = shapes[index];
/*let bbox_min = shape.bbox_min;
let bbox_max = shape.bbox_max;
let dx = max(bbox_min.x - world.x, max(0.0, world.x - bbox_max.x));
let dy = max(bbox_min.y - world.y, max(0.0, world.y - bbox_max.y));
let dist_to_box = sqrt(dx * dx + dy * dy);
if dist_to_box >= minDist {
continue; // This shape cannot improve the distance
}
continue;
}*/
// Process all segments of the shape
for (var i = shape.start_segment; i < shape.start_segment + shape.segment_count; i++) {
@@ -208,10 +225,11 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
winding += ray_intersections_cubic(world, seg);
}
}
let sign = select(1.0, -1.0, winding != 0i);
let sdf = sign * minDist;
sdf = min(sdf, sign * minDist);
}
}
sdf_buffer[id.y * u32(tile_params.tile_size) + id.x] = sdf;
textureStore(sdf_buffer, vec2(u32(world.x), u32(world.y)), vec4f(sdf, 0.0, 0.0, 1.0));
}

View File

@@ -1,27 +1,20 @@
struct Uniforms {
pan: vec2f,
zoom: f32,
}
struct TileParams {
lod: u32,
tile_size: f32,
world_min: vec2f,
world_max: vec2f,
time: u32,
frame: u32,
mvp: mat4x4f,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<uniform> tile_params: TileParams;
@vertex fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4f {
const pos = array(
vec2( 0.0, 0.5),
vec2(-0.5, -0.5),
vec2( 0.5, -0.5)
);
@group(1) @binding(0) var sdf : texture_2d<f32>;
@group(1) @binding(1) var sdf_sampler : sampler;
return vec4f(pos[vertex_index], 0, 1);
@vertex fn vs_main(@location(0) position: vec2f) -> @builtin(position) vec4f
{
return vec4f(position, 0.0, 1.0) * uniforms.mvp;
}
@fragment fn fs_main() -> @location(0) vec4f {
return vec4(1, 0, 0, 1);
@fragment fn fs_main(@builtin(position) position: vec4f) -> @location(0) vec4f
{
return textureSample(sdf, sdf_sampler, position.xy);
}

View File

@@ -3,47 +3,59 @@
typedef struct uvec2 { uint32_t x, y; } uvec2;
typedef struct vec2 { float x, y; } vec2;
typedef struct ubox_t {
uint32_t min_x, min_y, max_x, max_y;
} ubox_t;
typedef struct box_t {
float min_x, min_y, max_x, max_y;
} box_t;
#define BOX_INTERSECTS(a, b) !(b.min_x > a.max_x \
|| b.max_x < a.min_x \
|| b.min_y > a.max_y \
|| b.max_y < a.min_y)
#define CIRCLE_INTERSECTS(box, circle) !(circle.center.x - circle.radius > box.max_x \
|| circle.center.x + circle.radius < box.min_x \
|| circle.center.y - circle.radius > box.max_y \
|| circle.center.y + circle.radius < box.min_y)
typedef struct segment_t {
vec2 p0, p1, p2, p3; // cubic Bézier: start, control1, control2, end
} segment_t;
typedef struct shape_meta_t {
int start_segment; // index into global segments array
int segment_count; // number of segments forming this closed loop
float bbox_min_x, bbox_min_y, bbox_max_x, bbox_max_y;
uint32_t start_segment; // index into global segments array
uint32_t segment_count; // number of segments forming this closed loop
box_t aabb;
} shape_meta_t;
#define TILE_SIZE 128 // pixels per tile (same for all LODs)
#define TILE_LAYERS 256 // 16 MB buffer
typedef struct circle_t {
vec2 center;
float radius;
} circle_t;
#define _INTERSECTS(a, b) !(b.min_x > a.max_x \
|| b.max_x < a.min_x \
|| b.min_y > a.max_y \
|| b.max_y < a.min_y)
#define TILE_SIZE 256 // texels per tile
// Tile descriptor (separated from tile_cache_entry to be sent to the GPU)
typedef struct tile_ID {
uint32_t lod;
uvec2 tile;
#define TILE_COUNT(size) (uint32_t)ceilf(size.x / TILE_SIZE) * (uint32_t)ceilf(size.y / TILE_SIZE)
// Cached tile bounds
#define INITIAL_SHAPE_SIZE 256
#define INITIAL_SEGMENTS_SIZE 8192
#define INITIAL_CIRCLE_SIZE 1024
#define INITIAL_INDICES_SIZE 16384
// Tile metadata for targetted updates
typedef struct tile_task_t {
box_t bounds;
} tile_ID;
typedef struct LOD_t {
tile_ID* tiles;
uvec2 dimension; //Tile amount per dimension
vec2 range; //Zoom min-max range
} LOD_t;
uint32_t offset;
uint32_t circle_count;
uint32_t count;
} tile_task_t;
typedef struct scene_t {
vec2 world_size;
struct {
uint32_t num_shapes;
uint32_t max_shapes;
shape_meta_t* shapes;
@@ -51,115 +63,199 @@ typedef struct scene_t {
uint32_t num_segments;
uint32_t max_segments;
segment_t* segments;
} shapes;
//Theorically, the LOD amount and tile per LOD are computable, so it's not relevant to store them.
uint32_t max_LOD;
LOD_t* LODs;
struct {
uint32_t num_circles;
uint32_t max_circles;
circle_t* circles;
} circles;
sg_view texture_cache;
struct {
tile_task_t* tiles; // Array of tiles metadata for the compute buffer
bool* tile_dirty; // Array of tiles state, since this info don't needs to be uploaded to the GPU, it is stored separately
uint32_t max_indices;
uint32_t num_indices;
uint32_t* indices; // Array of shapes/circles indices per tile, pre culled on the CPU side
} cache;
sg_view texture; //Layer count depends on the world size so the texture has to be allocate on the scene side
bool shape_dirty, segment_dirty, circle_dirty;
} scene_t;
typedef int (*tile_callback)(scene_t* s, tile_ID* tile);
// Initialise a scene_t with a given capacity for shapes and segments.
static int scene_init(scene_t* s, int init_shape_cap, int init_seg_cap, vec2 world_size) {
s->num_shapes = 0;
s->max_shapes = init_shape_cap;
s->shapes = (shape_meta_t*) malloc(init_shape_cap * sizeof(shape_meta_t));
static uint32_t scene_init(scene_t* s, vec2 world_size) {
s->shapes.num_shapes = 0;
s->shapes.max_shapes = INITIAL_SHAPE_SIZE;
s->shapes.shapes = (shape_meta_t*) emmalloc_malloc(INITIAL_SHAPE_SIZE * sizeof(shape_meta_t));
s->num_segments = 0;
s->max_segments = init_seg_cap;
s->segments = (segment_t*) malloc(init_seg_cap * sizeof(segment_t));
s->shapes.num_segments = 0;
s->shapes.max_segments = INITIAL_SEGMENTS_SIZE;
s->shapes.segments = (segment_t*) emmalloc_malloc(INITIAL_SEGMENTS_SIZE * sizeof(segment_t));
const uint32_t max_LOD = max(ceilf(log2(world_size.x / TILE_SIZE)), ceilf(log2(world_size.y / TILE_SIZE)));
s->circles.num_circles = 0;
s->circles.max_circles = INITIAL_SEGMENTS_SIZE;
s->circles.circles = (circle_t*) emmalloc_malloc(INITIAL_CIRCLE_SIZE * sizeof(circle_t));
s->max_LOD = max_LOD;
s->LODs = malloc(max_LOD * sizeof(LOD_t));
for(int i = 0; i < max_LOD; ++i)
const uint32_t tile_count_x = (uint32_t)ceilf(world_size.x / TILE_SIZE), tile_count_y = (uint32_t)ceilf(world_size.y / TILE_SIZE);
const uint32_t tile_count = tile_count_x * tile_count_y;
s->cache.tiles = (tile_task_t*) emmalloc_malloc(sizeof(tile_task_t) * tile_count);
s->cache.tile_dirty = (bool*) emmalloc_malloc(sizeof(bool) * tile_count);
uint32_t idx = 0;
for(uint32_t y = 0; y < tile_count_y; y++)
{
const float factor = 1 << i;
s->LODs[i].dimension = (uvec2) { (uint32_t) ceilf(world_size.x * factor / TILE_SIZE), (uint32_t) ceilf(world_size.y * factor / TILE_SIZE) };
const uint32_t tiles_count = s->LODs[i].dimension.x * s->LODs[i].dimension.y;
s->LODs[i].tiles = malloc(sizeof(tile_ID) * tiles_count);
s->LODs[i].range = (vec2) { }; //TODO
uint32_t x = 0; uint32_t y = 0;
for(int j = 0; j < tiles_count; j++)
for(uint32_t x = 0; x < tile_count_x; x++)
{
x++;
if(x >= s->LODs[i].dimension.x) { x = 0; y++; }
s->LODs[i].tiles[j] = (tile_ID) { .bounds = { x * factor, y * factor, (x + 1) * factor, (y + 1) * factor }, .lod = i, .tile = { x, y } };
s->cache.tile_dirty[idx] = true;
idx++;
}
}
s->cache.max_indices = INITIAL_INDICES_SIZE;
s->cache.num_indices = 0;
s->cache.indices = (uint32_t*) emmalloc_malloc(sizeof(uint32_t) * INITIAL_INDICES_SIZE);
s->world_size = world_size;
s->texture = sg_make_view(&(sg_view_desc) {
.label = "SDF tiles view",
.storage_image = {
.image = sg_make_image(&(sg_image_desc) {
.width = (uint32_t) ceilf(world_size.x),
.height = (uint32_t) ceilf(world_size.y),
.label = "SDF tiles texture",
.pixel_format = SG_PIXELFORMAT_R16F,
.type = SG_IMAGETYPE_2D,
.usage = { .storage_image = true },
}),
},
});
if (!s->shapes || !s->segments || !s->LODs) return 0; // allocation failure
if (!s->shapes.shapes || !s->shapes.segments || !s->circles.circles || !s->cache.indices || !s->cache.tiles || !s->cache.tile_dirty) return 0; // allocation failure
return 1;
}
//Viewport bounds in *world* space
static void scene_for_each_tiles(scene_t* s, box_t viewport, tile_callback callback)
#define COORD_TO_INDEX(x, y, scene) y * ceilf(s->world_size.x / TILE_SIZE) + x
static void scene_compute_culling(scene_t* s, tile_task_t* task)
{
const uint32_t lod = 0;
const uint32_t count = s->LODs[lod].dimension.x * s->LODs[lod].dimension.y;
for(uint32_t i = 0; i < count; ++i)
task->count = 0;
for(uint32_t i = 0; i < s->circles.num_circles; i++)
{
tile_ID tile = s->LODs[lod].tiles[i];
if(_INTERSECTS(tile.bounds, viewport))
callback(s, &tile);
if(CIRCLE_INTERSECTS(task->bounds, s->circles.circles[i]))
{
if(s->cache.num_indices >= s->cache.max_indices)
{
s->cache.max_indices *= 2;
s->cache.indices = emmalloc_realloc(s->cache.indices, s->cache.max_indices * sizeof(uint32_t));
}
s->cache.indices[s->cache.num_indices++] = i;
task->count++;
}
}
task->circle_count = task->count;
for(uint32_t i = 0; i < s->shapes.num_shapes; i++)
{
if(BOX_INTERSECTS(task->bounds, s->shapes.shapes[i].aabb))
{
if(s->cache.num_indices >= s->cache.max_indices)
{
s->cache.max_indices *= 2;
s->cache.indices = emmalloc_realloc(s->cache.indices, s->cache.max_indices * sizeof(uint32_t));
}
s->cache.indices[s->cache.num_indices++] = i;
task->count++;
}
}
}
// Append a segment, returns its index. (Reallocs if needed, simplified.)
static int scene_add_segment(scene_t* s, segment_t seg) {
if (s->num_segments >= s->max_segments) {
int new_cap = s->max_segments * 2;
segment_t* tmp = (segment_t*) realloc(s->segments, new_cap * sizeof(segment_t));
if (!tmp) return -1;
s->segments = tmp;
s->max_segments = new_cap;
// Find the proper LOD, the relevant tiles then run the culling process for the dirty tiles and upload data to the GPU for SDF rendering
static uint32_t scene_process_tiles(scene_t* s, float pan_x, float pan_y, float zoom, sg_bindings* bindings)
{
const float width = (float)sapp_width(), height = (float) sapp_height();
const float wpp = fmaxf(s->world_size.y / zoom / height, s->world_size.x / zoom / width); //World point per pixel
float view_w = width * wpp, view_h = height * wpp;
box_t frustum = {
.min_x = ceilf((pan_x - view_w * 0.5) / TILE_SIZE),
.min_y = ceilf((pan_y - view_h * 0.5) / TILE_SIZE),
.max_x = ceilf((pan_x + view_w * 0.5) / TILE_SIZE),
.max_y = ceilf((pan_y + view_h * 0.5) / TILE_SIZE)
};
uint32_t dirty_tile_count = 0, offset = 0;
for(uint32_t y = frustum.min_y; y < frustum.max_y; y++)
{
for(uint32_t x = frustum.min_x; x < frustum.max_x; x++)
{
uint32_t idx = COORD_TO_INDEX(x, y, s);
if(s->cache.tile_dirty[idx])
{
tile_task_t* task = &s->cache.tiles[dirty_tile_count++];
task->bounds.min_x = x * TILE_SIZE;
task->bounds.min_y = y * TILE_SIZE;
task->bounds.max_x = fminf((x + 1) * TILE_SIZE, s->world_size.x);
task->bounds.max_y = fminf((y + 1) * TILE_SIZE, s->world_size.y);
task->offset = offset;
scene_compute_culling(s, task);
offset += task->count;
s->cache.tile_dirty[idx] = false;
}
int idx = s->num_segments++;
s->segments[idx] = seg;
}
}
return dirty_tile_count;
}
// Append a segment, returns its index. (Reallocs if needed, simplified.)
static uint32_t scene_add_segment(scene_t* s, segment_t seg) {
if (s->shapes.num_segments >= s->shapes.max_segments) {
int new_cap = s->shapes.max_segments * 2;
segment_t* tmp = (segment_t*) realloc(s->shapes.segments, new_cap * sizeof(segment_t));
if (!tmp) return -1;
s->shapes.segments = tmp;
s->shapes.max_segments = new_cap;
}
int idx = s->shapes.num_segments++;
s->shapes.segments[idx] = seg;
return idx;
}
// Append a shape (meta data) and return its index.
static int scene_add_shape(scene_t* s, shape_meta_t meta) {
if (s->num_shapes >= s->max_shapes) {
int new_cap = s->max_shapes * 2;
shape_meta_t* tmp = (shape_meta_t*) realloc(s->shapes, new_cap * sizeof(shape_meta_t));
static uint32_t scene_add_shape(scene_t* s, shape_meta_t meta) {
if (s->shapes.num_shapes >= s->shapes.max_shapes) {
int new_cap = s->shapes.max_shapes * 2;
shape_meta_t* tmp = (shape_meta_t*) emmalloc_realloc(s->shapes.shapes, new_cap * sizeof(shape_meta_t));
if (!tmp) return -1;
s->shapes = tmp;
s->max_shapes = new_cap;
s->shapes.shapes = tmp;
s->shapes.max_shapes = new_cap;
}
int idx = s->num_shapes++;
s->shapes[idx] = meta;
int idx = s->shapes.num_shapes++;
s->shapes.shapes[idx] = meta;
return idx;
}
static int scene_shutdown(scene_t* s) {
free(s->segments);
free(s->shapes);
static uint32_t scene_shutdown(scene_t* s) {
emmalloc_free(s->shapes.segments);
emmalloc_free(s->shapes.shapes);
emmalloc_free(s->circles.circles);
for(uint32_t i = 0; i < s->max_LOD; ++i)
free(s->LODs[i].tiles);
emmalloc_free(s->cache.indices);
emmalloc_free(s->cache.tiles);
emmalloc_free(s->cache.tile_dirty);
free(s->LODs);
free(s);
emmalloc_free(s);
return 1;
}
// Compute axis-aligned bounding box for a set of segments (used after adding a shape).
static void compute_bbox(segment_t* segs, int start, int count,
float* minx, float* miny, float* maxx, float* maxy) {
static void compute_bbox(segment_t* segs, uint32_t start, uint32_t count, float* minx, float* miny, float* maxx, float* maxy) {
float mx = 1e30f, my = 1e30f, Mx = -1e30f, My = -1e30f;
for (int i = 0; i < count; i++) {
segment_t s = segs[start + i];
@@ -177,7 +273,7 @@ static void compute_bbox(segment_t* segs, int start, int count,
// Add an axisaligned rectangle centred at `center` with given width and height.
// Returns the shape index, or -1 on error.
int add_rectangle(scene_t* s, vec2 center, vec2 size) {
uint32_t add_rectangle(scene_t* s, vec2 center, vec2 size) {
float hw = size.x * 0.5f, hh = size.y * 0.5f;
vec2 corners[4] = {
{center.x - hw, center.y - hh}, // bottomleft
@@ -205,14 +301,14 @@ int add_rectangle(scene_t* s, vec2 center, vec2 size) {
shape_meta_t meta;
meta.start_segment = start;
meta.segment_count = 4;
compute_bbox(s->segments, start, 4, &meta.bbox_min_x, &meta.bbox_min_y, &meta.bbox_max_x, &meta.bbox_max_y);
meta.aabb = (box_t) { center.x - hw, center.y - hh, center.x + hw, center.y + hh };
return scene_add_shape(s, meta);
}
// Add a circle centred at `center` with given radius.
// approximated by 4 cubic Bézier segments (one per quadrant).
// Returns shape index or -1.
int add_circle(scene_t* s, vec2 center, float radius) {
uint32_t add_circle_as_shape(scene_t* s, vec2 center, float radius) {
const float k = 0.552284749831f; // magic constant for 90° arc (4/3 * (sqrt(2)-1))
float rk = radius * k;
@@ -256,6 +352,12 @@ int add_circle(scene_t* s, vec2 center, float radius) {
shape_meta_t meta;
meta.start_segment = start;
meta.segment_count = 4;
compute_bbox(s->segments, start, 4, &meta.bbox_min_x, &meta.bbox_min_y, &meta.bbox_max_x, &meta.bbox_max_y);
compute_bbox(s->shapes.segments, start, 4, &meta.aabb.min_x, &meta.aabb.min_y, &meta.aabb.max_x, &meta.aabb.max_y);
return scene_add_shape(s, meta);
}
uint32_t add_circle(scene_t* s, vec2 center, float radius) {
s->circles.circles[s->circles.num_circles++] = (circle_t) { center, radius };
return s->circles.num_circles - 1;
}