You've already forked flecs_tests
Compare commits
3 Commits
beea8a0281
...
rework
| Author | SHA1 | Date | |
|---|---|---|---|
| af0a39166c | |||
| 8ea6a0bf3e | |||
| dc708de354 |
87
README.md
87
README.md
@@ -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
|
||||
|
||||
@@ -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 ==="
|
||||
|
||||
7
makefile
7
makefile
@@ -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 \
|
||||
|
||||
57
src/api.h
57
src/api.h
@@ -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
107
src/cache.h
Normal 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
@@ -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;
|
||||
|
||||
346
src/main.c
346
src/main.c
@@ -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" });
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
sg_dispatch(TILE_SIZE / 8, TILE_SIZE / 8, 1);
|
||||
if(dirty)
|
||||
{
|
||||
uint32_t tile_count = scene_process_tiles(&ud->scene, ud->pan_x, ud->pan_y, ud->zoom, &ud->compute.bindings);
|
||||
|
||||
sg_end_pass();
|
||||
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_bindings(&ud->compute.bindings);
|
||||
|
||||
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,40 +196,51 @@ 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,
|
||||
}
|
||||
},
|
||||
[COMPUTE_VIEWIDX_tiles] = {
|
||||
.storage_buffer = {
|
||||
.stage = SG_SHADERSTAGE_COMPUTE,
|
||||
.wgsl_group1_binding_n = COMPUTE_VIEWIDX_tiles,
|
||||
.readonly = true,
|
||||
}
|
||||
},
|
||||
[COMPUTE_VIEWIDX_indices] = {
|
||||
.storage_buffer = {
|
||||
.stage = SG_SHADERSTAGE_COMPUTE,
|
||||
.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,
|
||||
}
|
||||
}
|
||||
},
|
||||
.uniform_blocks = {
|
||||
[0] = {
|
||||
.size = sizeof(compute_uniforms),
|
||||
.stage = SG_SHADERSTAGE_COMPUTE,
|
||||
.wgsl_group0_binding_n = 0,
|
||||
},
|
||||
[1] = {
|
||||
.size = sizeof(tile_ID),
|
||||
.stage = SG_SHADERSTAGE_COMPUTE,
|
||||
.wgsl_group0_binding_n = 1,
|
||||
},
|
||||
},
|
||||
.label = "SDF Compute Shader",
|
||||
}),
|
||||
.label = "SDF Compute Pipeline",
|
||||
@@ -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) {
|
||||
.label = "Vertex buffer",
|
||||
.usage.vertex_buffer = true,
|
||||
.size = sizeof(vec2),
|
||||
.data = SG_RANGE(quad),
|
||||
}),
|
||||
.vertex_buffers = {
|
||||
[0] = sg_make_buffer(&(sg_buffer_desc) {
|
||||
.label = "Vertex buffer",
|
||||
.usage.vertex_buffer = true,
|
||||
.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
36
src/pool.h
Normal 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 });
|
||||
}
|
||||
@@ -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,44 +174,62 @@ 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;
|
||||
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
|
||||
// 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];
|
||||
|
||||
// Process all segments of the shape
|
||||
for (var i = shape.start_segment; i < shape.start_segment + shape.segment_count; i++) {
|
||||
let seg = segments[i];
|
||||
let d = distance_to_cubic(world, seg);
|
||||
minDist = min(minDist, d);
|
||||
/*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;
|
||||
}*/
|
||||
|
||||
winding += ray_intersections_cubic(world, seg);
|
||||
// Process all segments of the shape
|
||||
for (var i = shape.start_segment; i < shape.start_segment + shape.segment_count; i++) {
|
||||
let seg = segments[i];
|
||||
let d = distance_to_cubic(world, seg);
|
||||
minDist = min(minDist, d);
|
||||
|
||||
winding += ray_intersections_cubic(world, seg);
|
||||
}
|
||||
|
||||
let sign = select(1.0, -1.0, winding != 0i);
|
||||
sdf = min(sdf, sign * minDist);
|
||||
}
|
||||
}
|
||||
|
||||
let sign = select(1.0, -1.0, winding != 0i);
|
||||
let 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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
306
src/shape.h
306
src/shape.h
@@ -3,163 +3,259 @@
|
||||
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;
|
||||
vec2 world_size;
|
||||
|
||||
uint32_t num_shapes;
|
||||
uint32_t max_shapes;
|
||||
shape_meta_t* shapes;
|
||||
struct {
|
||||
uint32_t num_shapes;
|
||||
uint32_t max_shapes;
|
||||
shape_meta_t* shapes;
|
||||
|
||||
uint32_t num_segments;
|
||||
uint32_t max_segments;
|
||||
segment_t* segments;
|
||||
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->world_size = world_size;
|
||||
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);
|
||||
|
||||
if (!s->shapes || !s->segments || !s->LODs) return 0; // allocation failure
|
||||
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.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 axis‑aligned 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}, // bottom‑left
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user