diff --git a/src/main.c b/src/main.c index d8650f5..50fb8ee 100644 --- a/src/main.c +++ b/src/main.c @@ -1,25 +1,49 @@ #include "api.h" -typedef struct uniform_t { - float x, y; - float zoom; -} uniform_t; +#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; + + float mvp[16]; +} display_uniforms; typedef struct renderer_t { sg_pipeline pipeline; sg_pass_action clear_pass; - uniform_t uniforms; + sg_bindings bindings; + + 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 } compute_t; typedef struct userdata_t { scene_t scene; renderer_t renderer; compute_t compute; + + //Viewport related values + float pan_x, pan_y; + 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) @@ -33,34 +57,42 @@ static void log_fn(const char* tag, uint32_t log_level, uint32_t log_item_id, co static void draw_scene(userdata_t* ud) { - if(ud->scene.dirty) - { - 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 }); + //Get visible tiles + float view_world_w = sapp_widthf() * ud->zoom; + float view_world_h = sapp_heightf() * ud->zoom; + box_t viewport = (box_t) { + ud->pan_x - view_world_w * 0.5f, + ud->pan_y - view_world_h * 0.5f, + ud->pan_x + view_world_w * 0.5f, + ud->pan_y + view_world_h * 0.5f, + }; - 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 }); + 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 }); - 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.tiles[0])); - sg_apply_bindings(&ud->compute.bindings); + 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 }); - sg_dispatch(TILE_SIZE / 8, TILE_SIZE / 8, 1); + 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); - sg_end_pass(); + sg_dispatch(TILE_SIZE / 8, TILE_SIZE / 8, 1); - ud->scene.dirty = false; - } + sg_end_pass(); } 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(); + draw_scene(ud); sg_begin_pass(&(sg_pass){ @@ -71,13 +103,13 @@ static void frame(void* _userdata) simgui_new_frame(&(simgui_frame_desc_t){ .width = sapp_width(), .height = sapp_height(), - .delta_time = sapp_frame_duration(), + .delta_time = sapp_frame_duration_unfiltered(), .dpi_scale = sapp_dpi_scale(), }); igBegin("Framerate", (bool*) true, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); - igText("%.0f FPS", 1 / sapp_frame_duration()); - igText("%.3fms", sapp_frame_duration() * 1000); + igText("%.0f FPS", 1 / sapp_frame_duration_unfiltered()); + igText("%.3fms", sapp_frame_duration_unfiltered() * 1000); igEnd(); simgui_render(); @@ -96,7 +128,7 @@ static void init(void* _userdata) simgui_setup(&(simgui_desc_t){0}); ud->scene = (scene_t) {0}; - if(!scene_init(&ud->scene, 128, 1024)) return; + if(!scene_init(&ud->scene, 128, 1024, (vec2) { 8192.0f, 8192.0f })) return; ud->compute = (compute_t) { .pipeline = sg_make_pipeline(&(sg_pipeline_desc){ @@ -131,7 +163,7 @@ static void init(void* _userdata) }, .uniform_blocks = { [0] = { - .size = sizeof(uniform_t), + .size = sizeof(compute_uniforms), .stage = SG_SHADERSTAGE_COMPUTE, .wgsl_group0_binding_n = 0, }, @@ -146,7 +178,7 @@ static void init(void* _userdata) .label = "SDF Compute Pipeline", }), .bindings = (sg_bindings) { - .views[0] = sg_make_view(&(sg_view_desc) { + .views[COMPUTE_VIEWIDX_segments] = sg_make_view(&(sg_view_desc) { .label = "Segments view", .storage_buffer = { .buffer = sg_make_buffer(&(sg_buffer_desc) { @@ -159,7 +191,7 @@ static void init(void* _userdata) }), }, }), - .views[1] = sg_make_view(&(sg_view_desc) { + .views[COMPUTE_VIEWIDX_shapes] = sg_make_view(&(sg_view_desc) { .label = "Shapes metadata view", .storage_buffer = { .buffer = sg_make_buffer(&(sg_buffer_desc) { @@ -172,19 +204,33 @@ static void init(void* _userdata) }), }, }), - .views[2] = sg_make_view(&(sg_view_desc) { - .label = "SDF field view", - .storage_buffer = { - .buffer = sg_make_buffer(&(sg_buffer_desc) { - .label = "SDF field buffer", - .size = sizeof(float), - .usage.storage_buffer = true, + .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 }, }), }, }), } }; + const vec2 quad[4] = { + { 0.0f, 1.0f }, // bottom left + { 1.0f, 1.0f }, // bottom right + { 1.0f, 0.0f }, // top right + { 0.0f, 0.0f }, // top left + }; + const uint16_t indices[] = { + 0, 1, 2, 0, 2, 3, + }; + ud->renderer = (renderer_t) { .pipeline = sg_make_pipeline(&(sg_pipeline_desc){ .shader = sg_make_shader(&(sg_shader_desc) { @@ -197,7 +243,7 @@ static void init(void* _userdata) .entry = "fs_main", }, .views = { - [0] = { + [DISPLAY_VIEWIDX_SDF] = { .storage_buffer = { .stage = SG_SHADERSTAGE_FRAGMENT, .wgsl_group1_binding_n = 1, @@ -207,7 +253,7 @@ static void init(void* _userdata) }, .uniform_blocks = { [0] = { - .size = sizeof(uniform_t), + .size = sizeof(display_uniforms), .stage = SG_SHADERSTAGE_VERTEX, .wgsl_group0_binding_n = 0, }, @@ -219,16 +265,38 @@ static void init(void* _userdata) }, .label = "Display Shader", }), + .cull_mode = SG_CULLMODE_FRONT, + .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), + .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), + }), + .views = { + [DISPLAY_VIEWIDX_SDF] = ud->compute.bindings.views[COMPUTE_VIEWIDX_SDF] + } } }; 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_cache_entry) { .id = { .lod = 0, .tile = TILE_SIZE, .world_min = { 0.0f, 0.0f }, .world_max = { 1024.0f, 1024.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) diff --git a/src/shape.h b/src/shape.h index 5c2a88d..3685f6d 100644 --- a/src/shape.h +++ b/src/shape.h @@ -3,6 +3,10 @@ typedef struct uvec2 { uint32_t x, y; } uvec2; typedef struct vec2 { float x, y; } vec2; +typedef struct box_t { + float min_x, min_y, max_x, max_y; +} box_t; + typedef struct segment_t { vec2 p0, p1, p2, p3; // cubic Bézier: start, control1, control2, end } segment_t; @@ -14,26 +18,32 @@ typedef struct shape_meta_t { } shape_meta_t; #define TILE_SIZE 128 // pixels per tile (same for all LODs) +#define TILE_LAYERS 256 // 16 MB buffer -// Tile descriptor +#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) + +// Tile descriptor (separated from tile_cache_entry to be sent to the GPU) typedef struct tile_ID { - uint32_t lod; // LOD level (0 = coarsest) - uvec2 tile; // position in the grid at that LOD + uint32_t lod; + uvec2 tile; - // world-space bounds (derived from lod, tile_x, tile_y) - vec2 world_min; - vec2 world_max; + // Cached tile bounds + box_t bounds; } tile_ID; -// Tile cache entry (CPU side) -typedef struct tile_cache_entry { - tile_ID id; - // GPU texture holding the SDF values (R32Float) - sg_buffer SDF; //TODO - bool ready; -} tile_cache_entry; +typedef struct LOD_t { + tile_ID* tiles; + + uvec2 dimension; //Tile amount per dimension + vec2 range; //Zoom min-max range +} LOD_t; typedef struct scene_t { + vec2 world_size; + uint32_t num_shapes; uint32_t max_shapes; shape_meta_t* shapes; @@ -42,15 +52,17 @@ typedef struct scene_t { uint32_t max_segments; segment_t* segments; - uint32_t num_tiles; - uint32_t max_tiles; - tile_cache_entry* tiles; + //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; - bool dirty; + sg_view texture_cache; } 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) { +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)); @@ -59,16 +71,49 @@ static int scene_init(scene_t* s, int init_shape_cap, int init_seg_cap) { s->max_segments = init_seg_cap; s->segments = (segment_t*) malloc(init_seg_cap * sizeof(segment_t)); - s->num_tiles = 0; - s->max_tiles = 8; - s->tiles = (tile_cache_entry*) malloc(8 * sizeof(tile_cache_entry)); + const uint32_t max_LOD = max(ceilf(log2(world_size.x / TILE_SIZE)), ceilf(log2(world_size.y / TILE_SIZE))); - s->dirty = true; + s->max_LOD = max_LOD; + s->LODs = malloc(max_LOD * sizeof(LOD_t)); + for(int i = 0; i < max_LOD; ++i) + { + 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) }; - if (!s->shapes || !s->segments || !s->tiles) return 0; // allocation failure + 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++) + { + 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->world_size = world_size; + + if (!s->shapes || !s->segments || !s->LODs) 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) +{ + 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) + { + tile_ID tile = s->LODs[lod].tiles[i]; + if(_INTERSECTS(tile.bounds, viewport)) + callback(s, &tile); + } +} + // 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) { @@ -81,7 +126,6 @@ static int scene_add_segment(scene_t* s, segment_t seg) { int idx = s->num_segments++; s->segments[idx] = seg; - s->dirty = true; return idx; } @@ -97,30 +141,17 @@ static int scene_add_shape(scene_t* s, shape_meta_t meta) { int idx = s->num_shapes++; s->shapes[idx] = meta; - s->dirty = true; - return idx; -} - -static int scene_add_tile(scene_t* s, tile_cache_entry tile) -{ - if (s->num_tiles >= s->max_tiles) { - int new_cap = s->max_tiles * 2; - tile_cache_entry* tmp = (tile_cache_entry*) realloc(s->tiles, new_cap * sizeof(tile_cache_entry)); - if (!tmp) return -1; - s->tiles = tmp; - s->max_tiles = new_cap; - } - int idx = s->num_tiles++; - s->tiles[idx] = tile; - - s->dirty = true; return idx; } static int scene_shutdown(scene_t* s) { free(s->segments); free(s->shapes); - free(s->tiles); + + for(uint32_t i = 0; i < s->max_LOD; ++i) + free(s->LODs[i].tiles); + + free(s->LODs); free(s); return 1;