comparison infinite_canvas/scene_store.c @ 281:c57149ad216e default tip

Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 22:18:15 -0700
parents
children
comparison
equal deleted inserted replaced
280:49e9e591c9bb 281:c57149ad216e
1 #include "infinite_canvas/scene_store.h"
2
3 #include <ctype.h>
4 #include <math.h>
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8
9 #if defined(_WIN32)
10 #include <windows.h>
11 #elif !defined(PLATFORM_WEB)
12 #include <unistd.h>
13 #endif
14
15 #define CANVAS_SCENE_STORE_VERSION 1
16 #define CANVAS_SCENE_STORE_HEADER_SIZE 48
17 #define CANVAS_SCENE_STORE_ENTITY_SIZE 2240
18 #define CANVAS_SCENE_STORE_MAX_BYTES \
19 (CANVAS_SCENE_STORE_HEADER_SIZE + \
20 CANVAS_MAX_ENTITIES * CANVAS_SCENE_STORE_ENTITY_SIZE)
21
22 static const uint8 CANVAS_SCENE_STORE_MAGIC[8] = {
23 'Z', 'E', 'N', 'B', 'U', 'M', 'A', 'P',
24 };
25
26 _Static_assert(sizeof(Color) == 4, "scene format requires RGBA8 Color");
27 _Static_assert(
28 CANVAS_ENTITY_TEXT_CAPACITY == 2048,
29 "scene format version must change when text capacity changes");
30 _Static_assert(
31 CANVAS_SCENE_STORE_ENTITY_SIZE ==
32 2 * 4 + 4 * 4 + 4 + 128 + 2048 + 3 * 4 + 6 * 4,
33 "scene entity record size is inconsistent");
34
35 typedef struct {
36 uint8 *p_data;
37 size_t size;
38 size_t offset;
39 boolean valid;
40 } Canvas_Scene_Writer;
41
42 typedef struct {
43 const uint8 *p_data;
44 size_t size;
45 size_t offset;
46 boolean valid;
47 } Canvas_Scene_Reader;
48
49 static uint64 Canvas_Scene_Store_FNV(
50 uint64 hash,
51 const uint8 *p_data,
52 size_t size)
53 {
54 for (size_t index = 0; index < size; index++) {
55 hash ^= p_data[index];
56 hash *= 1099511628211ULL;
57 }
58 return hash;
59 }
60
61 static void Canvas_Scene_Store_Write(
62 Canvas_Scene_Writer *p_writer,
63 const void *p_data,
64 size_t size)
65 {
66 if (!p_writer->valid ||
67 p_writer->offset > p_writer->size ||
68 size > p_writer->size - p_writer->offset) {
69 p_writer->valid = FALSE;
70 return;
71 }
72 memcpy(p_writer->p_data + p_writer->offset, p_data, size);
73 p_writer->offset += size;
74 }
75
76 static void Canvas_Scene_Store_Write_U32(
77 Canvas_Scene_Writer *p_writer,
78 uint32 value)
79 {
80 uint8 bytes[4] = {
81 (uint8)(value & 0xff),
82 (uint8)((value >> 8) & 0xff),
83 (uint8)((value >> 16) & 0xff),
84 (uint8)((value >> 24) & 0xff),
85 };
86 Canvas_Scene_Store_Write(p_writer, bytes, sizeof(bytes));
87 }
88
89 static void Canvas_Scene_Store_Write_F32(
90 Canvas_Scene_Writer *p_writer,
91 float value)
92 {
93 uint32 bits = 0;
94 memcpy(&bits, &value, sizeof(bits));
95 Canvas_Scene_Store_Write_U32(p_writer, bits);
96 }
97
98 static void Canvas_Scene_Store_Read(
99 Canvas_Scene_Reader *p_reader,
100 void *p_output,
101 size_t size)
102 {
103 if (!p_reader->valid ||
104 p_reader->offset > p_reader->size ||
105 size > p_reader->size - p_reader->offset) {
106 p_reader->valid = FALSE;
107 return;
108 }
109 memcpy(p_output, p_reader->p_data + p_reader->offset, size);
110 p_reader->offset += size;
111 }
112
113 static uint32 Canvas_Scene_Store_Read_U32(
114 Canvas_Scene_Reader *p_reader)
115 {
116 uint8 bytes[4] = {0};
117 Canvas_Scene_Store_Read(p_reader, bytes, sizeof(bytes));
118 return (uint32)bytes[0] |
119 ((uint32)bytes[1] << 8) |
120 ((uint32)bytes[2] << 16) |
121 ((uint32)bytes[3] << 24);
122 }
123
124 static float Canvas_Scene_Store_Read_F32(
125 Canvas_Scene_Reader *p_reader)
126 {
127 uint32 bits = Canvas_Scene_Store_Read_U32(p_reader);
128 float value = 0.0f;
129 memcpy(&value, &bits, sizeof(value));
130 return value;
131 }
132
133 static uint32 Canvas_Scene_Store_Entity_Flags(
134 const Canvas_Entity *p_entity)
135 {
136 uint32 flags = 0;
137 if (p_entity->active) flags |= 1u << 0;
138 if (p_entity->pinned) flags |= 1u << 1;
139 if (p_entity->text_muted) flags |= 1u << 2;
140 if (p_entity->parked) flags |= 1u << 3;
141 return flags;
142 }
143
144 static void Canvas_Scene_Store_Write_Entity(
145 Canvas_Scene_Writer *p_writer,
146 const Canvas_Entity *p_entity)
147 {
148 Canvas_Scene_Store_Write_U32(p_writer, p_entity->id);
149 Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->type);
150 Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.x);
151 Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.y);
152 Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.x);
153 Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.y);
154 Canvas_Scene_Store_Write(p_writer, &p_entity->color, 4);
155 Canvas_Scene_Store_Write(
156 p_writer, p_entity->label, sizeof(p_entity->label));
157 Canvas_Scene_Store_Write(
158 p_writer, p_entity->text, sizeof(p_entity->text));
159 Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->value);
160 Canvas_Scene_Store_Write_F32(p_writer, p_entity->text_scroll_y);
161 Canvas_Scene_Store_Write_U32(
162 p_writer, Canvas_Scene_Store_Entity_Flags(p_entity));
163 Canvas_Scene_Store_Write_F32(
164 p_writer, p_entity->pinned_screen_position.x);
165 Canvas_Scene_Store_Write_F32(
166 p_writer, p_entity->pinned_screen_position.y);
167 Canvas_Scene_Store_Write_F32(
168 p_writer, p_entity->pinned_screen_size.x);
169 Canvas_Scene_Store_Write_F32(
170 p_writer, p_entity->pinned_screen_size.y);
171 Canvas_Scene_Store_Write_F32(
172 p_writer, p_entity->parked_restore_position.x);
173 Canvas_Scene_Store_Write_F32(
174 p_writer, p_entity->parked_restore_position.y);
175 }
176
177 static Canvas_Entity Canvas_Scene_Store_Read_Entity(
178 Canvas_Scene_Reader *p_reader)
179 {
180 Canvas_Entity entity = {0};
181 entity.id = Canvas_Scene_Store_Read_U32(p_reader);
182 entity.type =
183 (Canvas_Entity_Type)Canvas_Scene_Store_Read_U32(p_reader);
184 entity.position.x = Canvas_Scene_Store_Read_F32(p_reader);
185 entity.position.y = Canvas_Scene_Store_Read_F32(p_reader);
186 entity.size.x = Canvas_Scene_Store_Read_F32(p_reader);
187 entity.size.y = Canvas_Scene_Store_Read_F32(p_reader);
188 Canvas_Scene_Store_Read(p_reader, &entity.color, 4);
189 Canvas_Scene_Store_Read(
190 p_reader, entity.label, sizeof(entity.label));
191 Canvas_Scene_Store_Read(
192 p_reader, entity.text, sizeof(entity.text));
193 entity.label[sizeof(entity.label) - 1] = '\0';
194 entity.text[sizeof(entity.text) - 1] = '\0';
195 entity.value = (int32)Canvas_Scene_Store_Read_U32(p_reader);
196 entity.text_scroll_y = Canvas_Scene_Store_Read_F32(p_reader);
197 uint32 flags = Canvas_Scene_Store_Read_U32(p_reader);
198 entity.active = flags & (1u << 0) ? TRUE : FALSE;
199 entity.pinned = flags & (1u << 1) ? TRUE : FALSE;
200 entity.text_muted = flags & (1u << 2) ? TRUE : FALSE;
201 entity.parked = flags & (1u << 3) ? TRUE : FALSE;
202 entity.pinned_screen_position.x =
203 Canvas_Scene_Store_Read_F32(p_reader);
204 entity.pinned_screen_position.y =
205 Canvas_Scene_Store_Read_F32(p_reader);
206 entity.pinned_screen_size.x =
207 Canvas_Scene_Store_Read_F32(p_reader);
208 entity.pinned_screen_size.y =
209 Canvas_Scene_Store_Read_F32(p_reader);
210 entity.parked_restore_position.x =
211 Canvas_Scene_Store_Read_F32(p_reader);
212 entity.parked_restore_position.y =
213 Canvas_Scene_Store_Read_F32(p_reader);
214 entity.text_cursor = (int32)strlen(entity.text);
215 entity.text_selection_anchor = entity.text_cursor;
216 entity.visibility_amount = 1.0f;
217 entity.screen_space_initialized = entity.pinned;
218 entity.animation_amount =
219 entity.type == CANVAS_ENTITY_CONVERSATION
220 ? (entity.active ? 0.0f : 1.0f)
221 : (entity.active ? 1.0f : 0.0f);
222 return entity;
223 }
224
225 static boolean Canvas_Scene_Store_Entity_Valid(
226 const Canvas_Entity *p_entity)
227 {
228 boolean base_valid = p_entity->id != 0 &&
229 p_entity->type >= 0 &&
230 p_entity->type < CANVAS_ENTITY_TYPE_COUNT &&
231 isfinite(p_entity->position.x) &&
232 isfinite(p_entity->position.y) &&
233 isfinite(p_entity->size.x) &&
234 isfinite(p_entity->size.y) &&
235 p_entity->size.x > 0.0f &&
236 p_entity->size.y > 0.0f &&
237 fabsf(p_entity->position.x) <= 1000000000.0f &&
238 fabsf(p_entity->position.y) <= 1000000000.0f &&
239 p_entity->size.x <= 10000000.0f &&
240 p_entity->size.y <= 10000000.0f;
241 if (!base_valid) return FALSE;
242 if (p_entity->pinned &&
243 (!isfinite(p_entity->pinned_screen_position.x) ||
244 !isfinite(p_entity->pinned_screen_position.y) ||
245 !isfinite(p_entity->pinned_screen_size.x) ||
246 !isfinite(p_entity->pinned_screen_size.y) ||
247 p_entity->pinned_screen_size.x <= 0.0f ||
248 p_entity->pinned_screen_size.y <= 0.0f)) {
249 return FALSE;
250 }
251 return !p_entity->parked ||
252 (isfinite(p_entity->parked_restore_position.x) &&
253 isfinite(p_entity->parked_restore_position.y));
254 }
255
256 boolean Canvas_Scene_Store_Path(
257 const char *p_name,
258 char *p_path,
259 size_t path_capacity)
260 {
261 #if defined(PLATFORM_WEB)
262 (void)p_name;
263 (void)p_path;
264 (void)path_capacity;
265 return FALSE;
266 #else
267 if (!p_name || !p_name[0] || !p_path || path_capacity == 0) {
268 return FALSE;
269 }
270 size_t name_length = strlen(p_name);
271 if (name_length >= CANVAS_SCENE_STORE_NAME_CAPACITY) return FALSE;
272 for (size_t index = 0; index < name_length; index++) {
273 uint8 character = (uint8)p_name[index];
274 if (!isalnum(character) &&
275 character != '-' &&
276 character != '_') {
277 return FALSE;
278 }
279 }
280 const char *p_root = getenv("XDG_STATE_HOME");
281 const char *p_suffix = "/zenbu/infinite-canvas/snapshots";
282 char fallback[CANVAS_SCENE_STORE_PATH_CAPACITY];
283 if (!p_root || !p_root[0]) {
284 const char *p_home = getenv("HOME");
285 if (!p_home || !p_home[0]) return FALSE;
286 int fallback_written = snprintf(
287 fallback,
288 sizeof(fallback),
289 "%s/.local/state",
290 p_home);
291 if (fallback_written <= 0 ||
292 (size_t)fallback_written >= sizeof(fallback)) {
293 return FALSE;
294 }
295 p_root = fallback;
296 }
297 int written = snprintf(
298 p_path,
299 path_capacity,
300 "%s%s/%s.zmap",
301 p_root,
302 p_suffix,
303 p_name);
304 return written > 0 && (size_t)written < path_capacity;
305 #endif
306 }
307
308 boolean Canvas_Scene_Store_Save(
309 const char *p_path,
310 const Canvas_Scene *p_scene,
311 const Canvas_Camera *p_camera)
312 {
313 #if defined(PLATFORM_WEB)
314 (void)p_path;
315 (void)p_scene;
316 (void)p_camera;
317 return FALSE;
318 #else
319 if (!p_path || !p_scene || !p_camera) return FALSE;
320 size_t entity_count = 0;
321 for (size_t index = 0;
322 index < Dowa_Array_Length(p_scene->p_entities);
323 index++) {
324 if (!p_scene->p_entities[index].removing) entity_count++;
325 }
326 if (entity_count > CANVAS_MAX_ENTITIES) return FALSE;
327 size_t size = CANVAS_SCENE_STORE_HEADER_SIZE +
328 entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE;
329 uint8 *p_data = (uint8 *)calloc(size, 1);
330 if (!p_data) return FALSE;
331 Canvas_Scene_Writer writer = {
332 .p_data = p_data,
333 .size = size,
334 .valid = TRUE,
335 };
336 Canvas_Scene_Store_Write(
337 &writer, CANVAS_SCENE_STORE_MAGIC, sizeof(CANVAS_SCENE_STORE_MAGIC));
338 Canvas_Scene_Store_Write_U32(&writer, CANVAS_SCENE_STORE_VERSION);
339 Canvas_Scene_Store_Write_U32(&writer, (uint32)entity_count);
340 Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity);
341 Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity_id);
342 Canvas_Scene_Store_Write_U32(
343 &writer, p_scene->show_grid ? 1u : 0u);
344 Canvas_Scene_Store_Write_F32(&writer, p_camera->target.x);
345 Canvas_Scene_Store_Write_F32(&writer, p_camera->target.y);
346 Canvas_Scene_Store_Write_F32(&writer, p_camera->zoom);
347 Canvas_Scene_Store_Write_U32(
348 &writer, (uint32)(size - CANVAS_SCENE_STORE_HEADER_SIZE));
349 Canvas_Scene_Store_Write_U32(&writer, 0);
350 for (size_t index = 0;
351 index < Dowa_Array_Length(p_scene->p_entities);
352 index++) {
353 if (!p_scene->p_entities[index].removing) {
354 Canvas_Scene_Store_Write_Entity(
355 &writer, &p_scene->p_entities[index]);
356 }
357 }
358 if (!writer.valid || writer.offset != size) {
359 Dowa_Free(p_data);
360 return FALSE;
361 }
362 uint64 checksum_hash = Canvas_Scene_Store_FNV(
363 1469598103934665603ULL,
364 p_data,
365 44);
366 checksum_hash = Canvas_Scene_Store_FNV(
367 checksum_hash,
368 p_data + CANVAS_SCENE_STORE_HEADER_SIZE,
369 size - CANVAS_SCENE_STORE_HEADER_SIZE);
370 uint32 checksum = (uint32)checksum_hash;
371 p_data[44] = (uint8)(checksum & 0xff);
372 p_data[45] = (uint8)((checksum >> 8) & 0xff);
373 p_data[46] = (uint8)((checksum >> 16) & 0xff);
374 p_data[47] = (uint8)((checksum >> 24) & 0xff);
375
376 char directory[CANVAS_SCENE_STORE_PATH_CAPACITY];
377 char temporary[CANVAS_SCENE_STORE_PATH_CAPACITY];
378 snprintf(directory, sizeof(directory), "%s", p_path);
379 char *p_separator = strrchr(directory, '/');
380 #if defined(_WIN32)
381 char *p_windows_separator = strrchr(directory, '\\');
382 if (!p_separator || p_windows_separator > p_separator) {
383 p_separator = p_windows_separator;
384 }
385 #endif
386 if (!p_separator) {
387 Dowa_Free(p_data);
388 return FALSE;
389 }
390 *p_separator = '\0';
391 if (MakeDirectory(directory) != 0 && !DirectoryExists(directory)) {
392 Dowa_Free(p_data);
393 return FALSE;
394 }
395 int temporary_written = snprintf(
396 temporary, sizeof(temporary), "%s.tmp", p_path);
397 if (temporary_written <= 0 ||
398 (size_t)temporary_written >= sizeof(temporary)) {
399 Dowa_Free(p_data);
400 return FALSE;
401 }
402 FILE *p_file = fopen(temporary, "wb");
403 if (!p_file) {
404 Dowa_Free(p_data);
405 return FALSE;
406 }
407 boolean success =
408 fwrite(p_data, 1, size, p_file) == size &&
409 fflush(p_file) == 0;
410 #if !defined(_WIN32)
411 if (success) success = fsync(fileno(p_file)) == 0;
412 #endif
413 if (fclose(p_file) != 0) success = FALSE;
414 Dowa_Free(p_data);
415 if (!success) {
416 remove(temporary);
417 return FALSE;
418 }
419 #if defined(_WIN32)
420 success = MoveFileExA(
421 temporary,
422 p_path,
423 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)
424 ? TRUE
425 : FALSE;
426 #else
427 success = rename(temporary, p_path) == 0 ? TRUE : FALSE;
428 #endif
429 if (!success) remove(temporary);
430 return success;
431 #endif
432 }
433
434 boolean Canvas_Scene_Store_Load(
435 const char *p_path,
436 Canvas_Scene *p_scene,
437 Canvas_Camera *p_camera)
438 {
439 #if defined(PLATFORM_WEB)
440 (void)p_path;
441 (void)p_scene;
442 (void)p_camera;
443 return FALSE;
444 #else
445 if (!p_path || !p_scene || !p_camera) return FALSE;
446 FILE *p_file = fopen(p_path, "rb");
447 if (!p_file) return FALSE;
448 if (fseek(p_file, 0, SEEK_END) != 0) {
449 fclose(p_file);
450 return FALSE;
451 }
452 long file_size = ftell(p_file);
453 if (file_size < CANVAS_SCENE_STORE_HEADER_SIZE ||
454 file_size > CANVAS_SCENE_STORE_MAX_BYTES ||
455 fseek(p_file, 0, SEEK_SET) != 0) {
456 fclose(p_file);
457 return FALSE;
458 }
459 uint8 *p_data = (uint8 *)malloc((size_t)file_size);
460 if (!p_data) {
461 fclose(p_file);
462 return FALSE;
463 }
464 boolean success =
465 fread(p_data, 1, (size_t)file_size, p_file) == (size_t)file_size;
466 fclose(p_file);
467 if (!success) {
468 Dowa_Free(p_data);
469 return FALSE;
470 }
471
472 Canvas_Scene_Reader reader = {
473 .p_data = p_data,
474 .size = (size_t)file_size,
475 .valid = TRUE,
476 };
477 uint8 magic[8] = {0};
478 Canvas_Scene_Store_Read(&reader, magic, sizeof(magic));
479 uint32 version = Canvas_Scene_Store_Read_U32(&reader);
480 uint32 entity_count = Canvas_Scene_Store_Read_U32(&reader);
481 uint32 next_entity = Canvas_Scene_Store_Read_U32(&reader);
482 uint32 next_entity_id = Canvas_Scene_Store_Read_U32(&reader);
483 uint32 scene_flags = Canvas_Scene_Store_Read_U32(&reader);
484 float camera_x = Canvas_Scene_Store_Read_F32(&reader);
485 float camera_y = Canvas_Scene_Store_Read_F32(&reader);
486 float camera_zoom = Canvas_Scene_Store_Read_F32(&reader);
487 uint32 payload_size = Canvas_Scene_Store_Read_U32(&reader);
488 uint32 stored_checksum = Canvas_Scene_Store_Read_U32(&reader);
489 size_t expected_size = CANVAS_SCENE_STORE_HEADER_SIZE +
490 (size_t)entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE;
491 uint64 checksum_hash = Canvas_Scene_Store_FNV(
492 1469598103934665603ULL,
493 p_data,
494 44);
495 checksum_hash = Canvas_Scene_Store_FNV(
496 checksum_hash,
497 p_data + CANVAS_SCENE_STORE_HEADER_SIZE,
498 (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE);
499 uint32 actual_checksum = (uint32)checksum_hash;
500 if (!reader.valid ||
501 memcmp(magic, CANVAS_SCENE_STORE_MAGIC, sizeof(magic)) != 0 ||
502 version != CANVAS_SCENE_STORE_VERSION ||
503 entity_count > CANVAS_MAX_ENTITIES ||
504 expected_size != (size_t)file_size ||
505 payload_size != (uint32)(
506 (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE) ||
507 stored_checksum != actual_checksum ||
508 next_entity == 0xffffffffu ||
509 next_entity_id == 0xffffffffu ||
510 !isfinite(camera_x) ||
511 !isfinite(camera_y) ||
512 !isfinite(camera_zoom) ||
513 fabsf(camera_x) > 1000000000.0f ||
514 fabsf(camera_y) > 1000000000.0f ||
515 camera_zoom < p_camera->min_zoom ||
516 camera_zoom > p_camera->max_zoom) {
517 Dowa_Free(p_data);
518 return FALSE;
519 }
520
521 Canvas_Entity *p_entities = (Canvas_Entity *)calloc(
522 entity_count ? entity_count : 1,
523 sizeof(*p_entities));
524 if (!p_entities) {
525 Dowa_Free(p_data);
526 return FALSE;
527 }
528 uint32 max_id = 0;
529 for (uint32 index = 0; index < entity_count; index++) {
530 p_entities[index] = Canvas_Scene_Store_Read_Entity(&reader);
531 if (!reader.valid ||
532 !Canvas_Scene_Store_Entity_Valid(&p_entities[index])) {
533 Dowa_Free(p_entities);
534 Dowa_Free(p_data);
535 return FALSE;
536 }
537 for (uint32 previous = 0; previous < index; previous++) {
538 if (p_entities[previous].id == p_entities[index].id) {
539 Dowa_Free(p_entities);
540 Dowa_Free(p_data);
541 return FALSE;
542 }
543 }
544 if (p_entities[index].id > max_id) {
545 max_id = p_entities[index].id;
546 }
547 }
548 Dowa_Free(p_data);
549 if (!reader.valid || reader.offset != (size_t)file_size) {
550 Dowa_Free(p_entities);
551 return FALSE;
552 }
553
554 Canvas_Scene_Clear(p_scene);
555 for (uint32 index = 0; index < entity_count; index++) {
556 Dowa_Array_Push_Arena(
557 p_scene->p_entities, p_entities[index], p_scene->p_arena);
558 }
559 Dowa_Free(p_entities);
560 p_scene->next_entity =
561 next_entity < entity_count ? entity_count : next_entity;
562 p_scene->next_entity_id =
563 next_entity_id < max_id ? max_id : next_entity_id;
564 p_scene->show_grid = scene_flags & 1u ? TRUE : FALSE;
565 p_camera->target = (Vector2){camera_x, camera_y};
566 p_camera->zoom = camera_zoom;
567 return TRUE;
568 #endif
569 }