diff --git a/include/audio.h b/include/audio.h index 6382682..254667a 100644 --- a/include/audio.h +++ b/include/audio.h @@ -1,80 +1,80 @@ -// audio.h -#pragma once -#ifndef AUDIO_H -#define AUDIO_H - -#ifdef __cplusplus -#include -extern "C" { -#else -#include -#include -#endif - -typedef struct audio_t audio_t; -typedef uint32_t audio_stream_id_t; - -// If the return type is bool, false means failure. - -// Initialize audio system -// nullptr means failure -audio_t* audio_init(void); - -// Destroy audio system and free resources -void audio_destroy(audio_t* audio); - -// Load MADPCM data from a WAV file. -// Returns a pointer to the allocated audio data, or nullptr on failure. -// The returned buffer must be freed with audio_free_data(). -uint8_t* audio_load_madpcm(const char* filename, int* out_size, int* out_channels, int* out_rate, int* out_total_samples); - -// Free data allocated by audio_load_madpcm -void audio_free_data(uint8_t* data); - -// Play a MADPCM stream, returns unique ID -// data must remain valid until the stream ends or is stopped -// loop: if true, the stream restarts on EOF -// 0 means failure, valid IDs start from 1 -audio_stream_id_t audio_play(audio_t* audio, - const uint8_t* data, - int size, - int channels, - int rate, - bool loop); - -// Stop and remove stream by ID -bool audio_stop(audio_t* audio, audio_stream_id_t id); - -bool audio_pause(audio_t* audio, audio_stream_id_t id); -bool audio_resume(audio_t* audio, audio_stream_id_t id); - -// Returns true if the stream is active (playing or paused) -bool audio_is_active(audio_t* audio, audio_stream_id_t id); - -// Returns true if the stream is currently playing (not paused, not stopped) -bool audio_is_playing(audio_t* audio, audio_stream_id_t id); - -// Per-stream volume, immediate effect, range [0.0, 1.0] -bool audio_set_volume(audio_t* audio, audio_stream_id_t id, float volume); - -// Animate per-stream volume linearly over time, driven by audio_tick() -bool audio_animate_volume(audio_t* audio, audio_stream_id_t id, - float to_volume, float duration_sec); - -// Global volume, immediate effect, range [0.0, 1.0] -// Applied on top of per-stream volume at mix time -void audio_set_global_volume(audio_t* audio, float volume); - -// Animate global volume linearly over time, driven by audio_tick() -void audio_animate_global_volume(audio_t* audio, - float to_volume, float duration_sec); - -// Advances stream decoding and volume animations -// Call once per main loop iteration with the same delta as compositor_tick() -void audio_tick(audio_t* audio, float delta_time_sec); - -#ifdef __cplusplus -} -#endif - +// audio.h +#pragma once +#ifndef AUDIO_H +#define AUDIO_H + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#include +#endif + +typedef struct audio_t audio_t; +typedef uint32_t audio_stream_id_t; + +// If the return type is bool, false means failure. + +// Initialize audio system +// nullptr means failure +audio_t* audio_init(void); + +// Destroy audio system and free resources +void audio_destroy(audio_t* audio); + +// Load MADPCM data from a WAV file. +// Returns a pointer to the allocated audio data, or nullptr on failure. +// The returned buffer must be freed with audio_free_data(). +uint8_t* audio_load_madpcm(const char* filename, int* out_size, int* out_channels, int* out_rate, int* out_total_samples); + +// Free data allocated by audio_load_madpcm +void audio_free_data(uint8_t* data); + +// Play a MADPCM stream, returns unique ID +// data must remain valid until the stream ends or is stopped +// loop: if true, the stream restarts on EOF +// 0 means failure, valid IDs start from 1 +audio_stream_id_t audio_play(audio_t* audio, + const uint8_t* data, + int size, + int channels, + int rate, + bool loop); + +// Stop and remove stream by ID +bool audio_stop(audio_t* audio, audio_stream_id_t id); + +bool audio_pause(audio_t* audio, audio_stream_id_t id); +bool audio_resume(audio_t* audio, audio_stream_id_t id); + +// Returns true if the stream is active (playing or paused) +bool audio_is_active(audio_t* audio, audio_stream_id_t id); + +// Returns true if the stream is currently playing (not paused, not stopped) +bool audio_is_playing(audio_t* audio, audio_stream_id_t id); + +// Per-stream volume, immediate effect, range [0.0, 1.0] +bool audio_set_volume(audio_t* audio, audio_stream_id_t id, float volume); + +// Animate per-stream volume linearly over time, driven by audio_tick() +bool audio_animate_volume(audio_t* audio, audio_stream_id_t id, + float to_volume, float duration_sec); + +// Global volume, immediate effect, range [0.0, 1.0] +// Applied on top of per-stream volume at mix time +void audio_set_global_volume(audio_t* audio, float volume); + +// Animate global volume linearly over time, driven by audio_tick() +void audio_animate_global_volume(audio_t* audio, + float to_volume, float duration_sec); + +// Advances stream decoding and volume animations +// Call once per main loop iteration with the same delta as compositor_tick() +void audio_tick(audio_t* audio, float delta_time_sec); + +#ifdef __cplusplus +} +#endif + #endif // AUDIO_H \ No newline at end of file diff --git a/include/compositor.h b/include/compositor.h index 175a594..030552c 100644 --- a/include/compositor.h +++ b/include/compositor.h @@ -1,120 +1,120 @@ -// compositor.h v0.3 -#pragma once -#ifndef COMPOSITOR_H -#define COMPOSITOR_H - -// This subsystem is thread-unsafe. -// Caller must serialize all calls. Calls do not need to be made from the main thread. -// Removing an element while ticking/rendering may crash. -// Modifying an element while ticking/rendering may cause visual glitches. - -// Easing functions -enum easing_t { - EASE_LINEAR = 0, - EASE_IN_QUAD, - EASE_OUT_QUAD, - EASE_IN_OUT_QUAD, - EASE_IN_CUBIC, - EASE_OUT_CUBIC, - EASE_IN_OUT_CUBIC -}; - -// Anchor points -enum anchor_point_t { - ANCHOR_TOP_LEFT, - ANCHOR_TOP_CENTER, - ANCHOR_TOP_RIGHT, - ANCHOR_CENTER_LEFT, - ANCHOR_CENTER, - ANCHOR_CENTER_RIGHT, - ANCHOR_BOTTOM_LEFT, - ANCHOR_BOTTOM_CENTER, - ANCHOR_BOTTOM_RIGHT, -}; - -#ifdef __cplusplus -#include -extern "C" { -#else -#include -#include -#endif - -typedef struct compositor_t compositor_t; -typedef uint32_t element_handle_t; - -// If the return type is bool, false means failure. - -// Initialize compositor with given dimensions -// nullptr means failure -compositor_t* compositor_init(uint32_t width, uint32_t height); - -// Destroy compositor and free resources. Everything dies with it. -void compositor_destroy(compositor_t* comp); - -// Insert element, returns unique ID -// FB format is ARGB8888 and it always will be -// The compositor takes a reference, caller must keep alive until removed -// 0 means failure, valid IDs start from 1 -element_handle_t compositor_insert(compositor_t* comp, - const uint32_t* fb, - uint32_t fb_width, - uint32_t fb_height, - int32_t x, int32_t y, - int32_t z_order, float scale, - anchor_point_t anchor); - -// Remove element by ID -bool compositor_remove(compositor_t* comp, element_handle_t id); - -// Setters, immediate effect -bool compositor_set_position(compositor_t* comp, element_handle_t id, - int32_t x, int32_t y); -bool compositor_set_zorder(compositor_t* comp, element_handle_t id, - int32_t z_order); -bool compositor_set_scale(compositor_t* comp, element_handle_t id, - float scale); -bool compositor_set_visible(compositor_t* comp, element_handle_t id, - bool visible); -bool compositor_set_alpha(compositor_t* comp, element_handle_t id, - float alpha); - -// Advances all animations by a given time delta. -void compositor_tick(compositor_t* comp, float delta_time_sec); - -// Sets a target state to advance over time by tick() -// Queue means the animation will start after the previous animation completed -// Not queueing will play the animation alongside any current animation -// You cannot stack the same type of animation without queueing, the second call will cancel the first call. -bool compositor_animate_move(compositor_t* comp, element_handle_t id, - int32_t to_x, int32_t to_y, - float duration_sec, easing_t easing, bool queue, bool parallel); -bool compositor_animate_fade(compositor_t* comp, element_handle_t id, - float to_alpha, - float duration_sec, easing_t easing, bool queue, bool parallel); -bool compositor_animate_scale(compositor_t* comp, element_handle_t id, - float to_scale, - float duration_sec, easing_t easing, bool queue, bool parallel); - -// Immediately finish all animations on a specific object (optionally only N queued, 0 = all), or every object. -bool compositor_animation_finish_element(compositor_t* comp, element_handle_t id, int num_queued /*0=all*/); -void compositor_animation_finish_all(compositor_t* comp); - -// Returns true if any element is animating or has queued animations. -bool compositor_is_animating(compositor_t* comp); - -// Returns true if the specified element is animating or has queued animations. -bool compositor_is_element_animating(compositor_t* comp, element_handle_t id); - -// Compose all visible elements and return framebuffer pointer -const uint32_t* compositor_compose(compositor_t* comp); - -// Mark an element's content as dirty for redrawing -bool compositor_mark_content_dirty(compositor_t* comp, element_handle_t id); -void compositor_mark_all_dirty(compositor_t* comp); - -#ifdef __cplusplus -} -#endif - +// compositor.h v0.3 +#pragma once +#ifndef COMPOSITOR_H +#define COMPOSITOR_H + +// This subsystem is thread-unsafe. +// Caller must serialize all calls. Calls do not need to be made from the main thread. +// Removing an element while ticking/rendering may crash. +// Modifying an element while ticking/rendering may cause visual glitches. + +// Easing functions +enum easing_t { + EASE_LINEAR = 0, + EASE_IN_QUAD, + EASE_OUT_QUAD, + EASE_IN_OUT_QUAD, + EASE_IN_CUBIC, + EASE_OUT_CUBIC, + EASE_IN_OUT_CUBIC +}; + +// Anchor points +enum anchor_point_t { + ANCHOR_TOP_LEFT, + ANCHOR_TOP_CENTER, + ANCHOR_TOP_RIGHT, + ANCHOR_CENTER_LEFT, + ANCHOR_CENTER, + ANCHOR_CENTER_RIGHT, + ANCHOR_BOTTOM_LEFT, + ANCHOR_BOTTOM_CENTER, + ANCHOR_BOTTOM_RIGHT, +}; + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#include +#endif + +typedef struct compositor_t compositor_t; +typedef uint32_t element_handle_t; + +// If the return type is bool, false means failure. + +// Initialize compositor with given dimensions +// nullptr means failure +compositor_t* compositor_init(uint32_t width, uint32_t height); + +// Destroy compositor and free resources. Everything dies with it. +void compositor_destroy(compositor_t* comp); + +// Insert element, returns unique ID +// FB format is ARGB8888 and it always will be +// The compositor takes a reference, caller must keep alive until removed +// 0 means failure, valid IDs start from 1 +element_handle_t compositor_insert(compositor_t* comp, + const uint32_t* fb, + uint32_t fb_width, + uint32_t fb_height, + int32_t x, int32_t y, + int32_t z_order, float scale, + anchor_point_t anchor); + +// Remove element by ID +bool compositor_remove(compositor_t* comp, element_handle_t id); + +// Setters, immediate effect +bool compositor_set_position(compositor_t* comp, element_handle_t id, + int32_t x, int32_t y); +bool compositor_set_zorder(compositor_t* comp, element_handle_t id, + int32_t z_order); +bool compositor_set_scale(compositor_t* comp, element_handle_t id, + float scale); +bool compositor_set_visible(compositor_t* comp, element_handle_t id, + bool visible); +bool compositor_set_alpha(compositor_t* comp, element_handle_t id, + float alpha); + +// Advances all animations by a given time delta. +void compositor_tick(compositor_t* comp, float delta_time_sec); + +// Sets a target state to advance over time by tick() +// Queue means the animation will start after the previous animation completed +// Not queueing will play the animation alongside any current animation +// You cannot stack the same type of animation without queueing, the second call will cancel the first call. +bool compositor_animate_move(compositor_t* comp, element_handle_t id, + int32_t to_x, int32_t to_y, + float duration_sec, easing_t easing, bool queue, bool parallel); +bool compositor_animate_fade(compositor_t* comp, element_handle_t id, + float to_alpha, + float duration_sec, easing_t easing, bool queue, bool parallel); +bool compositor_animate_scale(compositor_t* comp, element_handle_t id, + float to_scale, + float duration_sec, easing_t easing, bool queue, bool parallel); + +// Immediately finish all animations on a specific object (optionally only N queued, 0 = all), or every object. +bool compositor_animation_finish_element(compositor_t* comp, element_handle_t id, int num_queued /*0=all*/); +void compositor_animation_finish_all(compositor_t* comp); + +// Returns true if any element is animating or has queued animations. +bool compositor_is_animating(compositor_t* comp); + +// Returns true if the specified element is animating or has queued animations. +bool compositor_is_element_animating(compositor_t* comp, element_handle_t id); + +// Compose all visible elements and return framebuffer pointer +const uint32_t* compositor_compose(compositor_t* comp); + +// Mark an element's content as dirty for redrawing +bool compositor_mark_content_dirty(compositor_t* comp, element_handle_t id); +void compositor_mark_all_dirty(compositor_t* comp); + +#ifdef __cplusplus +} +#endif + #endif // COMPOSITOR_H \ No newline at end of file diff --git a/include/image.h b/include/image.h index a095920..d891155 100644 --- a/include/image.h +++ b/include/image.h @@ -1,24 +1,24 @@ -// image.h -#pragma once -#ifndef IMAGE_H -#define IMAGE_H - -#ifdef __cplusplus -#include -extern "C" { -#else -#include -#endif - -// Loads an image file as ARGB8888. Returns pointer to pixel data, sets width/height. NULL on failure. -uint32_t* image_load(const char* filename, int* width, int* height); -// Loads and resizes an image file to desired dimensions as ARGB8888. Returns pointer to pixel data. NULL on failure. -uint32_t* image_load_resized(const char* filename, int desired_width, int desired_height); -// Frees image data. -void image_free(uint32_t* data); - -#ifdef __cplusplus -} -#endif - -#endif // IMAGE_H +// image.h +#pragma once +#ifndef IMAGE_H +#define IMAGE_H + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#endif + +// Loads an image file as ARGB8888. Returns pointer to pixel data, sets width/height. NULL on failure. +uint32_t* image_load(const char* filename, int* width, int* height); +// Loads and resizes an image file to desired dimensions as ARGB8888. Returns pointer to pixel data. NULL on failure. +uint32_t* image_load_resized(const char* filename, int desired_width, int desired_height); +// Frees image data. +void image_free(uint32_t* data); + +#ifdef __cplusplus +} +#endif + +#endif // IMAGE_H diff --git a/include/text.h b/include/text.h index f7b5f93..dcd1972 100644 --- a/include/text.h +++ b/include/text.h @@ -1,45 +1,45 @@ -// text.h -#pragma once -#ifndef TEXT_RENDERER_H -#define TEXT_RENDERER_H - -#include - -struct text_t; - -#ifdef __cplusplus -#include -extern "C" { -#else -#include -#endif - -// initializes a new text renderer with given width and height -text_t* text_init(int width, int height); -// shares cache, prefer when using the same font to save memory -text_t* text_init_shared(text_t* source, int width, int height); -// destroys the text renderer and frees resources -void text_destroy(text_t* text); -// pushes new text buffer to be rendered, can be stacked -void text_push(text_t* text, - const char* str, - float speed, - bool outline); -// load font and set as current font -void text_set_font(text_t* text, const char* font_path, float size); -// finish whole current text buffer immediately -void text_show_all_current(text_t* text); -// returns true if text is still animating -bool text_is_animating(text_t* text); -// advance, call every frame, returns true when it touched the framebuffer -bool text_nextframe(text_t* text, float deltatime); -// gets the framebuffer of the text renderer -const uint32_t* text_getfb(text_t* text); -// clears the text buffer and the frame buffer -void text_clear(text_t* text); - -#ifdef __cplusplus -} -#endif - +// text.h +#pragma once +#ifndef TEXT_RENDERER_H +#define TEXT_RENDERER_H + +#include + +struct text_t; + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#endif + +// initializes a new text renderer with given width and height +text_t* text_init(int width, int height); +// shares cache, prefer when using the same font to save memory +text_t* text_init_shared(text_t* source, int width, int height); +// destroys the text renderer and frees resources +void text_destroy(text_t* text); +// pushes new text buffer to be rendered, can be stacked +void text_push(text_t* text, + const char* str, + float speed, + bool outline); +// load font and set as current font +void text_set_font(text_t* text, const char* font_path, float size); +// finish whole current text buffer immediately +void text_show_all_current(text_t* text); +// returns true if text is still animating +bool text_is_animating(text_t* text); +// advance, call every frame, returns true when it touched the framebuffer +bool text_nextframe(text_t* text, float deltatime); +// gets the framebuffer of the text renderer +const uint32_t* text_getfb(text_t* text); +// clears the text buffer and the frame buffer +void text_clear(text_t* text); + +#ifdef __cplusplus +} +#endif + #endif // TEXT_RENDERER_H \ No newline at end of file diff --git a/include/window.h b/include/window.h index af66b6a..77aa717 100644 --- a/include/window.h +++ b/include/window.h @@ -1,77 +1,77 @@ -// window.h -#pragma once -#ifndef WINDOW_H -#define WINDOW_H - -typedef struct window_t window_t; - -enum KeyCode { - // Letters - KEY_A = 4, - KEY_D = 7, - KEY_S = 22, - KEY_W = 26, - - // Numbers - KEY_1 = 30, - KEY_0 = 39, - - // Controls - KEY_RETURN = 40, - KEY_ESCAPE = 41, - KEY_BACKSPACE = 42, - KEY_TAB = 43, - KEY_SPACE = 44, - - // Modifiers - KEY_LCTRL = 224, - KEY_LSHIFT = 225, - KEY_RCTRL = 228, - KEY_RSHIFT = 229, - - // Navigation - KEY_RIGHT = 79, - KEY_LEFT = 80, - KEY_DOWN = 81, - KEY_UP = 82, - - // Function Keys - KEY_F1 = 58, - KEY_F2 = 59 -}; - -enum MouseButton { - MOUSE_BUTTON_LEFT = 1, - MOUSE_BUTTON_MIDDLE = 2, - MOUSE_BUTTON_RIGHT = 3 -}; - -#ifdef __cplusplus -#include -extern "C" { -#else -#include -#endif - -typedef struct { int x, y; } mouse_pos_t; - -// creates a window with given width, height and title, title can be utf8 -window_t* window_create(int width, int height, const char* title); -// destroys the window -void window_destroy(window_t* s); -// polls window events, returns true if the window should close -bool window_pollevent(window_t* s); -// blits the given framebuffer to the window. must be the same size as the window and ARGB8888 format -void window_blit(window_t* s, const uint32_t* framebuffer); -// returns true if the given key is currently held down -bool window_is_key_down(window_t* s, int key_scancode); -// returns true if the given mouse button is currently held down -bool window_is_mouse_button_down(window_t* s, int button_index); -// gets the current mouse position in window coordinates -mouse_pos_t window_get_mouse_position(window_t* s); - -#ifdef __cplusplus -} -#endif - -#endif // WINDOW_H +// window.h +#pragma once +#ifndef WINDOW_H +#define WINDOW_H + +typedef struct window_t window_t; + +enum KeyCode { + // Letters + KEY_A = 4, + KEY_D = 7, + KEY_S = 22, + KEY_W = 26, + + // Numbers + KEY_1 = 30, + KEY_0 = 39, + + // Controls + KEY_RETURN = 40, + KEY_ESCAPE = 41, + KEY_BACKSPACE = 42, + KEY_TAB = 43, + KEY_SPACE = 44, + + // Modifiers + KEY_LCTRL = 224, + KEY_LSHIFT = 225, + KEY_RCTRL = 228, + KEY_RSHIFT = 229, + + // Navigation + KEY_RIGHT = 79, + KEY_LEFT = 80, + KEY_DOWN = 81, + KEY_UP = 82, + + // Function Keys + KEY_F1 = 58, + KEY_F2 = 59 +}; + +enum MouseButton { + MOUSE_BUTTON_LEFT = 1, + MOUSE_BUTTON_MIDDLE = 2, + MOUSE_BUTTON_RIGHT = 3 +}; + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#endif + +typedef struct { int x, y; } mouse_pos_t; + +// creates a window with given width, height and title, title can be utf8 +window_t* window_create(int width, int height, const char* title); +// destroys the window +void window_destroy(window_t* s); +// polls window events, returns true if the window should close +bool window_pollevent(window_t* s); +// blits the given framebuffer to the window. must be the same size as the window and ARGB8888 format +void window_blit(window_t* s, const uint32_t* framebuffer); +// returns true if the given key is currently held down +bool window_is_key_down(window_t* s, int key_scancode); +// returns true if the given mouse button is currently held down +bool window_is_mouse_button_down(window_t* s, int button_index); +// gets the current mouse position in window coordinates +mouse_pos_t window_get_mouse_position(window_t* s); + +#ifdef __cplusplus +} +#endif + +#endif // WINDOW_H diff --git a/src/audio.cpp b/src/audio.cpp index 0340e91..bdb0802 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -1,343 +1,343 @@ -// audio.cpp -#include "audio.h" -#include -#define MADPCM_IMPLEMENTATION -#include "madpcm.h" -#include -#include -#include -#include - -#define AHEAD_BLOCKS 10 - -struct AudioDevice { - SDL_AudioDeviceID id = 0; - AudioDevice() = default; - explicit AudioDevice(SDL_AudioDeviceID id) : id(id) {} - ~AudioDevice() { if (id) SDL_CloseAudioDevice(id); } - AudioDevice(const AudioDevice&) = delete; - AudioDevice& operator=(const AudioDevice&) = delete; - AudioDevice(AudioDevice&& o) noexcept : id(o.id) { o.id = 0; } - AudioDevice& operator=(AudioDevice&& o) noexcept { - if (this != &o) { if (id) SDL_CloseAudioDevice(id); id = o.id; o.id = 0; } - return *this; - } - operator bool() const { return id != 0; } -}; - -struct Stream { - uint32_t id = 0; - AudioDevice device; - const uint8_t* data = nullptr; - int total_blocks = 0; - int current_block = 0; - int channels = 0; - bool loop = false; - bool paused = false; - - float volume = 1.0f; - float volume_from = 1.0f; - float volume_to = 1.0f; - float volume_anim_elapsed = 0.0f; - float volume_anim_duration = 0.0f; -}; - -struct RiffHeader { - char riff[4]; - uint32_t fileSize; - char wave[4]; -}; - -struct audio_t { - std::unordered_map streams; - uint32_t next_id = 1; - - float global_volume = 1.0f; - float global_volume_from = 1.0f; - float global_volume_to = 1.0f; - float global_volume_anim_elapsed = 0.0f; - float global_volume_anim_duration = 0.0f; -}; - -static float clampf(float v, float lo, float hi) { - return v < lo ? lo : v > hi ? hi : v; -} - -// Decode one block, scale by combined volume, queue to device. -// Returns false if the stream has ended (non-looping, EOF). -static bool pump_block(audio_t* audio, Stream& s) { - int block_size = (s.channels == 2) ? MADPCM_STEREO_SIZE : MADPCM_MONO_SIZE; - - if (s.current_block >= s.total_blocks) { - if (s.loop) s.current_block = 0; - else return false; - } - - const uint8_t* block = s.data + s.current_block * block_size; - int16_t pcm[MADPCM_BLOCK_SAMPLES * 2]; - int sample_count; - - if (s.channels == 2) { - int16_t L[MADPCM_BLOCK_SAMPLES], R[MADPCM_BLOCK_SAMPLES]; - madpcm_decode_block_stereo(block, L, R); - for (int i = 0; i < MADPCM_BLOCK_SAMPLES; i++) { - pcm[i * 2] = L[i]; - pcm[i * 2 + 1] = R[i]; - } - sample_count = MADPCM_BLOCK_SAMPLES * 2; - } - else { - madpcm_decode_block(block, pcm); - sample_count = MADPCM_BLOCK_SAMPLES; - } - - float vol = clampf(s.volume * audio->global_volume, 0.0f, 1.0f); - for (int i = 0; i < sample_count; i++) - pcm[i] = static_cast(pcm[i] * vol); - - SDL_QueueAudio(s.device.id, pcm, static_cast(sample_count * sizeof(int16_t))); - s.current_block++; - return true; -} - -extern "C" { - -audio_t* audio_init() { - if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) return nullptr; - return new audio_t{}; -} - -void audio_destroy(audio_t* audio) { - if (!audio) return; - // streams destructor closes all SDL devices via RAII - delete audio; - SDL_QuitSubSystem(SDL_INIT_AUDIO); -} - -uint8_t* audio_load_madpcm(const char* filename, int* out_size, int* out_channels, int* out_rate, int* out_total_samples) { - FILE* f = fopen(filename, "rb"); - if (!f) return nullptr; - - RiffHeader h; - if (fread(&h, sizeof(h), 1, f) != 1 || - memcmp(h.riff, "RIFF", 4) != 0 || - memcmp(h.wave, "WAVE", 4) != 0) { - fclose(f); - return nullptr; - } - - char chunkId[4]; - uint32_t chunkSize; - int fmtFound = 0; - uint16_t audioFmt = 0; - uint16_t numChannels = 0; - uint32_t dataLen = 0; - int rate = 0; - uint8_t* data = nullptr; - - while (fread(chunkId, 1, 4, f) == 4) { - if (fread(&chunkSize, 4, 1, f) != 1) break; - - if (memcmp(chunkId, "fmt ", 4) == 0) { - if (fread(&audioFmt, 2, 1, f) != 1) break; - if (fread(&numChannels, 2, 1, f) != 1) break; - if (fread(&rate, 4, 1, f) != 1) break; - fseek(f, chunkSize - 8, SEEK_CUR); - fmtFound = 1; - } - else if (memcmp(chunkId, "data", 4) == 0) { - dataLen = chunkSize; - data = static_cast(malloc(dataLen)); - if (fread(data, 1, dataLen, f) != dataLen) { - free(data); - fclose(f); - return nullptr; - } - break; - } - else { - fseek(f, chunkSize, SEEK_CUR); - } - } - - fclose(f); - - if (!fmtFound || audioFmt != MADPCM_WAVE_FORMAT || !data) { - if (data) free(data); - return nullptr; - } - - if (out_channels) *out_channels = static_cast(numChannels); - if (out_size) *out_size = static_cast(dataLen); - if (out_rate) *out_rate = rate; - - if (out_total_samples) { - int blockSize = (numChannels == 2) ? MADPCM_STEREO_SIZE : MADPCM_MONO_SIZE; - int numBlocks = dataLen / blockSize; - *out_total_samples = numBlocks * MADPCM_BLOCK_SAMPLES; - } - - return data; -} - -void audio_free_data(uint8_t* data) { - if (data) { - free(data); - } -} - -audio_stream_id_t audio_play(audio_t* audio, const uint8_t* data, int size, - int channels, int rate, bool loop) { - if (!audio || !data) return 0; - - SDL_AudioSpec spec{}; - spec.freq = rate; - spec.format = AUDIO_S16SYS; - spec.channels = static_cast(channels); - spec.samples = MADPCM_BLOCK_SAMPLES; - - SDL_AudioDeviceID dev = SDL_OpenAudioDevice(nullptr, 0, &spec, nullptr, 0); - if (!dev) return 0; - - int block_size = (channels == 2) ? MADPCM_STEREO_SIZE : MADPCM_MONO_SIZE; - int total_blocks = size / block_size; - uint32_t id = audio->next_id++; - - Stream& s = audio->streams[id]; - s.id = id; - s.device = AudioDevice(dev); - s.data = data; - s.total_blocks = total_blocks; - s.channels = channels; - s.loop = loop; - - for (int i = 0; i < AHEAD_BLOCKS; i++) - if (!pump_block(audio, s)) break; - - SDL_PauseAudioDevice(dev, 0); - return id; -} - -bool audio_stop(audio_t* audio, audio_stream_id_t id) { - if (!audio) return false; - return audio->streams.erase(id) > 0; // destructor closes the device -} - -bool audio_pause(audio_t* audio, audio_stream_id_t id) { - if (!audio) return false; - auto it = audio->streams.find(id); - if (it == audio->streams.end()) return false; - Stream& s = it->second; - if (!s.paused) { - SDL_PauseAudioDevice(s.device.id, 1); - s.paused = true; - } - return true; -} - -bool audio_resume(audio_t* audio, audio_stream_id_t id) { - if (!audio) return false; - auto it = audio->streams.find(id); - if (it == audio->streams.end()) return false; - Stream& s = it->second; - if (s.paused) { - SDL_PauseAudioDevice(s.device.id, 0); - s.paused = false; - } - return true; -} - -bool audio_is_active(audio_t* audio, audio_stream_id_t id) { - if (!audio) return false; - return audio->streams.count(id) > 0; -} - -bool audio_is_playing(audio_t* audio, audio_stream_id_t id) { - if (!audio) return false; - auto it = audio->streams.find(id); - if (it == audio->streams.end()) return false; - return !it->second.paused; -} - -bool audio_set_volume(audio_t* audio, audio_stream_id_t id, float volume) { - if (!audio) return false; - auto it = audio->streams.find(id); - if (it == audio->streams.end()) return false; - Stream& s = it->second; - s.volume = clampf(volume, 0.0f, 1.0f); - s.volume_anim_duration = 0.0f; - return true; -} - -bool audio_animate_volume(audio_t* audio, audio_stream_id_t id, - float to_volume, float duration_sec) { - if (!audio) return false; - auto it = audio->streams.find(id); - if (it == audio->streams.end()) return false; - Stream& s = it->second; - s.volume_from = s.volume; - s.volume_to = clampf(to_volume, 0.0f, 1.0f); - s.volume_anim_elapsed = 0.0f; - s.volume_anim_duration = duration_sec; - return true; -} - -void audio_set_global_volume(audio_t* audio, float volume) { - if (!audio) return; - audio->global_volume = clampf(volume, 0.0f, 1.0f); - audio->global_volume_anim_duration = 0.0f; -} - -void audio_animate_global_volume(audio_t* audio, - float to_volume, float duration_sec) { - if (!audio) return; - audio->global_volume_from = audio->global_volume; - audio->global_volume_to = clampf(to_volume, 0.0f, 1.0f); - audio->global_volume_anim_elapsed = 0.0f; - audio->global_volume_anim_duration = duration_sec; -} - -void audio_tick(audio_t* audio, float delta_time_sec) { - if (!audio) return; - - // Advance global volume animation - if (audio->global_volume_anim_duration > 0.0f) { - audio->global_volume_anim_elapsed += delta_time_sec; - float t = audio->global_volume_anim_elapsed / audio->global_volume_anim_duration; - if (t >= 1.0f) { t = 1.0f; audio->global_volume_anim_duration = 0.0f; } - audio->global_volume = audio->global_volume_from + - (audio->global_volume_to - audio->global_volume_from) * t; - } - - // Collect IDs of streams to remove after iteration to avoid invalidating the map - std::vector to_remove; - - for (auto& [id, s] : audio->streams) { - // Advance per-stream volume animation - if (s.volume_anim_duration > 0.0f) { - s.volume_anim_elapsed += delta_time_sec; - float t = s.volume_anim_elapsed / s.volume_anim_duration; - if (t >= 1.0f) { t = 1.0f; s.volume_anim_duration = 0.0f; } - s.volume = s.volume_from + (s.volume_to - s.volume_from) * t; - } - - if (s.paused) continue; - - size_t target_bytes = AHEAD_BLOCKS * MADPCM_BLOCK_SAMPLES - * s.channels * sizeof(int16_t); - - while (SDL_GetQueuedAudioSize(s.device.id) < target_bytes) { - if (!pump_block(audio, s)) { - // Non-looping EOF — wait for queue to fully drain then remove - if (SDL_GetQueuedAudioSize(s.device.id) == 0) - to_remove.push_back(id); - break; - } - } - } - - for (uint32_t id : to_remove) - audio->streams.erase(id); -} - -} // extern "C" +// audio.cpp +#include "audio.h" +#include +#define MADPCM_IMPLEMENTATION +#include "madpcm.h" +#include +#include +#include +#include + +#define AHEAD_BLOCKS 10 + +struct AudioDevice { + SDL_AudioDeviceID id = 0; + AudioDevice() = default; + explicit AudioDevice(SDL_AudioDeviceID id) : id(id) {} + ~AudioDevice() { if (id) SDL_CloseAudioDevice(id); } + AudioDevice(const AudioDevice&) = delete; + AudioDevice& operator=(const AudioDevice&) = delete; + AudioDevice(AudioDevice&& o) noexcept : id(o.id) { o.id = 0; } + AudioDevice& operator=(AudioDevice&& o) noexcept { + if (this != &o) { if (id) SDL_CloseAudioDevice(id); id = o.id; o.id = 0; } + return *this; + } + operator bool() const { return id != 0; } +}; + +struct Stream { + uint32_t id = 0; + AudioDevice device; + const uint8_t* data = nullptr; + int total_blocks = 0; + int current_block = 0; + int channels = 0; + bool loop = false; + bool paused = false; + + float volume = 1.0f; + float volume_from = 1.0f; + float volume_to = 1.0f; + float volume_anim_elapsed = 0.0f; + float volume_anim_duration = 0.0f; +}; + +struct RiffHeader { + char riff[4]; + uint32_t fileSize; + char wave[4]; +}; + +struct audio_t { + std::unordered_map streams; + uint32_t next_id = 1; + + float global_volume = 1.0f; + float global_volume_from = 1.0f; + float global_volume_to = 1.0f; + float global_volume_anim_elapsed = 0.0f; + float global_volume_anim_duration = 0.0f; +}; + +static float clampf(float v, float lo, float hi) { + return v < lo ? lo : v > hi ? hi : v; +} + +// Decode one block, scale by combined volume, queue to device. +// Returns false if the stream has ended (non-looping, EOF). +static bool pump_block(audio_t* audio, Stream& s) { + int block_size = (s.channels == 2) ? MADPCM_STEREO_SIZE : MADPCM_MONO_SIZE; + + if (s.current_block >= s.total_blocks) { + if (s.loop) s.current_block = 0; + else return false; + } + + const uint8_t* block = s.data + s.current_block * block_size; + int16_t pcm[MADPCM_BLOCK_SAMPLES * 2]; + int sample_count; + + if (s.channels == 2) { + int16_t L[MADPCM_BLOCK_SAMPLES], R[MADPCM_BLOCK_SAMPLES]; + madpcm_decode_block_stereo(block, L, R); + for (int i = 0; i < MADPCM_BLOCK_SAMPLES; i++) { + pcm[i * 2] = L[i]; + pcm[i * 2 + 1] = R[i]; + } + sample_count = MADPCM_BLOCK_SAMPLES * 2; + } + else { + madpcm_decode_block(block, pcm); + sample_count = MADPCM_BLOCK_SAMPLES; + } + + float vol = clampf(s.volume * audio->global_volume, 0.0f, 1.0f); + for (int i = 0; i < sample_count; i++) + pcm[i] = static_cast(pcm[i] * vol); + + SDL_QueueAudio(s.device.id, pcm, static_cast(sample_count * sizeof(int16_t))); + s.current_block++; + return true; +} + +extern "C" { + +audio_t* audio_init() { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) return nullptr; + return new audio_t{}; +} + +void audio_destroy(audio_t* audio) { + if (!audio) return; + // streams destructor closes all SDL devices via RAII + delete audio; + SDL_QuitSubSystem(SDL_INIT_AUDIO); +} + +uint8_t* audio_load_madpcm(const char* filename, int* out_size, int* out_channels, int* out_rate, int* out_total_samples) { + FILE* f = fopen(filename, "rb"); + if (!f) return nullptr; + + RiffHeader h; + if (fread(&h, sizeof(h), 1, f) != 1 || + memcmp(h.riff, "RIFF", 4) != 0 || + memcmp(h.wave, "WAVE", 4) != 0) { + fclose(f); + return nullptr; + } + + char chunkId[4]; + uint32_t chunkSize; + int fmtFound = 0; + uint16_t audioFmt = 0; + uint16_t numChannels = 0; + uint32_t dataLen = 0; + int rate = 0; + uint8_t* data = nullptr; + + while (fread(chunkId, 1, 4, f) == 4) { + if (fread(&chunkSize, 4, 1, f) != 1) break; + + if (memcmp(chunkId, "fmt ", 4) == 0) { + if (fread(&audioFmt, 2, 1, f) != 1) break; + if (fread(&numChannels, 2, 1, f) != 1) break; + if (fread(&rate, 4, 1, f) != 1) break; + fseek(f, chunkSize - 8, SEEK_CUR); + fmtFound = 1; + } + else if (memcmp(chunkId, "data", 4) == 0) { + dataLen = chunkSize; + data = static_cast(malloc(dataLen)); + if (fread(data, 1, dataLen, f) != dataLen) { + free(data); + fclose(f); + return nullptr; + } + break; + } + else { + fseek(f, chunkSize, SEEK_CUR); + } + } + + fclose(f); + + if (!fmtFound || audioFmt != MADPCM_WAVE_FORMAT || !data) { + if (data) free(data); + return nullptr; + } + + if (out_channels) *out_channels = static_cast(numChannels); + if (out_size) *out_size = static_cast(dataLen); + if (out_rate) *out_rate = rate; + + if (out_total_samples) { + int blockSize = (numChannels == 2) ? MADPCM_STEREO_SIZE : MADPCM_MONO_SIZE; + int numBlocks = dataLen / blockSize; + *out_total_samples = numBlocks * MADPCM_BLOCK_SAMPLES; + } + + return data; +} + +void audio_free_data(uint8_t* data) { + if (data) { + free(data); + } +} + +audio_stream_id_t audio_play(audio_t* audio, const uint8_t* data, int size, + int channels, int rate, bool loop) { + if (!audio || !data) return 0; + + SDL_AudioSpec spec{}; + spec.freq = rate; + spec.format = AUDIO_S16SYS; + spec.channels = static_cast(channels); + spec.samples = MADPCM_BLOCK_SAMPLES; + + SDL_AudioDeviceID dev = SDL_OpenAudioDevice(nullptr, 0, &spec, nullptr, 0); + if (!dev) return 0; + + int block_size = (channels == 2) ? MADPCM_STEREO_SIZE : MADPCM_MONO_SIZE; + int total_blocks = size / block_size; + uint32_t id = audio->next_id++; + + Stream& s = audio->streams[id]; + s.id = id; + s.device = AudioDevice(dev); + s.data = data; + s.total_blocks = total_blocks; + s.channels = channels; + s.loop = loop; + + for (int i = 0; i < AHEAD_BLOCKS; i++) + if (!pump_block(audio, s)) break; + + SDL_PauseAudioDevice(dev, 0); + return id; +} + +bool audio_stop(audio_t* audio, audio_stream_id_t id) { + if (!audio) return false; + return audio->streams.erase(id) > 0; // destructor closes the device +} + +bool audio_pause(audio_t* audio, audio_stream_id_t id) { + if (!audio) return false; + auto it = audio->streams.find(id); + if (it == audio->streams.end()) return false; + Stream& s = it->second; + if (!s.paused) { + SDL_PauseAudioDevice(s.device.id, 1); + s.paused = true; + } + return true; +} + +bool audio_resume(audio_t* audio, audio_stream_id_t id) { + if (!audio) return false; + auto it = audio->streams.find(id); + if (it == audio->streams.end()) return false; + Stream& s = it->second; + if (s.paused) { + SDL_PauseAudioDevice(s.device.id, 0); + s.paused = false; + } + return true; +} + +bool audio_is_active(audio_t* audio, audio_stream_id_t id) { + if (!audio) return false; + return audio->streams.count(id) > 0; +} + +bool audio_is_playing(audio_t* audio, audio_stream_id_t id) { + if (!audio) return false; + auto it = audio->streams.find(id); + if (it == audio->streams.end()) return false; + return !it->second.paused; +} + +bool audio_set_volume(audio_t* audio, audio_stream_id_t id, float volume) { + if (!audio) return false; + auto it = audio->streams.find(id); + if (it == audio->streams.end()) return false; + Stream& s = it->second; + s.volume = clampf(volume, 0.0f, 1.0f); + s.volume_anim_duration = 0.0f; + return true; +} + +bool audio_animate_volume(audio_t* audio, audio_stream_id_t id, + float to_volume, float duration_sec) { + if (!audio) return false; + auto it = audio->streams.find(id); + if (it == audio->streams.end()) return false; + Stream& s = it->second; + s.volume_from = s.volume; + s.volume_to = clampf(to_volume, 0.0f, 1.0f); + s.volume_anim_elapsed = 0.0f; + s.volume_anim_duration = duration_sec; + return true; +} + +void audio_set_global_volume(audio_t* audio, float volume) { + if (!audio) return; + audio->global_volume = clampf(volume, 0.0f, 1.0f); + audio->global_volume_anim_duration = 0.0f; +} + +void audio_animate_global_volume(audio_t* audio, + float to_volume, float duration_sec) { + if (!audio) return; + audio->global_volume_from = audio->global_volume; + audio->global_volume_to = clampf(to_volume, 0.0f, 1.0f); + audio->global_volume_anim_elapsed = 0.0f; + audio->global_volume_anim_duration = duration_sec; +} + +void audio_tick(audio_t* audio, float delta_time_sec) { + if (!audio) return; + + // Advance global volume animation + if (audio->global_volume_anim_duration > 0.0f) { + audio->global_volume_anim_elapsed += delta_time_sec; + float t = audio->global_volume_anim_elapsed / audio->global_volume_anim_duration; + if (t >= 1.0f) { t = 1.0f; audio->global_volume_anim_duration = 0.0f; } + audio->global_volume = audio->global_volume_from + + (audio->global_volume_to - audio->global_volume_from) * t; + } + + // Collect IDs of streams to remove after iteration to avoid invalidating the map + std::vector to_remove; + + for (auto& [id, s] : audio->streams) { + // Advance per-stream volume animation + if (s.volume_anim_duration > 0.0f) { + s.volume_anim_elapsed += delta_time_sec; + float t = s.volume_anim_elapsed / s.volume_anim_duration; + if (t >= 1.0f) { t = 1.0f; s.volume_anim_duration = 0.0f; } + s.volume = s.volume_from + (s.volume_to - s.volume_from) * t; + } + + if (s.paused) continue; + + size_t target_bytes = AHEAD_BLOCKS * MADPCM_BLOCK_SAMPLES + * s.channels * sizeof(int16_t); + + while (SDL_GetQueuedAudioSize(s.device.id) < target_bytes) { + if (!pump_block(audio, s)) { + // Non-looping EOF — wait for queue to fully drain then remove + if (SDL_GetQueuedAudioSize(s.device.id) == 0) + to_remove.push_back(id); + break; + } + } + } + + for (uint32_t id : to_remove) + audio->streams.erase(id); +} + +} // extern "C" diff --git a/src/compositor.cpp b/src/compositor.cpp index f88a6ef..a492698 100644 --- a/src/compositor.cpp +++ b/src/compositor.cpp @@ -1,1185 +1,1185 @@ -#include -#include -#include -#include -#include -#include -#include - -// Define the tile size -#define TILE_SIZE 64 - -struct property_anim_t { - float progress_sec; - float duration_sec; - easing_t easing; -}; - -// Animation types -enum animation_type_t { - ANIM_NONE = 0, - ANIM_MOVE, - ANIM_FADE, - ANIM_SCALE, - ANIM_MOVE_FADE // Combined animation -}; - -struct animation_t { - animation_type_t type = ANIM_NONE; - - // Start/End state - int32_t start_x = 0, start_y = 0; - int32_t end_x = 0, end_y = 0; - - float start_scale = 1.0f; - float end_scale = 1.0f; - - float start_alpha = 1.0f; - float end_alpha = 1.0f; - - // Per-property animation state - property_anim_t position_anim; - property_anim_t scale_anim; - property_anim_t alpha_anim; - - // Track which properties are being animated - bool animating_position = false; - bool animating_scale = false; - bool animating_alpha = false; -}; - -// Queued animation structure -struct queued_animation_t { - animation_type_t type; - easing_t easing; - float duration_sec; - - // Target values - int32_t to_x, to_y; - float to_scale; - float to_alpha; - - bool is_move_fade; - - // Which properties this animation affects - bool affects_position; - bool affects_scale; - bool affects_alpha; - bool parallel = false; -}; - -struct element_t { - const uint32_t* fb; // Format ARGB8888 - uint32_t fb_width; - uint32_t fb_height; - int32_t x, y; - int32_t z_order; // Used for draw order - bool visible; - bool valid; // Do we still exist? - float scale; - float alpha = 1.0f; - anchor_point_t anchor; // Anchor point for positioning and scaling - uint32_t id; // Unique identifier - std::vector occupied_tiles; // List of occupied tiles - animation_t anim; - std::vector anim_queue; -}; - -struct compositor_t { - // Main Framebuffer - std::vector framebuffer; - uint32_t width; - uint32_t height; - - // Element Management - // Storage: Map unique ID to element data (O(1) lookup) - std::unordered_map elements_map; - uint32_t next_id = 1; // Unique ID counter (0 is reserved/invalid) - - // Draw Order: Vector of IDs, maintained sorted by z_order - std::vector draw_order; - - // Tiling System (Spatial Hashing) - uint32_t tiles_x; - uint32_t tiles_y; - // dirty_tiles: Flags which tiles need redrawing - std::vector dirty_tiles; - // tile_map: List of element IDs that overlap each tile - std::vector> tile_map; -}; - -// Linear interpolation (lerp) -static float lerp(float a, float b, float t) { - return a + (b - a) * t; -} - -// Applies the selected easing function to a time value -static float apply_easing(easing_t easing, float t) { - // (t is a value from 0.0 to 1.0) - switch (easing) { - case EASE_IN_QUAD: - return t * t; - case EASE_OUT_QUAD: - return 1.0f - (1.0f - t) * (1.0f - t); - case EASE_IN_OUT_QUAD: - return t < 0.5f ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) / 2.0f; - case EASE_IN_CUBIC: - return t * t * t; - case EASE_OUT_CUBIC: - return 1.0f - std::pow(1.0f - t, 3.0f); - case EASE_IN_OUT_CUBIC: - return t < 0.5f ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f; - case EASE_LINEAR: - default: - return t; - } -} - -// Finds the element structure corresponding to a given ID, or nullptr if not found. -static element_t* find_element(compositor_t* comp, uint32_t id) { - auto it = comp->elements_map.find(id); - if (it == comp->elements_map.end()) { - return nullptr; - } - if (!it->second.valid) { - return nullptr; - } - return &it->second; -} - -// Calculates the final screen-space bounding box for an element (THE source of truth). -static inline void get_element_bounds(const element_t* elem, - int32_t& out_x0, int32_t& out_y0, - int32_t& out_x1, int32_t& out_y1) { - // Calculate scaled dimensions, rounding to nearest integer to prevent jitter - int32_t scaled_width = (int32_t)std::round(elem->fb_width * elem->scale); - int32_t scaled_height = (int32_t)std::round(elem->fb_height * elem->scale); - - float anchor_x_factor = 0.0f; - float anchor_y_factor = 0.0f; - - // Determine the anchor's proportional position within the element - switch (elem->anchor) { - case ANCHOR_TOP_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 0.0f; break; - case ANCHOR_TOP_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 0.0f; break; - case ANCHOR_TOP_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 0.0f; break; - case ANCHOR_CENTER_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 0.5f; break; - case ANCHOR_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 0.5f; break; - case ANCHOR_CENTER_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 0.5f; break; - case ANCHOR_BOTTOM_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 1.0f; break; - case ANCHOR_BOTTOM_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 1.0f; break; - case ANCHOR_BOTTOM_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 1.0f; break; - } - - // The element's (x, y) represents the anchor point's position on screen. - // We calculate the top-left corner (out_x0, out_y0) by offsetting - // from the anchor's position. - out_x0 = elem->x - (int32_t)std::round(scaled_width * anchor_x_factor); - out_y0 = elem->y - (int32_t)std::round(scaled_height * anchor_y_factor); - - // The bottom-right corner is the top-left plus the scaled dimensions. - out_x1 = out_x0 + scaled_width; - out_y1 = out_y0 + scaled_height; -} - -// Marks all tiles an element occupies as dirty. -static void mark_element_dirty(compositor_t* comp, const element_t* elem) { - if (!elem || !elem->visible) return; - - // Get bounds from our new single source of truth - int32_t x0, y0, x1, y1; - get_element_bounds(elem, x0, y0, x1, y1); - - // Calculate bounding box in tile coordinates - int32_t tile_x0 = x0 / TILE_SIZE; - int32_t tile_y0 = y0 / TILE_SIZE; - int32_t tile_x1 = (x1 + TILE_SIZE - 1) / TILE_SIZE; - int32_t tile_y1 = (y1 + TILE_SIZE - 1) / TILE_SIZE; - - // Clamp tile coordinates to bounds - tile_x0 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x0)); - tile_y0 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y0)); - tile_x1 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x1)); - tile_y1 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y1)); - - // Mark tiles as dirty - for (int32_t ty = tile_y0; ty < tile_y1; ty++) { - for (int32_t tx = tile_x0; tx < tile_x1; tx++) { - comp->dirty_tiles[ty * comp->tiles_x + tx] = 1; - } - } -} - -// Removes an element's ID from all tiles in the spatial hash map. -static void clear_spatial_hash(compositor_t* comp, uint32_t id) { - element_t* elem = find_element(comp, id); - if (!elem) return; - - for (uint32_t tile_idx : elem->occupied_tiles) { - if (tile_idx >= comp->tile_map.size()) continue; - auto& tile_list = comp->tile_map[tile_idx]; - tile_list.erase(std::remove(tile_list.begin(), tile_list.end(), id), tile_list.end()); - } - - elem->occupied_tiles.clear(); -} - -// Updates an element's position in the spatial hash map and marks its new tiles as dirty. -static void update_spatial_hash(compositor_t* comp, element_t* elem) { - if (!elem || !elem->visible) return; - - mark_element_dirty(comp, elem); // Mark new area - elem->occupied_tiles.clear(); - - // Get bounds from our new single source of truth - int32_t x0, y0, x1, y1; - get_element_bounds(elem, x0, y0, x1, y1); - - // Convert to tile coordinates - int32_t tile_x0 = x0 / TILE_SIZE; - int32_t tile_y0 = y0 / TILE_SIZE; - int32_t tile_x1 = (x1 + TILE_SIZE - 1) / TILE_SIZE; - int32_t tile_y1 = (y1 + TILE_SIZE - 1) / TILE_SIZE; - - // Clamp - tile_x0 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x0)); - tile_y0 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y0)); - tile_x1 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x1)); - tile_y1 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y1)); - - // Add to spatial hash and record tiles - for (int32_t ty = tile_y0; ty < tile_y1; ty++) { - for (int32_t tx = tile_x0; tx < tile_x1; tx++) { - uint32_t tile_idx = ty * comp->tiles_x + tx; - auto& tile_list = comp->tile_map[tile_idx]; - - auto it_insert = std::lower_bound( - tile_list.begin(), - tile_list.end(), - elem->z_order, - [comp](uint32_t current_id, int32_t target_z) { - return comp->elements_map[current_id].z_order < target_z; - } - ); - - tile_list.insert(it_insert, elem->id); - elem->occupied_tiles.push_back(tile_idx); - } - } -} - -// Uses an incremental insertion sort to place an element in the correct z-order position. -static void update_draw_order(compositor_t* comp, uint32_t id) { - element_t* elem = find_element(comp, id); - if (!elem) return; - - // Remove element from its current position in draw_order - auto it_to_remove = std::find(comp->draw_order.begin(), comp->draw_order.end(), id); - if (it_to_remove != comp->draw_order.end()) { - comp->draw_order.erase(it_to_remove); - } - - // Find the new correct position using binary search logic (lower_bound) - auto it_insert = std::lower_bound( - comp->draw_order.begin(), - comp->draw_order.end(), - elem->z_order, - [comp](uint32_t current_id, int32_t target_z) { - // Find the z_order of the element corresponding to current_id - auto it = comp->elements_map.find(current_id); - if (it == comp->elements_map.end()) return true; // Safety break - return it->second.z_order < target_z; - } - ); - - // Insert the ID at the new position - comp->draw_order.insert(it_insert, id); -} - -extern "C" { - -// Advances all animations by a given time delta. - void compositor_tick(compositor_t* comp, float delta_time_sec) { - if (!comp) return; - - for (auto& pair : comp->elements_map) { - element_t* elem = &pair.second; - if (!elem->valid) continue; - animation_t* anim = &elem->anim; - - // Are we currently busy doing ANYTHING? - bool is_busy = anim->animating_position || anim->animating_alpha || anim->animating_scale; - - // If idle, pop the next block of queued animations! - if (!is_busy && !elem->anim_queue.empty()) { - - while (!elem->anim_queue.empty()) { - queued_animation_t& next = elem->anim_queue.front(); - - // Route the queued data to the active state - if (next.affects_position) { - anim->animating_position = true; - anim->start_x = elem->x; - anim->start_y = elem->y; - anim->end_x = next.to_x; - anim->end_y = next.to_y; - anim->position_anim.progress_sec = 0.0f; - anim->position_anim.duration_sec = next.duration_sec; - anim->position_anim.easing = next.easing; - } - if (next.affects_alpha) { - anim->animating_alpha = true; - anim->start_alpha = elem->alpha; - anim->end_alpha = next.to_alpha; - anim->alpha_anim.progress_sec = 0.0f; - anim->alpha_anim.duration_sec = next.duration_sec; - anim->alpha_anim.easing = next.easing; - // Auto-show if fading in from 0 - if (elem->alpha == 0.0f && next.to_alpha > 0.0f) { - elem->visible = true; - } - } - if (next.affects_scale) { - anim->animating_scale = true; - anim->start_scale = elem->scale; - anim->end_scale = next.to_scale; - anim->scale_anim.progress_sec = 0.0f; - anim->scale_anim.duration_sec = next.duration_sec; - anim->scale_anim.easing = next.easing; - } - - // Remove it from the queue - elem->anim_queue.erase(elem->anim_queue.begin()); - - // THE MAGIC TRICK: - // Look at the NEXT item. If it exists and wants to be parallel, - // loop around and pop it immediately! Otherwise, break and wait. - if (elem->anim_queue.empty() || !elem->anim_queue.front().parallel) { - break; - } - } - - // Re-evaluate busy status since we just popped new animations - is_busy = anim->animating_position || anim->animating_alpha || anim->animating_scale; - } - - // Skip ticking if nothing is animating at all - if (!is_busy) continue; - - mark_element_dirty(comp, elem); - bool position_changed = false; - - // --- UPDATE ACTIVE ANIMATIONS --- - - // Update Position - if (anim->animating_position) { - anim->position_anim.progress_sec += delta_time_sec; - float t_raw = anim->position_anim.progress_sec / anim->position_anim.duration_sec; - - if (t_raw >= 1.0f) { - elem->x = anim->end_x; - elem->y = anim->end_y; - anim->animating_position = false; - } - else { - float t_eased = apply_easing(anim->position_anim.easing, t_raw); - // Note: using whatever lerp function you have in your engine - elem->x = anim->start_x + (anim->end_x - anim->start_x) * t_eased; - elem->y = anim->start_y + (anim->end_y - anim->start_y) * t_eased; - } - position_changed = true; - } - - // Update Alpha - if (anim->animating_alpha) { - anim->alpha_anim.progress_sec += delta_time_sec; - float t_raw = anim->alpha_anim.progress_sec / anim->alpha_anim.duration_sec; - - if (t_raw >= 1.0f) { - elem->alpha = anim->end_alpha; - if (elem->alpha <= 0.0f) elem->visible = false; - anim->animating_alpha = false; - } - else { - float t_eased = apply_easing(anim->alpha_anim.easing, t_raw); - elem->alpha = anim->start_alpha + (anim->end_alpha - anim->start_alpha) * t_eased; - } - } - - // Update Scale - if (anim->animating_scale) { - anim->scale_anim.progress_sec += delta_time_sec; - float t_raw = anim->scale_anim.progress_sec / anim->scale_anim.duration_sec; - - if (t_raw >= 1.0f) { - elem->scale = anim->end_scale; - anim->animating_scale = false; - } - else { - float t_eased = apply_easing(anim->scale_anim.easing, t_raw); - elem->scale = anim->start_scale + (anim->end_scale - anim->start_scale) * t_eased; - } - position_changed = true; - } - - // Cleanup empty state marker - if (!anim->animating_position && !anim->animating_alpha && !anim->animating_scale) { - anim->type = ANIM_NONE; - } - - // Update spatial hash - if (position_changed) { - clear_spatial_hash(comp, elem->id); - update_spatial_hash(comp, elem); - } - else { - // Just redrawing pixels, didn't move - mark_element_dirty(comp, elem); - } - } - } - -// Allocates and initializes a new compositor instance. -compositor_t* compositor_init(uint32_t width, uint32_t height) { - // Allocate the structure using C++ new - compositor_t* comp = new (std::nothrow) compositor_t(); - - // Cannot set error if initial allocation fails - if (!comp) return nullptr; - - try { - comp->width = width; - comp->height = height; - comp->framebuffer.resize(width * height, 0xFF000000); // Initialize to opaque black - - // Initialize Tiling - comp->tiles_x = (width + TILE_SIZE - 1) / TILE_SIZE; - comp->tiles_y = (height + TILE_SIZE - 1) / TILE_SIZE; - uint32_t total_tiles = comp->tiles_x * comp->tiles_y; - - comp->dirty_tiles.resize(total_tiles, 1); // Mark all dirty initially - comp->tile_map.resize(total_tiles); - - } - catch (const std::bad_alloc&) { - // Handle allocation failure during initialization of vectors - delete comp; - return nullptr; - } - - return comp; -} - -// Frees all memory associated with the compositor instance. -void compositor_destroy(compositor_t* comp) { - if (!comp) return; - delete comp; // Uses the default C++ destructor for cleanup -} - -// Inserts a new element into the compositor and returns a unique ID. -element_handle_t compositor_insert(compositor_t* comp, - const uint32_t* fb, - uint32_t fb_width, - uint32_t fb_height, - int32_t x, int32_t y, - int32_t z_order, float scale, - anchor_point_t anchor) { - if (!comp) { - // Cannot set error if comp is NULL - return 0; - } - if (!fb) { - return 0; - } - - // Generate unique ID - uint32_t id = comp->next_id++; - if (id == 0) { // Wrap around safety - comp->next_id = 1; - id = comp->next_id++; - } - - // Create and store the new element in the map - element_t new_elem = { - .fb = fb, - .fb_width = fb_width, - .fb_height = fb_height, - .x = x, - .y = y, - .z_order = z_order, - .visible = true, - .valid = true, - .scale = scale, - .anchor = anchor, - .id = id - }; - comp->elements_map[id] = new_elem; - - // Update the sorted draw order - update_draw_order(comp, id); - - // Update spatial hash and mark tiles dirty - update_spatial_hash(comp, &comp->elements_map[id]); // Pass pointer to map value - return id; -} - -// Removes an element from the compositor. -bool compositor_remove(compositor_t* comp, element_handle_t id) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - // Mark area dirty before removal (to force background redraw) - mark_element_dirty(comp, elem); - - // Remove from draw order - auto it = std::find(comp->draw_order.begin(), comp->draw_order.end(), id); - if (it != comp->draw_order.end()) { - comp->draw_order.erase(it); - } - - // Remove from spatial hash - clear_spatial_hash(comp, id); - - // Mark as invalid before erasing - elem->valid = false; - - // Remove from element map - comp->elements_map.erase(id); - - return true; -} - -// setter for an element's (x, y) position. -bool compositor_set_position(compositor_t* comp, element_handle_t id, int32_t x, int32_t y) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - // Mark old area dirty and clear spatial hash - mark_element_dirty(comp, elem); - clear_spatial_hash(comp, id); - elem->x = x; - elem->y = y; - // Update spatial hash and mark new area dirty - update_spatial_hash(comp, elem); - - return true; -} - -// setter for an element's z-order. -bool compositor_set_zorder(compositor_t* comp, element_handle_t id, int32_t z_order) { - if (!comp) return false; - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - if (elem->z_order != z_order) { - // Must clear from spatial hash BEFORE changing z_order to safely re-insert - mark_element_dirty(comp, elem); - clear_spatial_hash(comp, id); - - elem->z_order = z_order; - update_draw_order(comp, id); - - // Re-add to spatial hash, which will now insert it using the new z_order - update_spatial_hash(comp, elem); - } - - return true; -} - -// setter for an element's visibility. -bool compositor_set_visible(compositor_t* comp, element_handle_t id, bool visible) { - if (!comp) return false; - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - if (elem->visible != visible) { - elem->visible = visible; - if (visible) { - update_spatial_hash(comp, elem); - } - else { - mark_element_dirty(comp, elem); - clear_spatial_hash(comp, id); - } - } - - return true; -} - -// setter for an element's scale. -bool compositor_set_scale(compositor_t* comp, element_handle_t id, float scale) { - if (!comp) return false; - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - if (scale <= 0.0f) { - return false; - } - if (elem->scale == scale) { - return true; - } - mark_element_dirty(comp, elem); - clear_spatial_hash(comp, id); - elem->scale = scale; - update_spatial_hash(comp, elem); - - return true; -} - -// setter for an element's alpha. -bool compositor_set_alpha(compositor_t* comp, element_handle_t id, float alpha) { - if (!comp) return false; - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - elem->alpha = std::max(0.0f, std::min(1.0f, alpha)); - - // If we're making it visible from invisible, it might need hashing. - if (elem->alpha > 0.0f && !elem->visible) { - elem->visible = true; - update_spatial_hash(comp, elem); - } - // If it's now invisible, mark it dirty - else if (elem->alpha == 0.0f) { - mark_element_dirty(comp, elem); - // Note: we don't clear the hash, as fade-out animations - // will handle setting visible=false on completion. - // This setter is for instant changes. - } - - return true; -} - -// setter for an element's anchor point. -bool compositor_set_anchor(compositor_t* comp, element_handle_t id, anchor_point_t anchor) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - if (elem->anchor == anchor) { - return true; - } - - // Changing anchor changes bounds if scale != 1.0, so treat it like a move/scale. - mark_element_dirty(comp, elem); - clear_spatial_hash(comp, id); - - elem->anchor = anchor; - - update_spatial_hash(comp, elem); - - return true; -} - -bool compositor_animate_move(compositor_t* comp, element_handle_t id, - int32_t to_x, int32_t to_y, - float duration_sec, easing_t easing, bool queue, bool parallel) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - if (queue) { - queued_animation_t queued; - - queued.type = ANIM_MOVE; - queued.easing = easing; - queued.duration_sec = duration_sec; - queued.to_x = to_x; - queued.to_y = to_y; - queued.is_move_fade = false; - queued.parallel = parallel; - queued.affects_position = true; - queued.affects_scale = false; - queued.affects_alpha = false; - elem->anim_queue.push_back(queued); - - return true; - } - - // Parallel execution with independent timing - elem->anim.start_x = elem->x; - elem->anim.start_y = elem->y; - elem->anim.end_x = to_x; - elem->anim.end_y = to_y; - elem->anim.animating_position = true; - - // Set position-specific timing - elem->anim.position_anim.progress_sec = 0.0f; - elem->anim.position_anim.duration_sec = duration_sec; - elem->anim.position_anim.easing = easing; - - if (elem->anim.type == ANIM_NONE) { - elem->anim.type = ANIM_MOVE; - } - - return true; -} - -bool compositor_animate_scale(compositor_t* comp, element_handle_t id, - float to_scale, float duration_sec, easing_t easing, bool queue, bool parallel) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - if (queue) { - queued_animation_t queued; - - queued.type = ANIM_SCALE; - queued.easing = easing; - queued.duration_sec = duration_sec; - queued.to_scale = to_scale; - queued.is_move_fade = false; - queued.parallel = parallel; - queued.affects_position = false; - queued.affects_scale = true; - queued.affects_alpha = false; - elem->anim_queue.push_back(queued); - - return true; - } - - // Parallel execution with independent timing - elem->anim.start_scale = elem->scale; - elem->anim.end_scale = to_scale; - elem->anim.animating_scale = true; - - // Set scale-specific timing - elem->anim.scale_anim.progress_sec = 0.0f; - elem->anim.scale_anim.duration_sec = duration_sec; - elem->anim.scale_anim.easing = easing; - - if (elem->anim.type == ANIM_NONE) { - elem->anim.type = ANIM_SCALE; - } - - return true; -} - -bool compositor_animate_fade(compositor_t* comp, element_handle_t id, - float to_alpha, float duration_sec, easing_t easing, bool queue, bool parallel) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - to_alpha = std::max(0.0f, std::min(1.0f, to_alpha)); - - if (queue) { - queued_animation_t queued; - - queued.type = ANIM_FADE; - queued.easing = easing; - queued.duration_sec = duration_sec; - queued.to_alpha = to_alpha; - queued.is_move_fade = false; - queued.parallel = parallel; - queued.affects_position = false; - queued.affects_scale = false; - queued.affects_alpha = true; - elem->anim_queue.push_back(queued); - - return true; - } - - // Parallel execution with independent timing - if (elem->alpha == 0.0f && to_alpha > 0.0f) { - elem->visible = true; - } - - elem->anim.start_alpha = elem->alpha; - elem->anim.end_alpha = to_alpha; - elem->anim.animating_alpha = true; - - // Set alpha-specific timing - elem->anim.alpha_anim.progress_sec = 0.0f; - elem->anim.alpha_anim.duration_sec = duration_sec; - elem->anim.alpha_anim.easing = easing; - - if (elem->anim.type == ANIM_NONE) { - elem->anim.type = ANIM_FADE; - } - - return true; -} - -// Finish all or up to num_queued queued animations for a specific element -bool compositor_animation_finish_element(compositor_t* comp, element_handle_t id, int num_queued) { - if (!comp) { - return false; - } - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - mark_element_dirty(comp, elem); - bool position_changed = false; - - // Complete current animation - if (elem->anim.type != ANIM_NONE) { - if (elem->anim.animating_position) { - elem->x = elem->anim.end_x; - elem->y = elem->anim.end_y; - position_changed = true; - } - if (elem->anim.animating_alpha) { - elem->alpha = elem->anim.end_alpha; - if (elem->alpha == 0.0f) elem->visible = false; - } - if (elem->anim.animating_scale) { - elem->scale = elem->anim.end_scale; - position_changed = true; - } - } - - // Complete up to num_queued queued animations (or all if num_queued==0) - int count = 0; - auto it = elem->anim_queue.begin(); - while (it != elem->anim_queue.end() && (num_queued == 0 || count < num_queued)) { - const auto& queued = *it; - if (queued.affects_position) { - elem->x = queued.to_x; - elem->y = queued.to_y; - position_changed = true; - } - if (queued.affects_alpha) { - elem->alpha = queued.to_alpha; - if (elem->alpha == 0.0f) elem->visible = false; - } - if (queued.affects_scale) { - elem->scale = queued.to_scale; - position_changed = true; - } - ++count; - ++it; - } - // Erase only the completed queued animations - elem->anim_queue.erase(elem->anim_queue.begin(), it); - - // Clear current animation state - elem->anim.type = ANIM_NONE; - elem->anim.animating_position = false; - elem->anim.animating_scale = false; - elem->anim.animating_alpha = false; - - // If num_queued==0, clear all queued animations - if (num_queued == 0) { - elem->anim_queue.clear(); - } - - // Update spatial hash - if (position_changed) { - clear_spatial_hash(comp, elem->id); - update_spatial_hash(comp, elem); - } else { - mark_element_dirty(comp, elem); - } - - return true; -} - -// Finish all animations (current and queued) for all elements -void compositor_animation_finish_all(compositor_t* comp) { - if (!comp) return; - - // Iterate over all elements - for (auto& pair : comp->elements_map) { - element_t* elem = &pair.second; - - mark_element_dirty(comp, elem); - bool position_changed = false; - - // Complete current animation - if (elem->anim.type != ANIM_NONE) { - if (elem->anim.animating_position) { - elem->x = elem->anim.end_x; - elem->y = elem->anim.end_y; - position_changed = true; - } - if (elem->anim.animating_alpha) { - elem->alpha = elem->anim.end_alpha; - if (elem->alpha == 0.0f) elem->visible = false; - } - if (elem->anim.animating_scale) { - elem->scale = elem->anim.end_scale; - position_changed = true; - } - } - - // Complete all queued animations - for (const auto& queued : elem->anim_queue) { - if (queued.affects_position) { - elem->x = queued.to_x; - elem->y = queued.to_y; - position_changed = true; - } - if (queued.affects_alpha) { - elem->alpha = queued.to_alpha; - if (elem->alpha == 0.0f) elem->visible = false; - } - if (queued.affects_scale) { - elem->scale = queued.to_scale; - position_changed = true; - } - } - - // Clear animations - elem->anim.type = ANIM_NONE; - elem->anim.animating_position = false; - elem->anim.animating_scale = false; - elem->anim.animating_alpha = false; - elem->anim_queue.clear(); - - // Update spatial hash - if (position_changed) { - clear_spatial_hash(comp, elem->id); - update_spatial_hash(comp, elem); - } else { - mark_element_dirty(comp, elem); - } - } -} - -// Forces all screen tiles to be redrawn on the next compose pass. -void compositor_mark_all_dirty(compositor_t* comp) { - if (!comp) return; - std::fill(comp->dirty_tiles.begin(), comp->dirty_tiles.end(), (unsigned char)1); -} - -// Marks the tiles occupied by a specific element as dirty. -bool compositor_mark_content_dirty(compositor_t* comp, element_handle_t id) { - if (!comp) { - return false; - } - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - // The helper function handles bounds and visibility check. - mark_element_dirty(comp, elem); - - return true; -} - -// Returns true if any element has an active animation or queued animations. -bool compositor_is_animating(compositor_t* comp) { - if (!comp) return false; - for (const auto& pair : comp->elements_map) { - const element_t& elem = pair.second; - const animation_t& anim = elem.anim; - - // Active animation (type set) or any per-property animating flags - if (anim.type != ANIM_NONE) return true; - if (anim.animating_position || anim.animating_scale || anim.animating_alpha) return true; - - // Pending queued animations - if (!elem.anim_queue.empty()) return true; - } - return false; -} - -// Returns true if the specified element is animating or has queued animations. -bool compositor_is_element_animating(compositor_t* comp, element_handle_t id) { - if (!comp) return false; - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - const animation_t& anim = elem->anim; - // Active animation (type set) or any per-property animating flags - if (anim.type != ANIM_NONE) return true; - if (anim.animating_position || anim.animating_scale || anim.animating_alpha) return true; - // Pending queued animations - if (!elem->anim_queue.empty()) return true; - return false; -} - -// Renders all dirty tiles and returns a pointer to the main framebuffer. -const uint32_t* compositor_compose(compositor_t* comp) { - if (!comp) return nullptr; - - // OpenMP 2.0 requires signed integers for loop counters - int total_tiles = (int)(comp->tiles_y * comp->tiles_x); - - // Uncomment when ready to test threading again! - //#pragma omp parallel for schedule(dynamic, 8) - for (int i = 0; i < total_tiles; i++) { - uint32_t ty = (uint32_t)(i / comp->tiles_x); - uint32_t tx = (uint32_t)(i % comp->tiles_x); - uint32_t tile_idx = (uint32_t)i; - - // Skip if tile is not dirty - if (!comp->dirty_tiles[tile_idx]) continue; - - // Tile boundaries - uint32_t tile_x0 = tx * TILE_SIZE; - uint32_t tile_y0 = ty * TILE_SIZE; - uint32_t tile_x1 = std::min(tile_x0 + TILE_SIZE, comp->width); - uint32_t tile_y1 = std::min(tile_y0 + TILE_SIZE, comp->height); - - // FAST CLEAR: Use std::fill per row for contiguous memory writes - for (uint32_t y = tile_y0; y < tile_y1; y++) { - uint32_t* row_ptr = &comp->framebuffer[y * comp->width + tile_x0]; - std::fill(row_ptr, row_ptr + (tile_x1 - tile_x0), 0xFF000000); - } - - // SPATIAL HASH: Only process elements that actually touch this tile - const std::vector& local_draw_list = comp->tile_map[tile_idx]; - - // If nothing is in this tile, we're done here - if (local_draw_list.empty()) { - comp->dirty_tiles[tile_idx] = 0; - continue; - } - - // Draw only the local elements - for (uint32_t element_id : local_draw_list) { - const element_t* elem = find_element(comp, element_id); - if (!elem || !elem->valid || !elem->visible || elem->alpha == 0.0f) continue; - - int32_t elem_x0, elem_y0, elem_x1, elem_y1; - get_element_bounds(elem, elem_x0, elem_y0, elem_x1, elem_y1); - - int32_t blend_x0 = std::max((int32_t)tile_x0, elem_x0); - int32_t blend_y0 = std::max((int32_t)tile_y0, elem_y0); - int32_t blend_x1 = std::min((int32_t)tile_x1, elem_x1); - int32_t blend_y1 = std::min((int32_t)tile_y1, elem_y1); - - // Setup math variables outside the loop - float inv_scale = 1.0f / elem->scale; - bool is_1to1_scale = (inv_scale == 1.0f); - uint32_t elem_a_u8 = (uint32_t)(elem->alpha * 255.0f); - - // 16.16 Fixed-point scale factor - int32_t inv_scale_fp = (int32_t)(inv_scale * 65536.0f); - - // BRANCH HOISTING: Separate loops for 1:1 vs scaled rendering - if (is_1to1_scale) { - for (int32_t y = blend_y0; y < blend_y1; y++) { - int32_t src_y = y - elem_y0; - const uint32_t* src_row = elem->fb + (src_y * elem->fb_width); - uint32_t* dst_row = &comp->framebuffer[y * comp->width]; - - for (int32_t x = blend_x0; x < blend_x1; x++) { - int32_t src_x = x - elem_x0; - - uint32_t src = src_row[src_x]; - uint32_t src_a_raw = (src >> 24) & 0xFF; - - // Bitshift division approximation - uint32_t mod_a = (src_a_raw * elem_a_u8) >> 8; - - if (mod_a >= 254) { // Effectively opaque - dst_row[x] = (src & 0x00FFFFFF) | 0xFF000000; - continue; - } - if (mod_a == 0) continue; - - uint32_t dst = dst_row[x]; - uint32_t inv_a = 255 - mod_a; - - uint32_t src_r = (src >> 16) & 0xFF; - uint32_t src_g = (src >> 8) & 0xFF; - uint32_t src_b = src & 0xFF; - - uint32_t dst_r = (dst >> 16) & 0xFF; - uint32_t dst_g = (dst >> 8) & 0xFF; - uint32_t dst_b = dst & 0xFF; - - // Bitshift division for final blend - uint32_t out_r = (src_r * mod_a + dst_r * inv_a) >> 8; - uint32_t out_g = (src_g * mod_a + dst_g * inv_a) >> 8; - uint32_t out_b = (src_b * mod_a + dst_b * inv_a) >> 8; - - dst_row[x] = 0xFF000000 | (out_r << 16) | (out_g << 8) | out_b; - } - } - } - else { - for (int32_t y = blend_y0; y < blend_y1; y++) { - // Fixed point math for Y - int32_t src_y = ((y - elem_y0) * inv_scale_fp) >> 16; - const uint32_t* src_row = elem->fb + (src_y * elem->fb_width); - uint32_t* dst_row = &comp->framebuffer[y * comp->width]; - - for (int32_t x = blend_x0; x < blend_x1; x++) { - // Fixed point math for X - int32_t src_x = ((x - elem_x0) * inv_scale_fp) >> 16; - - uint32_t src = src_row[src_x]; - uint32_t src_a_raw = (src >> 24) & 0xFF; - - uint32_t mod_a = (src_a_raw * elem_a_u8) >> 8; - - if (mod_a >= 254) { - dst_row[x] = (src & 0x00FFFFFF) | 0xFF000000; - continue; - } - if (mod_a == 0) continue; - - uint32_t dst = dst_row[x]; - uint32_t inv_a = 255 - mod_a; - - uint32_t src_r = (src >> 16) & 0xFF; - uint32_t src_g = (src >> 8) & 0xFF; - uint32_t src_b = src & 0xFF; - - uint32_t dst_r = (dst >> 16) & 0xFF; - uint32_t dst_g = (dst >> 8) & 0xFF; - uint32_t dst_b = dst & 0xFF; - - uint32_t out_r = (src_r * mod_a + dst_r * inv_a) >> 8; - uint32_t out_g = (src_g * mod_a + dst_g * inv_a) >> 8; - uint32_t out_b = (src_b * mod_a + dst_b * inv_a) >> 8; - - dst_row[x] = 0xFF000000 | (out_r << 16) | (out_g << 8) | out_b; - } - } - } - } - - // Clean tile flag safely (distinct threads write to distinct tiles) - comp->dirty_tiles[tile_idx] = 0; - } - - return comp->framebuffer.data(); -} - -} // extern "C" +#include +#include +#include +#include +#include +#include +#include + +// Define the tile size +#define TILE_SIZE 64 + +struct property_anim_t { + float progress_sec; + float duration_sec; + easing_t easing; +}; + +// Animation types +enum animation_type_t { + ANIM_NONE = 0, + ANIM_MOVE, + ANIM_FADE, + ANIM_SCALE, + ANIM_MOVE_FADE // Combined animation +}; + +struct animation_t { + animation_type_t type = ANIM_NONE; + + // Start/End state + int32_t start_x = 0, start_y = 0; + int32_t end_x = 0, end_y = 0; + + float start_scale = 1.0f; + float end_scale = 1.0f; + + float start_alpha = 1.0f; + float end_alpha = 1.0f; + + // Per-property animation state + property_anim_t position_anim; + property_anim_t scale_anim; + property_anim_t alpha_anim; + + // Track which properties are being animated + bool animating_position = false; + bool animating_scale = false; + bool animating_alpha = false; +}; + +// Queued animation structure +struct queued_animation_t { + animation_type_t type; + easing_t easing; + float duration_sec; + + // Target values + int32_t to_x, to_y; + float to_scale; + float to_alpha; + + bool is_move_fade; + + // Which properties this animation affects + bool affects_position; + bool affects_scale; + bool affects_alpha; + bool parallel = false; +}; + +struct element_t { + const uint32_t* fb; // Format ARGB8888 + uint32_t fb_width; + uint32_t fb_height; + int32_t x, y; + int32_t z_order; // Used for draw order + bool visible; + bool valid; // Do we still exist? + float scale; + float alpha = 1.0f; + anchor_point_t anchor; // Anchor point for positioning and scaling + uint32_t id; // Unique identifier + std::vector occupied_tiles; // List of occupied tiles + animation_t anim; + std::vector anim_queue; +}; + +struct compositor_t { + // Main Framebuffer + std::vector framebuffer; + uint32_t width; + uint32_t height; + + // Element Management + // Storage: Map unique ID to element data (O(1) lookup) + std::unordered_map elements_map; + uint32_t next_id = 1; // Unique ID counter (0 is reserved/invalid) + + // Draw Order: Vector of IDs, maintained sorted by z_order + std::vector draw_order; + + // Tiling System (Spatial Hashing) + uint32_t tiles_x; + uint32_t tiles_y; + // dirty_tiles: Flags which tiles need redrawing + std::vector dirty_tiles; + // tile_map: List of element IDs that overlap each tile + std::vector> tile_map; +}; + +// Linear interpolation (lerp) +static float lerp(float a, float b, float t) { + return a + (b - a) * t; +} + +// Applies the selected easing function to a time value +static float apply_easing(easing_t easing, float t) { + // (t is a value from 0.0 to 1.0) + switch (easing) { + case EASE_IN_QUAD: + return t * t; + case EASE_OUT_QUAD: + return 1.0f - (1.0f - t) * (1.0f - t); + case EASE_IN_OUT_QUAD: + return t < 0.5f ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) / 2.0f; + case EASE_IN_CUBIC: + return t * t * t; + case EASE_OUT_CUBIC: + return 1.0f - std::pow(1.0f - t, 3.0f); + case EASE_IN_OUT_CUBIC: + return t < 0.5f ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f; + case EASE_LINEAR: + default: + return t; + } +} + +// Finds the element structure corresponding to a given ID, or nullptr if not found. +static element_t* find_element(compositor_t* comp, uint32_t id) { + auto it = comp->elements_map.find(id); + if (it == comp->elements_map.end()) { + return nullptr; + } + if (!it->second.valid) { + return nullptr; + } + return &it->second; +} + +// Calculates the final screen-space bounding box for an element (THE source of truth). +static inline void get_element_bounds(const element_t* elem, + int32_t& out_x0, int32_t& out_y0, + int32_t& out_x1, int32_t& out_y1) { + // Calculate scaled dimensions, rounding to nearest integer to prevent jitter + int32_t scaled_width = (int32_t)std::round(elem->fb_width * elem->scale); + int32_t scaled_height = (int32_t)std::round(elem->fb_height * elem->scale); + + float anchor_x_factor = 0.0f; + float anchor_y_factor = 0.0f; + + // Determine the anchor's proportional position within the element + switch (elem->anchor) { + case ANCHOR_TOP_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 0.0f; break; + case ANCHOR_TOP_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 0.0f; break; + case ANCHOR_TOP_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 0.0f; break; + case ANCHOR_CENTER_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 0.5f; break; + case ANCHOR_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 0.5f; break; + case ANCHOR_CENTER_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 0.5f; break; + case ANCHOR_BOTTOM_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 1.0f; break; + case ANCHOR_BOTTOM_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 1.0f; break; + case ANCHOR_BOTTOM_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 1.0f; break; + } + + // The element's (x, y) represents the anchor point's position on screen. + // We calculate the top-left corner (out_x0, out_y0) by offsetting + // from the anchor's position. + out_x0 = elem->x - (int32_t)std::round(scaled_width * anchor_x_factor); + out_y0 = elem->y - (int32_t)std::round(scaled_height * anchor_y_factor); + + // The bottom-right corner is the top-left plus the scaled dimensions. + out_x1 = out_x0 + scaled_width; + out_y1 = out_y0 + scaled_height; +} + +// Marks all tiles an element occupies as dirty. +static void mark_element_dirty(compositor_t* comp, const element_t* elem) { + if (!elem || !elem->visible) return; + + // Get bounds from our new single source of truth + int32_t x0, y0, x1, y1; + get_element_bounds(elem, x0, y0, x1, y1); + + // Calculate bounding box in tile coordinates + int32_t tile_x0 = x0 / TILE_SIZE; + int32_t tile_y0 = y0 / TILE_SIZE; + int32_t tile_x1 = (x1 + TILE_SIZE - 1) / TILE_SIZE; + int32_t tile_y1 = (y1 + TILE_SIZE - 1) / TILE_SIZE; + + // Clamp tile coordinates to bounds + tile_x0 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x0)); + tile_y0 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y0)); + tile_x1 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x1)); + tile_y1 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y1)); + + // Mark tiles as dirty + for (int32_t ty = tile_y0; ty < tile_y1; ty++) { + for (int32_t tx = tile_x0; tx < tile_x1; tx++) { + comp->dirty_tiles[ty * comp->tiles_x + tx] = 1; + } + } +} + +// Removes an element's ID from all tiles in the spatial hash map. +static void clear_spatial_hash(compositor_t* comp, uint32_t id) { + element_t* elem = find_element(comp, id); + if (!elem) return; + + for (uint32_t tile_idx : elem->occupied_tiles) { + if (tile_idx >= comp->tile_map.size()) continue; + auto& tile_list = comp->tile_map[tile_idx]; + tile_list.erase(std::remove(tile_list.begin(), tile_list.end(), id), tile_list.end()); + } + + elem->occupied_tiles.clear(); +} + +// Updates an element's position in the spatial hash map and marks its new tiles as dirty. +static void update_spatial_hash(compositor_t* comp, element_t* elem) { + if (!elem || !elem->visible) return; + + mark_element_dirty(comp, elem); // Mark new area + elem->occupied_tiles.clear(); + + // Get bounds from our new single source of truth + int32_t x0, y0, x1, y1; + get_element_bounds(elem, x0, y0, x1, y1); + + // Convert to tile coordinates + int32_t tile_x0 = x0 / TILE_SIZE; + int32_t tile_y0 = y0 / TILE_SIZE; + int32_t tile_x1 = (x1 + TILE_SIZE - 1) / TILE_SIZE; + int32_t tile_y1 = (y1 + TILE_SIZE - 1) / TILE_SIZE; + + // Clamp + tile_x0 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x0)); + tile_y0 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y0)); + tile_x1 = std::max(0, std::min((int32_t)comp->tiles_x, tile_x1)); + tile_y1 = std::max(0, std::min((int32_t)comp->tiles_y, tile_y1)); + + // Add to spatial hash and record tiles + for (int32_t ty = tile_y0; ty < tile_y1; ty++) { + for (int32_t tx = tile_x0; tx < tile_x1; tx++) { + uint32_t tile_idx = ty * comp->tiles_x + tx; + auto& tile_list = comp->tile_map[tile_idx]; + + auto it_insert = std::lower_bound( + tile_list.begin(), + tile_list.end(), + elem->z_order, + [comp](uint32_t current_id, int32_t target_z) { + return comp->elements_map[current_id].z_order < target_z; + } + ); + + tile_list.insert(it_insert, elem->id); + elem->occupied_tiles.push_back(tile_idx); + } + } +} + +// Uses an incremental insertion sort to place an element in the correct z-order position. +static void update_draw_order(compositor_t* comp, uint32_t id) { + element_t* elem = find_element(comp, id); + if (!elem) return; + + // Remove element from its current position in draw_order + auto it_to_remove = std::find(comp->draw_order.begin(), comp->draw_order.end(), id); + if (it_to_remove != comp->draw_order.end()) { + comp->draw_order.erase(it_to_remove); + } + + // Find the new correct position using binary search logic (lower_bound) + auto it_insert = std::lower_bound( + comp->draw_order.begin(), + comp->draw_order.end(), + elem->z_order, + [comp](uint32_t current_id, int32_t target_z) { + // Find the z_order of the element corresponding to current_id + auto it = comp->elements_map.find(current_id); + if (it == comp->elements_map.end()) return true; // Safety break + return it->second.z_order < target_z; + } + ); + + // Insert the ID at the new position + comp->draw_order.insert(it_insert, id); +} + +extern "C" { + +// Advances all animations by a given time delta. + void compositor_tick(compositor_t* comp, float delta_time_sec) { + if (!comp) return; + + for (auto& pair : comp->elements_map) { + element_t* elem = &pair.second; + if (!elem->valid) continue; + animation_t* anim = &elem->anim; + + // Are we currently busy doing ANYTHING? + bool is_busy = anim->animating_position || anim->animating_alpha || anim->animating_scale; + + // If idle, pop the next block of queued animations! + if (!is_busy && !elem->anim_queue.empty()) { + + while (!elem->anim_queue.empty()) { + queued_animation_t& next = elem->anim_queue.front(); + + // Route the queued data to the active state + if (next.affects_position) { + anim->animating_position = true; + anim->start_x = elem->x; + anim->start_y = elem->y; + anim->end_x = next.to_x; + anim->end_y = next.to_y; + anim->position_anim.progress_sec = 0.0f; + anim->position_anim.duration_sec = next.duration_sec; + anim->position_anim.easing = next.easing; + } + if (next.affects_alpha) { + anim->animating_alpha = true; + anim->start_alpha = elem->alpha; + anim->end_alpha = next.to_alpha; + anim->alpha_anim.progress_sec = 0.0f; + anim->alpha_anim.duration_sec = next.duration_sec; + anim->alpha_anim.easing = next.easing; + // Auto-show if fading in from 0 + if (elem->alpha == 0.0f && next.to_alpha > 0.0f) { + elem->visible = true; + } + } + if (next.affects_scale) { + anim->animating_scale = true; + anim->start_scale = elem->scale; + anim->end_scale = next.to_scale; + anim->scale_anim.progress_sec = 0.0f; + anim->scale_anim.duration_sec = next.duration_sec; + anim->scale_anim.easing = next.easing; + } + + // Remove it from the queue + elem->anim_queue.erase(elem->anim_queue.begin()); + + // THE MAGIC TRICK: + // Look at the NEXT item. If it exists and wants to be parallel, + // loop around and pop it immediately! Otherwise, break and wait. + if (elem->anim_queue.empty() || !elem->anim_queue.front().parallel) { + break; + } + } + + // Re-evaluate busy status since we just popped new animations + is_busy = anim->animating_position || anim->animating_alpha || anim->animating_scale; + } + + // Skip ticking if nothing is animating at all + if (!is_busy) continue; + + mark_element_dirty(comp, elem); + bool position_changed = false; + + // --- UPDATE ACTIVE ANIMATIONS --- + + // Update Position + if (anim->animating_position) { + anim->position_anim.progress_sec += delta_time_sec; + float t_raw = anim->position_anim.progress_sec / anim->position_anim.duration_sec; + + if (t_raw >= 1.0f) { + elem->x = anim->end_x; + elem->y = anim->end_y; + anim->animating_position = false; + } + else { + float t_eased = apply_easing(anim->position_anim.easing, t_raw); + // Note: using whatever lerp function you have in your engine + elem->x = anim->start_x + (anim->end_x - anim->start_x) * t_eased; + elem->y = anim->start_y + (anim->end_y - anim->start_y) * t_eased; + } + position_changed = true; + } + + // Update Alpha + if (anim->animating_alpha) { + anim->alpha_anim.progress_sec += delta_time_sec; + float t_raw = anim->alpha_anim.progress_sec / anim->alpha_anim.duration_sec; + + if (t_raw >= 1.0f) { + elem->alpha = anim->end_alpha; + if (elem->alpha <= 0.0f) elem->visible = false; + anim->animating_alpha = false; + } + else { + float t_eased = apply_easing(anim->alpha_anim.easing, t_raw); + elem->alpha = anim->start_alpha + (anim->end_alpha - anim->start_alpha) * t_eased; + } + } + + // Update Scale + if (anim->animating_scale) { + anim->scale_anim.progress_sec += delta_time_sec; + float t_raw = anim->scale_anim.progress_sec / anim->scale_anim.duration_sec; + + if (t_raw >= 1.0f) { + elem->scale = anim->end_scale; + anim->animating_scale = false; + } + else { + float t_eased = apply_easing(anim->scale_anim.easing, t_raw); + elem->scale = anim->start_scale + (anim->end_scale - anim->start_scale) * t_eased; + } + position_changed = true; + } + + // Cleanup empty state marker + if (!anim->animating_position && !anim->animating_alpha && !anim->animating_scale) { + anim->type = ANIM_NONE; + } + + // Update spatial hash + if (position_changed) { + clear_spatial_hash(comp, elem->id); + update_spatial_hash(comp, elem); + } + else { + // Just redrawing pixels, didn't move + mark_element_dirty(comp, elem); + } + } + } + +// Allocates and initializes a new compositor instance. +compositor_t* compositor_init(uint32_t width, uint32_t height) { + // Allocate the structure using C++ new + compositor_t* comp = new (std::nothrow) compositor_t(); + + // Cannot set error if initial allocation fails + if (!comp) return nullptr; + + try { + comp->width = width; + comp->height = height; + comp->framebuffer.resize(width * height, 0xFF000000); // Initialize to opaque black + + // Initialize Tiling + comp->tiles_x = (width + TILE_SIZE - 1) / TILE_SIZE; + comp->tiles_y = (height + TILE_SIZE - 1) / TILE_SIZE; + uint32_t total_tiles = comp->tiles_x * comp->tiles_y; + + comp->dirty_tiles.resize(total_tiles, 1); // Mark all dirty initially + comp->tile_map.resize(total_tiles); + + } + catch (const std::bad_alloc&) { + // Handle allocation failure during initialization of vectors + delete comp; + return nullptr; + } + + return comp; +} + +// Frees all memory associated with the compositor instance. +void compositor_destroy(compositor_t* comp) { + if (!comp) return; + delete comp; // Uses the default C++ destructor for cleanup +} + +// Inserts a new element into the compositor and returns a unique ID. +element_handle_t compositor_insert(compositor_t* comp, + const uint32_t* fb, + uint32_t fb_width, + uint32_t fb_height, + int32_t x, int32_t y, + int32_t z_order, float scale, + anchor_point_t anchor) { + if (!comp) { + // Cannot set error if comp is NULL + return 0; + } + if (!fb) { + return 0; + } + + // Generate unique ID + uint32_t id = comp->next_id++; + if (id == 0) { // Wrap around safety + comp->next_id = 1; + id = comp->next_id++; + } + + // Create and store the new element in the map + element_t new_elem = { + .fb = fb, + .fb_width = fb_width, + .fb_height = fb_height, + .x = x, + .y = y, + .z_order = z_order, + .visible = true, + .valid = true, + .scale = scale, + .anchor = anchor, + .id = id + }; + comp->elements_map[id] = new_elem; + + // Update the sorted draw order + update_draw_order(comp, id); + + // Update spatial hash and mark tiles dirty + update_spatial_hash(comp, &comp->elements_map[id]); // Pass pointer to map value + return id; +} + +// Removes an element from the compositor. +bool compositor_remove(compositor_t* comp, element_handle_t id) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + // Mark area dirty before removal (to force background redraw) + mark_element_dirty(comp, elem); + + // Remove from draw order + auto it = std::find(comp->draw_order.begin(), comp->draw_order.end(), id); + if (it != comp->draw_order.end()) { + comp->draw_order.erase(it); + } + + // Remove from spatial hash + clear_spatial_hash(comp, id); + + // Mark as invalid before erasing + elem->valid = false; + + // Remove from element map + comp->elements_map.erase(id); + + return true; +} + +// setter for an element's (x, y) position. +bool compositor_set_position(compositor_t* comp, element_handle_t id, int32_t x, int32_t y) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + // Mark old area dirty and clear spatial hash + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + elem->x = x; + elem->y = y; + // Update spatial hash and mark new area dirty + update_spatial_hash(comp, elem); + + return true; +} + +// setter for an element's z-order. +bool compositor_set_zorder(compositor_t* comp, element_handle_t id, int32_t z_order) { + if (!comp) return false; + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + if (elem->z_order != z_order) { + // Must clear from spatial hash BEFORE changing z_order to safely re-insert + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + + elem->z_order = z_order; + update_draw_order(comp, id); + + // Re-add to spatial hash, which will now insert it using the new z_order + update_spatial_hash(comp, elem); + } + + return true; +} + +// setter for an element's visibility. +bool compositor_set_visible(compositor_t* comp, element_handle_t id, bool visible) { + if (!comp) return false; + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + if (elem->visible != visible) { + elem->visible = visible; + if (visible) { + update_spatial_hash(comp, elem); + } + else { + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + } + } + + return true; +} + +// setter for an element's scale. +bool compositor_set_scale(compositor_t* comp, element_handle_t id, float scale) { + if (!comp) return false; + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + if (scale <= 0.0f) { + return false; + } + if (elem->scale == scale) { + return true; + } + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + elem->scale = scale; + update_spatial_hash(comp, elem); + + return true; +} + +// setter for an element's alpha. +bool compositor_set_alpha(compositor_t* comp, element_handle_t id, float alpha) { + if (!comp) return false; + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + elem->alpha = std::max(0.0f, std::min(1.0f, alpha)); + + // If we're making it visible from invisible, it might need hashing. + if (elem->alpha > 0.0f && !elem->visible) { + elem->visible = true; + update_spatial_hash(comp, elem); + } + // If it's now invisible, mark it dirty + else if (elem->alpha == 0.0f) { + mark_element_dirty(comp, elem); + // Note: we don't clear the hash, as fade-out animations + // will handle setting visible=false on completion. + // This setter is for instant changes. + } + + return true; +} + +// setter for an element's anchor point. +bool compositor_set_anchor(compositor_t* comp, element_handle_t id, anchor_point_t anchor) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + if (elem->anchor == anchor) { + return true; + } + + // Changing anchor changes bounds if scale != 1.0, so treat it like a move/scale. + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + + elem->anchor = anchor; + + update_spatial_hash(comp, elem); + + return true; +} + +bool compositor_animate_move(compositor_t* comp, element_handle_t id, + int32_t to_x, int32_t to_y, + float duration_sec, easing_t easing, bool queue, bool parallel) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + if (queue) { + queued_animation_t queued; + + queued.type = ANIM_MOVE; + queued.easing = easing; + queued.duration_sec = duration_sec; + queued.to_x = to_x; + queued.to_y = to_y; + queued.is_move_fade = false; + queued.parallel = parallel; + queued.affects_position = true; + queued.affects_scale = false; + queued.affects_alpha = false; + elem->anim_queue.push_back(queued); + + return true; + } + + // Parallel execution with independent timing + elem->anim.start_x = elem->x; + elem->anim.start_y = elem->y; + elem->anim.end_x = to_x; + elem->anim.end_y = to_y; + elem->anim.animating_position = true; + + // Set position-specific timing + elem->anim.position_anim.progress_sec = 0.0f; + elem->anim.position_anim.duration_sec = duration_sec; + elem->anim.position_anim.easing = easing; + + if (elem->anim.type == ANIM_NONE) { + elem->anim.type = ANIM_MOVE; + } + + return true; +} + +bool compositor_animate_scale(compositor_t* comp, element_handle_t id, + float to_scale, float duration_sec, easing_t easing, bool queue, bool parallel) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + if (queue) { + queued_animation_t queued; + + queued.type = ANIM_SCALE; + queued.easing = easing; + queued.duration_sec = duration_sec; + queued.to_scale = to_scale; + queued.is_move_fade = false; + queued.parallel = parallel; + queued.affects_position = false; + queued.affects_scale = true; + queued.affects_alpha = false; + elem->anim_queue.push_back(queued); + + return true; + } + + // Parallel execution with independent timing + elem->anim.start_scale = elem->scale; + elem->anim.end_scale = to_scale; + elem->anim.animating_scale = true; + + // Set scale-specific timing + elem->anim.scale_anim.progress_sec = 0.0f; + elem->anim.scale_anim.duration_sec = duration_sec; + elem->anim.scale_anim.easing = easing; + + if (elem->anim.type == ANIM_NONE) { + elem->anim.type = ANIM_SCALE; + } + + return true; +} + +bool compositor_animate_fade(compositor_t* comp, element_handle_t id, + float to_alpha, float duration_sec, easing_t easing, bool queue, bool parallel) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + to_alpha = std::max(0.0f, std::min(1.0f, to_alpha)); + + if (queue) { + queued_animation_t queued; + + queued.type = ANIM_FADE; + queued.easing = easing; + queued.duration_sec = duration_sec; + queued.to_alpha = to_alpha; + queued.is_move_fade = false; + queued.parallel = parallel; + queued.affects_position = false; + queued.affects_scale = false; + queued.affects_alpha = true; + elem->anim_queue.push_back(queued); + + return true; + } + + // Parallel execution with independent timing + if (elem->alpha == 0.0f && to_alpha > 0.0f) { + elem->visible = true; + } + + elem->anim.start_alpha = elem->alpha; + elem->anim.end_alpha = to_alpha; + elem->anim.animating_alpha = true; + + // Set alpha-specific timing + elem->anim.alpha_anim.progress_sec = 0.0f; + elem->anim.alpha_anim.duration_sec = duration_sec; + elem->anim.alpha_anim.easing = easing; + + if (elem->anim.type == ANIM_NONE) { + elem->anim.type = ANIM_FADE; + } + + return true; +} + +// Finish all or up to num_queued queued animations for a specific element +bool compositor_animation_finish_element(compositor_t* comp, element_handle_t id, int num_queued) { + if (!comp) { + return false; + } + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + mark_element_dirty(comp, elem); + bool position_changed = false; + + // Complete current animation + if (elem->anim.type != ANIM_NONE) { + if (elem->anim.animating_position) { + elem->x = elem->anim.end_x; + elem->y = elem->anim.end_y; + position_changed = true; + } + if (elem->anim.animating_alpha) { + elem->alpha = elem->anim.end_alpha; + if (elem->alpha == 0.0f) elem->visible = false; + } + if (elem->anim.animating_scale) { + elem->scale = elem->anim.end_scale; + position_changed = true; + } + } + + // Complete up to num_queued queued animations (or all if num_queued==0) + int count = 0; + auto it = elem->anim_queue.begin(); + while (it != elem->anim_queue.end() && (num_queued == 0 || count < num_queued)) { + const auto& queued = *it; + if (queued.affects_position) { + elem->x = queued.to_x; + elem->y = queued.to_y; + position_changed = true; + } + if (queued.affects_alpha) { + elem->alpha = queued.to_alpha; + if (elem->alpha == 0.0f) elem->visible = false; + } + if (queued.affects_scale) { + elem->scale = queued.to_scale; + position_changed = true; + } + ++count; + ++it; + } + // Erase only the completed queued animations + elem->anim_queue.erase(elem->anim_queue.begin(), it); + + // Clear current animation state + elem->anim.type = ANIM_NONE; + elem->anim.animating_position = false; + elem->anim.animating_scale = false; + elem->anim.animating_alpha = false; + + // If num_queued==0, clear all queued animations + if (num_queued == 0) { + elem->anim_queue.clear(); + } + + // Update spatial hash + if (position_changed) { + clear_spatial_hash(comp, elem->id); + update_spatial_hash(comp, elem); + } else { + mark_element_dirty(comp, elem); + } + + return true; +} + +// Finish all animations (current and queued) for all elements +void compositor_animation_finish_all(compositor_t* comp) { + if (!comp) return; + + // Iterate over all elements + for (auto& pair : comp->elements_map) { + element_t* elem = &pair.second; + + mark_element_dirty(comp, elem); + bool position_changed = false; + + // Complete current animation + if (elem->anim.type != ANIM_NONE) { + if (elem->anim.animating_position) { + elem->x = elem->anim.end_x; + elem->y = elem->anim.end_y; + position_changed = true; + } + if (elem->anim.animating_alpha) { + elem->alpha = elem->anim.end_alpha; + if (elem->alpha == 0.0f) elem->visible = false; + } + if (elem->anim.animating_scale) { + elem->scale = elem->anim.end_scale; + position_changed = true; + } + } + + // Complete all queued animations + for (const auto& queued : elem->anim_queue) { + if (queued.affects_position) { + elem->x = queued.to_x; + elem->y = queued.to_y; + position_changed = true; + } + if (queued.affects_alpha) { + elem->alpha = queued.to_alpha; + if (elem->alpha == 0.0f) elem->visible = false; + } + if (queued.affects_scale) { + elem->scale = queued.to_scale; + position_changed = true; + } + } + + // Clear animations + elem->anim.type = ANIM_NONE; + elem->anim.animating_position = false; + elem->anim.animating_scale = false; + elem->anim.animating_alpha = false; + elem->anim_queue.clear(); + + // Update spatial hash + if (position_changed) { + clear_spatial_hash(comp, elem->id); + update_spatial_hash(comp, elem); + } else { + mark_element_dirty(comp, elem); + } + } +} + +// Forces all screen tiles to be redrawn on the next compose pass. +void compositor_mark_all_dirty(compositor_t* comp) { + if (!comp) return; + std::fill(comp->dirty_tiles.begin(), comp->dirty_tiles.end(), (unsigned char)1); +} + +// Marks the tiles occupied by a specific element as dirty. +bool compositor_mark_content_dirty(compositor_t* comp, element_handle_t id) { + if (!comp) { + return false; + } + + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + + // The helper function handles bounds and visibility check. + mark_element_dirty(comp, elem); + + return true; +} + +// Returns true if any element has an active animation or queued animations. +bool compositor_is_animating(compositor_t* comp) { + if (!comp) return false; + for (const auto& pair : comp->elements_map) { + const element_t& elem = pair.second; + const animation_t& anim = elem.anim; + + // Active animation (type set) or any per-property animating flags + if (anim.type != ANIM_NONE) return true; + if (anim.animating_position || anim.animating_scale || anim.animating_alpha) return true; + + // Pending queued animations + if (!elem.anim_queue.empty()) return true; + } + return false; +} + +// Returns true if the specified element is animating or has queued animations. +bool compositor_is_element_animating(compositor_t* comp, element_handle_t id) { + if (!comp) return false; + element_t* elem = find_element(comp, id); + if (!elem) { + return false; + } + const animation_t& anim = elem->anim; + // Active animation (type set) or any per-property animating flags + if (anim.type != ANIM_NONE) return true; + if (anim.animating_position || anim.animating_scale || anim.animating_alpha) return true; + // Pending queued animations + if (!elem->anim_queue.empty()) return true; + return false; +} + +// Renders all dirty tiles and returns a pointer to the main framebuffer. +const uint32_t* compositor_compose(compositor_t* comp) { + if (!comp) return nullptr; + + // OpenMP 2.0 requires signed integers for loop counters + int total_tiles = (int)(comp->tiles_y * comp->tiles_x); + + // Uncomment when ready to test threading again! + //#pragma omp parallel for schedule(dynamic, 8) + for (int i = 0; i < total_tiles; i++) { + uint32_t ty = (uint32_t)(i / comp->tiles_x); + uint32_t tx = (uint32_t)(i % comp->tiles_x); + uint32_t tile_idx = (uint32_t)i; + + // Skip if tile is not dirty + if (!comp->dirty_tiles[tile_idx]) continue; + + // Tile boundaries + uint32_t tile_x0 = tx * TILE_SIZE; + uint32_t tile_y0 = ty * TILE_SIZE; + uint32_t tile_x1 = std::min(tile_x0 + TILE_SIZE, comp->width); + uint32_t tile_y1 = std::min(tile_y0 + TILE_SIZE, comp->height); + + // FAST CLEAR: Use std::fill per row for contiguous memory writes + for (uint32_t y = tile_y0; y < tile_y1; y++) { + uint32_t* row_ptr = &comp->framebuffer[y * comp->width + tile_x0]; + std::fill(row_ptr, row_ptr + (tile_x1 - tile_x0), 0xFF000000); + } + + // SPATIAL HASH: Only process elements that actually touch this tile + const std::vector& local_draw_list = comp->tile_map[tile_idx]; + + // If nothing is in this tile, we're done here + if (local_draw_list.empty()) { + comp->dirty_tiles[tile_idx] = 0; + continue; + } + + // Draw only the local elements + for (uint32_t element_id : local_draw_list) { + const element_t* elem = find_element(comp, element_id); + if (!elem || !elem->valid || !elem->visible || elem->alpha == 0.0f) continue; + + int32_t elem_x0, elem_y0, elem_x1, elem_y1; + get_element_bounds(elem, elem_x0, elem_y0, elem_x1, elem_y1); + + int32_t blend_x0 = std::max((int32_t)tile_x0, elem_x0); + int32_t blend_y0 = std::max((int32_t)tile_y0, elem_y0); + int32_t blend_x1 = std::min((int32_t)tile_x1, elem_x1); + int32_t blend_y1 = std::min((int32_t)tile_y1, elem_y1); + + // Setup math variables outside the loop + float inv_scale = 1.0f / elem->scale; + bool is_1to1_scale = (inv_scale == 1.0f); + uint32_t elem_a_u8 = (uint32_t)(elem->alpha * 255.0f); + + // 16.16 Fixed-point scale factor + int32_t inv_scale_fp = (int32_t)(inv_scale * 65536.0f); + + // BRANCH HOISTING: Separate loops for 1:1 vs scaled rendering + if (is_1to1_scale) { + for (int32_t y = blend_y0; y < blend_y1; y++) { + int32_t src_y = y - elem_y0; + const uint32_t* src_row = elem->fb + (src_y * elem->fb_width); + uint32_t* dst_row = &comp->framebuffer[y * comp->width]; + + for (int32_t x = blend_x0; x < blend_x1; x++) { + int32_t src_x = x - elem_x0; + + uint32_t src = src_row[src_x]; + uint32_t src_a_raw = (src >> 24) & 0xFF; + + // Bitshift division approximation + uint32_t mod_a = (src_a_raw * elem_a_u8) >> 8; + + if (mod_a >= 254) { // Effectively opaque + dst_row[x] = (src & 0x00FFFFFF) | 0xFF000000; + continue; + } + if (mod_a == 0) continue; + + uint32_t dst = dst_row[x]; + uint32_t inv_a = 255 - mod_a; + + uint32_t src_r = (src >> 16) & 0xFF; + uint32_t src_g = (src >> 8) & 0xFF; + uint32_t src_b = src & 0xFF; + + uint32_t dst_r = (dst >> 16) & 0xFF; + uint32_t dst_g = (dst >> 8) & 0xFF; + uint32_t dst_b = dst & 0xFF; + + // Bitshift division for final blend + uint32_t out_r = (src_r * mod_a + dst_r * inv_a) >> 8; + uint32_t out_g = (src_g * mod_a + dst_g * inv_a) >> 8; + uint32_t out_b = (src_b * mod_a + dst_b * inv_a) >> 8; + + dst_row[x] = 0xFF000000 | (out_r << 16) | (out_g << 8) | out_b; + } + } + } + else { + for (int32_t y = blend_y0; y < blend_y1; y++) { + // Fixed point math for Y + int32_t src_y = ((y - elem_y0) * inv_scale_fp) >> 16; + const uint32_t* src_row = elem->fb + (src_y * elem->fb_width); + uint32_t* dst_row = &comp->framebuffer[y * comp->width]; + + for (int32_t x = blend_x0; x < blend_x1; x++) { + // Fixed point math for X + int32_t src_x = ((x - elem_x0) * inv_scale_fp) >> 16; + + uint32_t src = src_row[src_x]; + uint32_t src_a_raw = (src >> 24) & 0xFF; + + uint32_t mod_a = (src_a_raw * elem_a_u8) >> 8; + + if (mod_a >= 254) { + dst_row[x] = (src & 0x00FFFFFF) | 0xFF000000; + continue; + } + if (mod_a == 0) continue; + + uint32_t dst = dst_row[x]; + uint32_t inv_a = 255 - mod_a; + + uint32_t src_r = (src >> 16) & 0xFF; + uint32_t src_g = (src >> 8) & 0xFF; + uint32_t src_b = src & 0xFF; + + uint32_t dst_r = (dst >> 16) & 0xFF; + uint32_t dst_g = (dst >> 8) & 0xFF; + uint32_t dst_b = dst & 0xFF; + + uint32_t out_r = (src_r * mod_a + dst_r * inv_a) >> 8; + uint32_t out_g = (src_g * mod_a + dst_g * inv_a) >> 8; + uint32_t out_b = (src_b * mod_a + dst_b * inv_a) >> 8; + + dst_row[x] = 0xFF000000 | (out_r << 16) | (out_g << 8) | out_b; + } + } + } + } + + // Clean tile flag safely (distinct threads write to distinct tiles) + comp->dirty_tiles[tile_idx] = 0; + } + + return comp->framebuffer.data(); +} + +} // extern "C" diff --git a/src/image.cpp b/src/image.cpp index e26d5bc..5892b31 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -1,74 +1,74 @@ -#pragma warning(push, 0) -#define STB_IMAGE_IMPLEMENTATION -#include -#define STB_IMAGE_RESIZE_IMPLEMENTATION -#include -#pragma warning(pop) -#include -#include -#include -#include - -extern "C" { -uint32_t* image_load(const char* filename, int* width, int* height) { - int n; - unsigned char* data = stbi_load(filename, width, height, &n, 4); // force 4 channels (RGBA) - if (!data) return nullptr; - - // Convert RGBA to ARGB - uint32_t* out = (uint32_t*)malloc(*width * *height * sizeof(uint32_t)); - if (!out) { - stbi_image_free(data); - return nullptr; - } - for (int i = 0; i < *width * *height; ++i) { - uint8_t r = data[4*i + 0]; - uint8_t g = data[4*i + 1]; - uint8_t b = data[4*i + 2]; - uint8_t a = data[4*i + 3]; - out[i] = (a << 24) | (r << 16) | (g << 8) | b; - } - stbi_image_free(data); - return out; -} - -uint32_t* image_load_resized(const char* filename, int desired_width, int desired_height) { - int original_width, original_height, n; - unsigned char* data = stbi_load(filename, &original_width, &original_height, &n, 4); - if (!data) return nullptr; - - // Allocate buffer for resized image - uint32_t* resized = (uint32_t*)malloc(desired_width * desired_height * 4); - if (!resized) { - stbi_image_free(data); - return nullptr; - } - - // Resize the image - stbir_resize_uint8_linear(data, original_width, original_height, 0, - (unsigned char* )resized, desired_width, desired_height, 0, - STBIR_RGBA); - - stbi_image_free(data); - - // In-place conversion from RGBA to ARGB - // We iterate backwards to ensure we don't overwrite bytes before reading them, - // though since we are working within the same 4-byte boundaries, forward works too! - for (int i = 0; i < desired_width * desired_height; ++i) { - uint8_t* pixel = (uint8_t*)&resized[i]; - uint8_t r = pixel[0]; - uint8_t g = pixel[1]; - uint8_t b = pixel[2]; - uint8_t a = pixel[3]; - - resized[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; - } - - return resized; -} - -void image_free(uint32_t* data) { - free(data); -} - -} // extern "C" +#pragma warning(push, 0) +#define STB_IMAGE_IMPLEMENTATION +#include +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include +#pragma warning(pop) +#include +#include +#include +#include + +extern "C" { +uint32_t* image_load(const char* filename, int* width, int* height) { + int n; + unsigned char* data = stbi_load(filename, width, height, &n, 4); // force 4 channels (RGBA) + if (!data) return nullptr; + + // Convert RGBA to ARGB + uint32_t* out = (uint32_t*)malloc(*width * *height * sizeof(uint32_t)); + if (!out) { + stbi_image_free(data); + return nullptr; + } + for (int i = 0; i < *width * *height; ++i) { + uint8_t r = data[4*i + 0]; + uint8_t g = data[4*i + 1]; + uint8_t b = data[4*i + 2]; + uint8_t a = data[4*i + 3]; + out[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + stbi_image_free(data); + return out; +} + +uint32_t* image_load_resized(const char* filename, int desired_width, int desired_height) { + int original_width, original_height, n; + unsigned char* data = stbi_load(filename, &original_width, &original_height, &n, 4); + if (!data) return nullptr; + + // Allocate buffer for resized image + uint32_t* resized = (uint32_t*)malloc(desired_width * desired_height * 4); + if (!resized) { + stbi_image_free(data); + return nullptr; + } + + // Resize the image + stbir_resize_uint8_linear(data, original_width, original_height, 0, + (unsigned char* )resized, desired_width, desired_height, 0, + STBIR_RGBA); + + stbi_image_free(data); + + // In-place conversion from RGBA to ARGB + // We iterate backwards to ensure we don't overwrite bytes before reading them, + // though since we are working within the same 4-byte boundaries, forward works too! + for (int i = 0; i < desired_width * desired_height; ++i) { + uint8_t* pixel = (uint8_t*)&resized[i]; + uint8_t r = pixel[0]; + uint8_t g = pixel[1]; + uint8_t b = pixel[2]; + uint8_t a = pixel[3]; + + resized[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + } + + return resized; +} + +void image_free(uint32_t* data) { + free(data); +} + +} // extern "C" diff --git a/src/main.cpp b/src/main.cpp index 5249ee7..44494be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,659 +1,659 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// RAII handles -// --------------------------------------------------------------------------- - -// A generic RAII wrapper. -template -struct EngineHandle { - T* ptr; - - // Constructor - EngineHandle(T* p = nullptr) : ptr(p) {} - - // Destructor - ~EngineHandle() { if (ptr) DestroyFn(ptr); } - - // DELETE Copy Constructor (Prevents accidental cloning) - EngineHandle(const EngineHandle&) = delete; - - // DELETE Copy Assignment (Prevents accidental overwrites) - EngineHandle& operator=(const EngineHandle&) = delete; - - // ALLOW Move Constructor (Safely transfer ownership) - EngineHandle(EngineHandle&& other) noexcept : ptr(other.ptr) { - other.ptr = nullptr; // Nullify the old pointer so it doesn't get destroyed - } - - // Move Assignment using Swap - EngineHandle& operator=(EngineHandle&& other) noexcept { - T* temp = other.ptr; - other.ptr = ptr; - ptr = temp; - return *this; - } - - // Implicit conversion operator - operator T* () const { return ptr; } -}; - -using WindowHandle = EngineHandle; -using CompositorHandle = EngineHandle; -using TextHandle = EngineHandle; -using AudioHandle = EngineHandle; -using AudioDataHandle = EngineHandle; -using ImageHandle = EngineHandle; - -// --------------------------------------------------------------------------- -// Script interpreter -// --------------------------------------------------------------------------- - -// A command is an opcode plus a closure that does the actual work. -// Keeping the closure allows the command set to directly capture backend -// pointers without a fat context struct threaded through everything. -// -// The closure returns true when the interpreter should YIELD (stop executing -// commands this frame and wait). Returning false means continue immediately -// to the next command. - -struct Command { - std::string opcode; // for debugging - std::function execute; -}; - -enum class WaitKind { - NONE, - CLICK, // wait for mouse click or space - ANIM_ALL, // wait until compositor_is_animating() == false - ANIM_ELEM, // wait until a specific element stops animating - END, // script finished, keep rendering until told to quit -}; - -class Interpreter { -public: - std::vector script; - size_t pc = 0; - - WaitKind waiting = WaitKind::NONE; - element_handle_t wait_elem = 0; // valid when waiting == ANIM_ELEM - - // Callbacks into the engine, set before first tick - compositor_t* compositor = nullptr; - text_t* dialogue = nullptr; - text_t* name_box = nullptr; - - // Advance the interpreter by one frame. - // clicked: true on the frame a left-click or spacebar press was detected. - // Returns true when the script has fully ended and the engine may quit. - bool tick(bool clicked) { - - // ---- resolve current wait condition -------------------------------- - switch (waiting) { - - case WaitKind::CLICK: - if (clicked) { - // If the typewriter is still running, snap it and stay on - // this line - let the player see the full text first. - if (text_is_animating(dialogue)) { - text_show_all_current(dialogue); - return false; - } - waiting = WaitKind::NONE; - } - else { - return false; - } - break; - - case WaitKind::ANIM_ALL: - if (compositor_is_animating(compositor)) - return false; - waiting = WaitKind::NONE; - break; - - case WaitKind::ANIM_ELEM: - if (compositor_is_element_animating(compositor, wait_elem)) - return false; - waiting = WaitKind::NONE; - break; - - case WaitKind::END: - return true; - - case WaitKind::NONE: - break; - } - - // ---- burn through commands until a yield --------------------------- - while (pc < script.size()) { - const Command& cmd = script[pc++]; - bool yield = cmd.execute(); - if (yield) return false; - } - - // Ran off the end of the script without an explicit END. - waiting = WaitKind::END; - return true; - } -}; - -// --------------------------------------------------------------------------- -// Script builder helpers -// -// These push Command objects onto the script vector. They capture the -// interpreter and backend pointers by reference/value as appropriate. -// All helpers return void and operate on a passed-in script vector so the -// build function stays readable. -// --------------------------------------------------------------------------- - -using Script = std::vector; - -static void cmd_speaker(Script& s, text_t* name_box, - element_handle_t name_id, compositor_t* comp, - const char* speaker) -{ - s.push_back({ "SPEAKER", [=]() -> bool { - text_clear(name_box); - if (speaker && speaker[0]) - text_push(name_box, speaker, 0.0f, true); - compositor_mark_content_dirty(comp, name_id); - return false; - } }); -} - -static void cmd_line(Script& s, text_t* dialogue, - element_handle_t dialogue_id, compositor_t* comp, - const char* text, float speed = 30.0f) -{ - s.push_back({ "LINE", [=]() -> bool { - text_clear(dialogue); - text_push(dialogue, text, speed, true); - compositor_mark_content_dirty(comp, dialogue_id); - return false; - } }); -} - -static void cmd_clear(Script& s, text_t* dialogue, - element_handle_t dialogue_id, compositor_t* comp) -{ - s.push_back({ "CLEAR", [=]() -> bool { - text_clear(dialogue); - compositor_mark_content_dirty(comp, dialogue_id); - return false; - } }); -} - -static void cmd_wait_click(Script& s, Interpreter& interp) -{ - s.push_back({ "WAIT_CLICK", [&interp]() -> bool { - interp.waiting = WaitKind::CLICK; - return true; - } }); -} - -static void cmd_wait_anim_all(Script& s, Interpreter& interp) -{ - s.push_back({ "WAIT_ANIM_ALL", [&interp]() -> bool { - interp.waiting = WaitKind::ANIM_ALL; - return true; - } }); -} - -static void cmd_wait_anim_elem(Script& s, Interpreter& interp, - element_handle_t elem) -{ - s.push_back({ "WAIT_ANIM_ELEM", [&interp, elem]() -> bool { - interp.waiting = WaitKind::ANIM_ELEM; - interp.wait_elem = elem; - return true; - } }); -} - -static void cmd_finish_all(Script& s, compositor_t* comp) -{ - s.push_back({ "ANIM_FINISH_ALL", [=]() -> bool { - compositor_animation_finish_all(comp); - return false; - } }); -} - -static void cmd_anim_scale(Script& s, compositor_t* comp, - element_handle_t elem, float to, - float dur, easing_t ease, bool queue, bool parallel) -{ - s.push_back({ "ANIM_SCALE", [=]() -> bool { - compositor_animate_scale(comp, elem, to, dur, ease, queue, parallel); - return false; - } }); -} - -static void cmd_anim_move(Script& s, compositor_t* comp, - element_handle_t elem, - int32_t x, int32_t y, - float dur, easing_t ease, bool queue, bool parallel) -{ - s.push_back({ "ANIM_MOVE", [=]() -> bool { - compositor_animate_move(comp, elem, x, y, dur, ease, queue, parallel); - return false; - } }); -} - -static void cmd_anim_fade(Script& s, compositor_t* comp, - element_handle_t elem, float to_alpha, - float dur, easing_t ease, bool queue, bool parallel) -{ - s.push_back({ "ANIM_FADE", [=]() -> bool { - compositor_animate_fade(comp, elem, to_alpha, dur, ease, queue, parallel); - return false; - } }); -} - -static void cmd_set_visible(Script& s, compositor_t* comp, - element_handle_t elem, bool visible) -{ - s.push_back({ "ELEM_SHOW/HIDE", [=]() -> bool { - compositor_set_visible(comp, elem, visible); - return false; - } }); -} - -static void cmd_set_scale(Script& s, compositor_t* comp, - element_handle_t elem, float scale) -{ - s.push_back({ "ELEM_SET_SCALE", [=]() -> bool { - compositor_set_scale(comp, elem, scale); - return false; - } }); -} - -static void cmd_set_alpha(Script& s, compositor_t* comp, - element_handle_t elem, float alpha) -{ - s.push_back({ "ELEM_SET_ALPHA", [=]() -> bool { - compositor_set_alpha(comp, elem, alpha); - return false; - } }); -} - -static void cmd_audio_fade(Script& s, audio_t* audio, - audio_stream_id_t id, - float to_vol, float dur) -{ - s.push_back({ "MUSIC_FADE", [=]() -> bool { - audio_animate_volume(audio, id, to_vol, dur); - return false; - } }); -} - -static void cmd_end(Script& s, Interpreter& interp) -{ - s.push_back({ "END", [&interp]() -> bool { - interp.waiting = WaitKind::END; - return true; - } }); -} - -// --------------------------------------------------------------------------- -// main -// --------------------------------------------------------------------------- - -int main() { - srand((unsigned int)time(NULL)); - - const int WIDTH = 1280; - const int HEIGHT = 720; - const int CHAR_WIDTH = 400; - const int CHAR_HEIGHT = 600; - - const int CENTER_X = WIDTH / 2; - const int CHAR_BASELINE_Y = HEIGHT - 140; - const int LEFT_CHAR_CENTER_X = 60 + (CHAR_WIDTH / 2); - const int RIGHT_CHAR_CENTER_X = WIDTH - 60 - (CHAR_WIDTH / 2); - const int OFFSCREEN_LEFT_X = LEFT_CHAR_CENTER_X - 660; - const int OFFSCREEN_RIGHT_X = RIGHT_CHAR_CENTER_X + 660; - - // ---- window / compositor / audio --------------------------------------- - - WindowHandle window(window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine")); - if (!window) { printf("Failed to create window\n"); return 1; } - - CompositorHandle compositor(compositor_init(WIDTH, HEIGHT)); - if (!compositor) { printf("Failed to initialize compositor\n"); return 1; } - - AudioHandle audio(audio_init()); - if (!audio) { printf("Failed to initialize audio\n"); return 1; } - - audio_set_global_volume(audio, 1.0f); - - typedef struct { - const char* title; - const char* composer; - const char* file; - } music_track_t; - - static const music_track_t strauss_tracks[] = { - {"Freikugeln", "Johann Strauss II", "assets/Freikugeln.wav"}, - {"Perpetuum Mobile", "Johann Strauss II", "assets/Perpetuum_Mobile.wav"}, - {"Ohne Sorgen!", "Josef Strauss", "assets/Ohne_Sorgen!.wav"}, - {"Auf Ferienreisen", "Josef Strauss", "assets/Auf_Ferienreisen.wav"}, - {"Dynamiden", "Josef Strauss", "assets/Dynamiden.wav"} - }; - - int track_idx = rand() % (sizeof(strauss_tracks) / sizeof(strauss_tracks[0])); - const music_track_t* track = &strauss_tracks[track_idx]; - - int music_size, music_channels, music_rate, music_total_samples; - AudioDataHandle music_data(audio_load_madpcm( - track->file, - &music_size, &music_channels, &music_rate, &music_total_samples)); - if (!music_data) { printf("Failed to load audio\n"); return 1; } - - audio_stream_id_t music_id = audio_play( - audio, music_data, music_size, music_channels, music_rate, true); - - // ---- assets / compositor elements ------------------------------------- - - ImageHandle bg(image_load_resized("assets/background.png", WIDTH, HEIGHT)); - if (bg) { - element_handle_t bg_id = compositor_insert( - compositor, bg, WIDTH, HEIGHT, 0, 0, 0, 1.0f, ANCHOR_TOP_LEFT); - compositor_set_alpha(compositor, bg_id, 1.0f); - } - - ImageHandle textbox(image_load_resized("assets/textbox.png", WIDTH, 180)); - if (textbox) - compositor_insert(compositor, textbox, WIDTH, 180, - 0, HEIGHT, 3, 1.0f, ANCHOR_BOTTOM_LEFT); - - ImageHandle maksw_happy(image_load_resized("assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT)); - element_handle_t maksw_happy_id = compositor_insert( - compositor, maksw_happy, CHAR_WIDTH, CHAR_HEIGHT, - OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); - - ImageHandle maksw_neutral(image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT)); - element_handle_t maksw_neutral_id = compositor_insert( - compositor, maksw_neutral, CHAR_WIDTH, CHAR_HEIGHT, - LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); - compositor_set_visible(compositor, maksw_neutral_id, false); - - ImageHandle wikor_img(image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT)); - element_handle_t wikor_id = compositor_insert( - compositor, wikor_img, CHAR_WIDTH, CHAR_HEIGHT, - OFFSCREEN_RIGHT_X, CHAR_BASELINE_Y, 2, 1.0f, ANCHOR_BOTTOM_CENTER); - - TextHandle name_text(text_init(300, 100)); - text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f); - element_handle_t name_id = compositor_insert( - compositor, text_getfb(name_text), - 300, 100, 60, HEIGHT - 180, 4, 1.0f, ANCHOR_TOP_LEFT); - - TextHandle dialogue_text(text_init(WIDTH - 140, 160)); - text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f); - element_handle_t dialogue_id = compositor_insert( - compositor, text_getfb(dialogue_text), - WIDTH - 140, 160, 60, HEIGHT - 130, 4, 1.0f, ANCHOR_TOP_LEFT); - - // mask: a wipe graphic, starts off-screen above, slides down to cover - ImageHandle mask(image_load_resized("assets/mask.png", 1280, 1080)); - element_handle_t mask_id = compositor_insert( - compositor, mask, 1280, 1080, 0, -1080, 5, 1.0f, ANCHOR_TOP_LEFT); - compositor_set_visible(compositor, mask_id, false); - - // mask2: slides off downward to reveal the new scene - ImageHandle mask2(image_load_resized("assets/mask2.png", 1280, 1080)); - element_handle_t mask2_id = compositor_insert( - compositor, mask2, 1280, 1080, 0, 0, 5, 1.0f, ANCHOR_TOP_LEFT); - compositor_set_visible(compositor, mask2_id, false); - - // ---- build the script ------------------------------------------------- - // - // Shorthand references so the builder lines stay readable. - compositor_t* comp = compositor; - text_t* dlg = dialogue_text; - text_t* nmb = name_text; - element_handle_t dlg_id = dialogue_id; - - Interpreter interp; - interp.compositor = comp; - interp.dialogue = dlg; - interp.name_box = nmb; - - Script& S = interp.script; - - // Convenience aliases so each script line stays short. - // All lambdas below capture by value (element handles, pointers) which is - // safe because all resources outlive the interpreter. - - auto speaker = [&](const char* who) { - cmd_speaker(S, nmb, name_id, comp, who); - }; - auto line = [&](const char* text, float speed = 30.0f) { - cmd_line(S, dlg, dlg_id, comp, text, speed); - }; - auto clear = [&]() { cmd_clear(S, dlg, dlg_id, comp); }; - auto wait_click = [&]() { cmd_wait_click(S, interp); }; - auto wait_anim_all = [&]() { cmd_wait_anim_all(S, interp); }; - auto wait_anim = [&](element_handle_t e) { cmd_wait_anim_elem(S, interp, e); }; - auto finish_all = [&]() { cmd_finish_all(S, comp); }; - auto anim_scale = [&](element_handle_t e, float to, float dur, - easing_t ease, bool q = false, bool p = false) { - cmd_anim_scale(S, comp, e, to, dur, ease, q, p); - }; - auto anim_move = [&](element_handle_t e, int32_t x, int32_t y, - float dur, easing_t ease, bool q = false, bool p = false) { - cmd_anim_move(S, comp, e, x, y, dur, ease, q, p); - }; - auto anim_fade = [&](element_handle_t e, float a, float dur, - easing_t ease, bool q = false, bool p = false) { - cmd_anim_fade(S, comp, e, a, dur, ease, q, p); - }; - auto show = [&](element_handle_t e) { cmd_set_visible(S, comp, e, true); }; - auto hide = [&](element_handle_t e) { cmd_set_visible(S, comp, e, false); }; - auto scale = [&](element_handle_t e, float v) { cmd_set_scale(S, comp, e, v); }; - auto alpha = [&](element_handle_t e, float v) { cmd_set_alpha(S, comp, e, v); }; - - // ------------------------------------------------------------------ - // Scene 1 - maksw slides in, introduces the engine - // ------------------------------------------------------------------ - - anim_move(maksw_happy_id, CENTER_X, CHAR_BASELINE_Y, - 1.5f, EASE_OUT_CUBIC, true); - anim_scale(maksw_happy_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC, /*queue=*/true, /*parallel=*/false); - speaker("maksw"); - line("Hello and welcome to the Obwarzanek Narrative Engine demo!"); - wait_click(); - - line("Today I'll present to you-"); - wait_click(); - - anim_move(wikor_id, RIGHT_CHAR_CENTER_X, CHAR_BASELINE_Y, 0.4f, EASE_OUT_CUBIC); - anim_move(maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y - 20, - 0.3f, EASE_OUT_CUBIC); - anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - - anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("wikor"); - line("Yeah, hello to you too."); - wait_click(); - - const char* greetings[] = { - "Oh Wiktor, what a nice surprise!", - "Oh hey Wiktor, didn't expect to see you!", - "Wiktor! What brings you here?" - }; - - const char* responses[] = { - "No, I was just walking by.", - "Nah, just passing through.", - "Not really, just wandering." - }; - - int greeting_idx = rand() % 3; - int response_idx = rand() % 3; - - anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - anim_scale(maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("maksw"); - line(greetings[greeting_idx]); - wait_click(); - - line("...Do you need anything?"); - wait_click(); - - anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("wikor"); - line(responses[response_idx]); - wait_click(); - - line("Cool engine though."); - wait_click(); - - finish_all(); - show(mask_id); - anim_move(mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC); - wait_anim(mask_id); - - hide(maksw_happy_id); - show(maksw_neutral_id); - scale(maksw_neutral_id, 1.0f); - scale(wikor_id, 1.05f); - - hide(mask_id); - show(mask2_id); - anim_move(mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC); - speaker("wikor"); - clear(); - wait_anim(mask2_id); - line("Anyways, how's your day going?"); - wait_click(); - - // ------------------------------------------------------------------ - // Scene 2 - neutral maksw, wikor is chatty - // ------------------------------------------------------------------ - - finish_all(); - anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("maksw"); - line("You hear what I'm hearing?"); - wait_click(); - - finish_all(); - anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("wikor"); - line("What? That music?"); - wait_click(); - - finish_all(); - anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("maksw"); - char music_line[256]; - snprintf(music_line, sizeof(music_line), - "Yep! %s by %s!", track->title, track->composer); - line(music_line); - wait_click(); - - finish_all(); - anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); - speaker("wikor"); - line("Sure thing..."); - wait_click(); - - finish_all(); - line("Like I know who he is."); - wait_click(); - - // ------------------------------------------------------------------ - // Ending - fade to black, fade out music, then quit - // ------------------------------------------------------------------ - - finish_all(); - anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); - // Reuse mask as a simple alpha-fade overlay (no positional wipe needed) - show(mask_id); - alpha(mask_id, 0.0f); - anim_fade(mask_id, 1.0f, 0.8f, EASE_IN_OUT_CUBIC); - cmd_audio_fade(S, audio, music_id, 0.0f, 0.6f); - wait_anim(mask_id); // wait until black, then exit - cmd_end(S, interp); - - // ---- main loop -------------------------------------------------------- - - bool running = true; - - auto last_frame_time = std::chrono::high_resolution_clock::now(); - auto last_report_time = last_frame_time; - uint64_t frame_count = 0; - float highest_frame_time = 0.0f; - float frame_time_sum = 0.0f; - const std::chrono::duration REPORT_INTERVAL = std::chrono::seconds(1); - - bool mouse_was_down = false; - bool key_was_down = false; - - while (running) { - if (window_pollevent(window)) running = false; - - auto current_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration delta_duration = current_time - last_frame_time; - float dt = delta_duration.count(); - last_frame_time = current_time; - - // FPS reporting - frame_count++; - frame_time_sum += dt; - if (dt > highest_frame_time) highest_frame_time = dt; - std::chrono::duration since_report = current_time - last_report_time; - if (since_report >= REPORT_INTERVAL) { - if (frame_count > 0) { - float fps = 1.0f / (frame_time_sum / (float)frame_count); - printf("FPS: %.2f (over %llu frames in %.2f seconds), highest frame time: %.2fms\n", - fps, frame_count, since_report.count(), highest_frame_time * 1000.0f); - } - frame_count = 0; frame_time_sum = 0.0f; highest_frame_time = 0.0f; - last_report_time = current_time; - } - - // Edge-detect click and keypress - bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT); - bool key_is_down = window_is_key_down(window, KEY_SPACE); - bool clicked = (mouse_is_down && !mouse_was_down) || - (key_is_down && !key_was_down); - mouse_was_down = mouse_is_down; - key_was_down = key_is_down; - - // Tick the interpreter - bool script_done = interp.tick(clicked); - if (script_done) running = false; - - // Tick subsystems - compositor_tick(compositor, dt); - - if (text_nextframe(name_text, dt)) compositor_mark_content_dirty(compositor, name_id); - if (text_nextframe(dialogue_text, dt)) compositor_mark_content_dirty(compositor, dialogue_id); - - const uint32_t* fb = compositor_compose(compositor); - window_blit(window, fb); - audio_tick(audio, dt); - } - - audio_stop(audio, music_id); - return 0; -} +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// RAII handles +// --------------------------------------------------------------------------- + +// A generic RAII wrapper. +template +struct EngineHandle { + T* ptr; + + // Constructor + EngineHandle(T* p = nullptr) : ptr(p) {} + + // Destructor + ~EngineHandle() { if (ptr) DestroyFn(ptr); } + + // DELETE Copy Constructor (Prevents accidental cloning) + EngineHandle(const EngineHandle&) = delete; + + // DELETE Copy Assignment (Prevents accidental overwrites) + EngineHandle& operator=(const EngineHandle&) = delete; + + // ALLOW Move Constructor (Safely transfer ownership) + EngineHandle(EngineHandle&& other) noexcept : ptr(other.ptr) { + other.ptr = nullptr; // Nullify the old pointer so it doesn't get destroyed + } + + // Move Assignment using Swap + EngineHandle& operator=(EngineHandle&& other) noexcept { + T* temp = other.ptr; + other.ptr = ptr; + ptr = temp; + return *this; + } + + // Implicit conversion operator + operator T* () const { return ptr; } +}; + +using WindowHandle = EngineHandle; +using CompositorHandle = EngineHandle; +using TextHandle = EngineHandle; +using AudioHandle = EngineHandle; +using AudioDataHandle = EngineHandle; +using ImageHandle = EngineHandle; + +// --------------------------------------------------------------------------- +// Script interpreter +// --------------------------------------------------------------------------- + +// A command is an opcode plus a closure that does the actual work. +// Keeping the closure allows the command set to directly capture backend +// pointers without a fat context struct threaded through everything. +// +// The closure returns true when the interpreter should YIELD (stop executing +// commands this frame and wait). Returning false means continue immediately +// to the next command. + +struct Command { + std::string opcode; // for debugging + std::function execute; +}; + +enum class WaitKind { + NONE, + CLICK, // wait for mouse click or space + ANIM_ALL, // wait until compositor_is_animating() == false + ANIM_ELEM, // wait until a specific element stops animating + END, // script finished, keep rendering until told to quit +}; + +class Interpreter { +public: + std::vector script; + size_t pc = 0; + + WaitKind waiting = WaitKind::NONE; + element_handle_t wait_elem = 0; // valid when waiting == ANIM_ELEM + + // Callbacks into the engine, set before first tick + compositor_t* compositor = nullptr; + text_t* dialogue = nullptr; + text_t* name_box = nullptr; + + // Advance the interpreter by one frame. + // clicked: true on the frame a left-click or spacebar press was detected. + // Returns true when the script has fully ended and the engine may quit. + bool tick(bool clicked) { + + // ---- resolve current wait condition -------------------------------- + switch (waiting) { + + case WaitKind::CLICK: + if (clicked) { + // If the typewriter is still running, snap it and stay on + // this line - let the player see the full text first. + if (text_is_animating(dialogue)) { + text_show_all_current(dialogue); + return false; + } + waiting = WaitKind::NONE; + } + else { + return false; + } + break; + + case WaitKind::ANIM_ALL: + if (compositor_is_animating(compositor)) + return false; + waiting = WaitKind::NONE; + break; + + case WaitKind::ANIM_ELEM: + if (compositor_is_element_animating(compositor, wait_elem)) + return false; + waiting = WaitKind::NONE; + break; + + case WaitKind::END: + return true; + + case WaitKind::NONE: + break; + } + + // ---- burn through commands until a yield --------------------------- + while (pc < script.size()) { + const Command& cmd = script[pc++]; + bool yield = cmd.execute(); + if (yield) return false; + } + + // Ran off the end of the script without an explicit END. + waiting = WaitKind::END; + return true; + } +}; + +// --------------------------------------------------------------------------- +// Script builder helpers +// +// These push Command objects onto the script vector. They capture the +// interpreter and backend pointers by reference/value as appropriate. +// All helpers return void and operate on a passed-in script vector so the +// build function stays readable. +// --------------------------------------------------------------------------- + +using Script = std::vector; + +static void cmd_speaker(Script& s, text_t* name_box, + element_handle_t name_id, compositor_t* comp, + const char* speaker) +{ + s.push_back({ "SPEAKER", [=]() -> bool { + text_clear(name_box); + if (speaker && speaker[0]) + text_push(name_box, speaker, 0.0f, true); + compositor_mark_content_dirty(comp, name_id); + return false; + } }); +} + +static void cmd_line(Script& s, text_t* dialogue, + element_handle_t dialogue_id, compositor_t* comp, + const char* text, float speed = 30.0f) +{ + s.push_back({ "LINE", [=]() -> bool { + text_clear(dialogue); + text_push(dialogue, text, speed, true); + compositor_mark_content_dirty(comp, dialogue_id); + return false; + } }); +} + +static void cmd_clear(Script& s, text_t* dialogue, + element_handle_t dialogue_id, compositor_t* comp) +{ + s.push_back({ "CLEAR", [=]() -> bool { + text_clear(dialogue); + compositor_mark_content_dirty(comp, dialogue_id); + return false; + } }); +} + +static void cmd_wait_click(Script& s, Interpreter& interp) +{ + s.push_back({ "WAIT_CLICK", [&interp]() -> bool { + interp.waiting = WaitKind::CLICK; + return true; + } }); +} + +static void cmd_wait_anim_all(Script& s, Interpreter& interp) +{ + s.push_back({ "WAIT_ANIM_ALL", [&interp]() -> bool { + interp.waiting = WaitKind::ANIM_ALL; + return true; + } }); +} + +static void cmd_wait_anim_elem(Script& s, Interpreter& interp, + element_handle_t elem) +{ + s.push_back({ "WAIT_ANIM_ELEM", [&interp, elem]() -> bool { + interp.waiting = WaitKind::ANIM_ELEM; + interp.wait_elem = elem; + return true; + } }); +} + +static void cmd_finish_all(Script& s, compositor_t* comp) +{ + s.push_back({ "ANIM_FINISH_ALL", [=]() -> bool { + compositor_animation_finish_all(comp); + return false; + } }); +} + +static void cmd_anim_scale(Script& s, compositor_t* comp, + element_handle_t elem, float to, + float dur, easing_t ease, bool queue, bool parallel) +{ + s.push_back({ "ANIM_SCALE", [=]() -> bool { + compositor_animate_scale(comp, elem, to, dur, ease, queue, parallel); + return false; + } }); +} + +static void cmd_anim_move(Script& s, compositor_t* comp, + element_handle_t elem, + int32_t x, int32_t y, + float dur, easing_t ease, bool queue, bool parallel) +{ + s.push_back({ "ANIM_MOVE", [=]() -> bool { + compositor_animate_move(comp, elem, x, y, dur, ease, queue, parallel); + return false; + } }); +} + +static void cmd_anim_fade(Script& s, compositor_t* comp, + element_handle_t elem, float to_alpha, + float dur, easing_t ease, bool queue, bool parallel) +{ + s.push_back({ "ANIM_FADE", [=]() -> bool { + compositor_animate_fade(comp, elem, to_alpha, dur, ease, queue, parallel); + return false; + } }); +} + +static void cmd_set_visible(Script& s, compositor_t* comp, + element_handle_t elem, bool visible) +{ + s.push_back({ "ELEM_SHOW/HIDE", [=]() -> bool { + compositor_set_visible(comp, elem, visible); + return false; + } }); +} + +static void cmd_set_scale(Script& s, compositor_t* comp, + element_handle_t elem, float scale) +{ + s.push_back({ "ELEM_SET_SCALE", [=]() -> bool { + compositor_set_scale(comp, elem, scale); + return false; + } }); +} + +static void cmd_set_alpha(Script& s, compositor_t* comp, + element_handle_t elem, float alpha) +{ + s.push_back({ "ELEM_SET_ALPHA", [=]() -> bool { + compositor_set_alpha(comp, elem, alpha); + return false; + } }); +} + +static void cmd_audio_fade(Script& s, audio_t* audio, + audio_stream_id_t id, + float to_vol, float dur) +{ + s.push_back({ "MUSIC_FADE", [=]() -> bool { + audio_animate_volume(audio, id, to_vol, dur); + return false; + } }); +} + +static void cmd_end(Script& s, Interpreter& interp) +{ + s.push_back({ "END", [&interp]() -> bool { + interp.waiting = WaitKind::END; + return true; + } }); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +int main() { + srand((unsigned int)time(NULL)); + + const int WIDTH = 1280; + const int HEIGHT = 720; + const int CHAR_WIDTH = 400; + const int CHAR_HEIGHT = 600; + + const int CENTER_X = WIDTH / 2; + const int CHAR_BASELINE_Y = HEIGHT - 140; + const int LEFT_CHAR_CENTER_X = 60 + (CHAR_WIDTH / 2); + const int RIGHT_CHAR_CENTER_X = WIDTH - 60 - (CHAR_WIDTH / 2); + const int OFFSCREEN_LEFT_X = LEFT_CHAR_CENTER_X - 660; + const int OFFSCREEN_RIGHT_X = RIGHT_CHAR_CENTER_X + 660; + + // ---- window / compositor / audio --------------------------------------- + + WindowHandle window(window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine")); + if (!window) { printf("Failed to create window\n"); return 1; } + + CompositorHandle compositor(compositor_init(WIDTH, HEIGHT)); + if (!compositor) { printf("Failed to initialize compositor\n"); return 1; } + + AudioHandle audio(audio_init()); + if (!audio) { printf("Failed to initialize audio\n"); return 1; } + + audio_set_global_volume(audio, 1.0f); + + typedef struct { + const char* title; + const char* composer; + const char* file; + } music_track_t; + + static const music_track_t strauss_tracks[] = { + {"Freikugeln", "Johann Strauss II", "assets/Freikugeln.wav"}, + {"Perpetuum Mobile", "Johann Strauss II", "assets/Perpetuum_Mobile.wav"}, + {"Ohne Sorgen!", "Josef Strauss", "assets/Ohne_Sorgen!.wav"}, + {"Auf Ferienreisen", "Josef Strauss", "assets/Auf_Ferienreisen.wav"}, + {"Dynamiden", "Josef Strauss", "assets/Dynamiden.wav"} + }; + + int track_idx = rand() % (sizeof(strauss_tracks) / sizeof(strauss_tracks[0])); + const music_track_t* track = &strauss_tracks[track_idx]; + + int music_size, music_channels, music_rate, music_total_samples; + AudioDataHandle music_data(audio_load_madpcm( + track->file, + &music_size, &music_channels, &music_rate, &music_total_samples)); + if (!music_data) { printf("Failed to load audio\n"); return 1; } + + audio_stream_id_t music_id = audio_play( + audio, music_data, music_size, music_channels, music_rate, true); + + // ---- assets / compositor elements ------------------------------------- + + ImageHandle bg(image_load_resized("assets/background.png", WIDTH, HEIGHT)); + if (bg) { + element_handle_t bg_id = compositor_insert( + compositor, bg, WIDTH, HEIGHT, 0, 0, 0, 1.0f, ANCHOR_TOP_LEFT); + compositor_set_alpha(compositor, bg_id, 1.0f); + } + + ImageHandle textbox(image_load_resized("assets/textbox.png", WIDTH, 180)); + if (textbox) + compositor_insert(compositor, textbox, WIDTH, 180, + 0, HEIGHT, 3, 1.0f, ANCHOR_BOTTOM_LEFT); + + ImageHandle maksw_happy(image_load_resized("assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT)); + element_handle_t maksw_happy_id = compositor_insert( + compositor, maksw_happy, CHAR_WIDTH, CHAR_HEIGHT, + OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); + + ImageHandle maksw_neutral(image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT)); + element_handle_t maksw_neutral_id = compositor_insert( + compositor, maksw_neutral, CHAR_WIDTH, CHAR_HEIGHT, + LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); + compositor_set_visible(compositor, maksw_neutral_id, false); + + ImageHandle wikor_img(image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT)); + element_handle_t wikor_id = compositor_insert( + compositor, wikor_img, CHAR_WIDTH, CHAR_HEIGHT, + OFFSCREEN_RIGHT_X, CHAR_BASELINE_Y, 2, 1.0f, ANCHOR_BOTTOM_CENTER); + + TextHandle name_text(text_init(300, 100)); + text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f); + element_handle_t name_id = compositor_insert( + compositor, text_getfb(name_text), + 300, 100, 60, HEIGHT - 180, 4, 1.0f, ANCHOR_TOP_LEFT); + + TextHandle dialogue_text(text_init(WIDTH - 140, 160)); + text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f); + element_handle_t dialogue_id = compositor_insert( + compositor, text_getfb(dialogue_text), + WIDTH - 140, 160, 60, HEIGHT - 130, 4, 1.0f, ANCHOR_TOP_LEFT); + + // mask: a wipe graphic, starts off-screen above, slides down to cover + ImageHandle mask(image_load_resized("assets/mask.png", 1280, 1080)); + element_handle_t mask_id = compositor_insert( + compositor, mask, 1280, 1080, 0, -1080, 5, 1.0f, ANCHOR_TOP_LEFT); + compositor_set_visible(compositor, mask_id, false); + + // mask2: slides off downward to reveal the new scene + ImageHandle mask2(image_load_resized("assets/mask2.png", 1280, 1080)); + element_handle_t mask2_id = compositor_insert( + compositor, mask2, 1280, 1080, 0, 0, 5, 1.0f, ANCHOR_TOP_LEFT); + compositor_set_visible(compositor, mask2_id, false); + + // ---- build the script ------------------------------------------------- + // + // Shorthand references so the builder lines stay readable. + compositor_t* comp = compositor; + text_t* dlg = dialogue_text; + text_t* nmb = name_text; + element_handle_t dlg_id = dialogue_id; + + Interpreter interp; + interp.compositor = comp; + interp.dialogue = dlg; + interp.name_box = nmb; + + Script& S = interp.script; + + // Convenience aliases so each script line stays short. + // All lambdas below capture by value (element handles, pointers) which is + // safe because all resources outlive the interpreter. + + auto speaker = [&](const char* who) { + cmd_speaker(S, nmb, name_id, comp, who); + }; + auto line = [&](const char* text, float speed = 30.0f) { + cmd_line(S, dlg, dlg_id, comp, text, speed); + }; + auto clear = [&]() { cmd_clear(S, dlg, dlg_id, comp); }; + auto wait_click = [&]() { cmd_wait_click(S, interp); }; + auto wait_anim_all = [&]() { cmd_wait_anim_all(S, interp); }; + auto wait_anim = [&](element_handle_t e) { cmd_wait_anim_elem(S, interp, e); }; + auto finish_all = [&]() { cmd_finish_all(S, comp); }; + auto anim_scale = [&](element_handle_t e, float to, float dur, + easing_t ease, bool q = false, bool p = false) { + cmd_anim_scale(S, comp, e, to, dur, ease, q, p); + }; + auto anim_move = [&](element_handle_t e, int32_t x, int32_t y, + float dur, easing_t ease, bool q = false, bool p = false) { + cmd_anim_move(S, comp, e, x, y, dur, ease, q, p); + }; + auto anim_fade = [&](element_handle_t e, float a, float dur, + easing_t ease, bool q = false, bool p = false) { + cmd_anim_fade(S, comp, e, a, dur, ease, q, p); + }; + auto show = [&](element_handle_t e) { cmd_set_visible(S, comp, e, true); }; + auto hide = [&](element_handle_t e) { cmd_set_visible(S, comp, e, false); }; + auto scale = [&](element_handle_t e, float v) { cmd_set_scale(S, comp, e, v); }; + auto alpha = [&](element_handle_t e, float v) { cmd_set_alpha(S, comp, e, v); }; + + // ------------------------------------------------------------------ + // Scene 1 - maksw slides in, introduces the engine + // ------------------------------------------------------------------ + + anim_move(maksw_happy_id, CENTER_X, CHAR_BASELINE_Y, + 1.5f, EASE_OUT_CUBIC, true); + anim_scale(maksw_happy_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC, /*queue=*/true, /*parallel=*/false); + speaker("maksw"); + line("Hello and welcome to the Obwarzanek Narrative Engine demo!"); + wait_click(); + + line("Today I'll present to you-"); + wait_click(); + + anim_move(wikor_id, RIGHT_CHAR_CENTER_X, CHAR_BASELINE_Y, 0.4f, EASE_OUT_CUBIC); + anim_move(maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y - 20, + 0.3f, EASE_OUT_CUBIC); + anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("Yeah, hello to you too."); + wait_click(); + + const char* greetings[] = { + "Oh Wiktor, what a nice surprise!", + "Oh hey Wiktor, didn't expect to see you!", + "Wiktor! What brings you here?" + }; + + const char* responses[] = { + "No, I was just walking by.", + "Nah, just passing through.", + "Not really, just wandering." + }; + + int greeting_idx = rand() % 3; + int response_idx = rand() % 3; + + anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("maksw"); + line(greetings[greeting_idx]); + wait_click(); + + line("...Do you need anything?"); + wait_click(); + + anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line(responses[response_idx]); + wait_click(); + + line("Cool engine though."); + wait_click(); + + finish_all(); + show(mask_id); + anim_move(mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC); + wait_anim(mask_id); + + hide(maksw_happy_id); + show(maksw_neutral_id); + scale(maksw_neutral_id, 1.0f); + scale(wikor_id, 1.05f); + + hide(mask_id); + show(mask2_id); + anim_move(mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC); + speaker("wikor"); + clear(); + wait_anim(mask2_id); + line("Anyways, how's your day going?"); + wait_click(); + + // ------------------------------------------------------------------ + // Scene 2 - neutral maksw, wikor is chatty + // ------------------------------------------------------------------ + + finish_all(); + anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("maksw"); + line("You hear what I'm hearing?"); + wait_click(); + + finish_all(); + anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("What? That music?"); + wait_click(); + + finish_all(); + anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("maksw"); + char music_line[256]; + snprintf(music_line, sizeof(music_line), + "Yep! %s by %s!", track->title, track->composer); + line(music_line); + wait_click(); + + finish_all(); + anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("Sure thing..."); + wait_click(); + + finish_all(); + line("Like I know who he is."); + wait_click(); + + // ------------------------------------------------------------------ + // Ending - fade to black, fade out music, then quit + // ------------------------------------------------------------------ + + finish_all(); + anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + // Reuse mask as a simple alpha-fade overlay (no positional wipe needed) + show(mask_id); + alpha(mask_id, 0.0f); + anim_fade(mask_id, 1.0f, 0.8f, EASE_IN_OUT_CUBIC); + cmd_audio_fade(S, audio, music_id, 0.0f, 0.6f); + wait_anim(mask_id); // wait until black, then exit + cmd_end(S, interp); + + // ---- main loop -------------------------------------------------------- + + bool running = true; + + auto last_frame_time = std::chrono::high_resolution_clock::now(); + auto last_report_time = last_frame_time; + uint64_t frame_count = 0; + float highest_frame_time = 0.0f; + float frame_time_sum = 0.0f; + const std::chrono::duration REPORT_INTERVAL = std::chrono::seconds(1); + + bool mouse_was_down = false; + bool key_was_down = false; + + while (running) { + if (window_pollevent(window)) running = false; + + auto current_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration delta_duration = current_time - last_frame_time; + float dt = delta_duration.count(); + last_frame_time = current_time; + + // FPS reporting + frame_count++; + frame_time_sum += dt; + if (dt > highest_frame_time) highest_frame_time = dt; + std::chrono::duration since_report = current_time - last_report_time; + if (since_report >= REPORT_INTERVAL) { + if (frame_count > 0) { + float fps = 1.0f / (frame_time_sum / (float)frame_count); + printf("FPS: %.2f (over %llu frames in %.2f seconds), highest frame time: %.2fms\n", + fps, frame_count, since_report.count(), highest_frame_time * 1000.0f); + } + frame_count = 0; frame_time_sum = 0.0f; highest_frame_time = 0.0f; + last_report_time = current_time; + } + + // Edge-detect click and keypress + bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT); + bool key_is_down = window_is_key_down(window, KEY_SPACE); + bool clicked = (mouse_is_down && !mouse_was_down) || + (key_is_down && !key_was_down); + mouse_was_down = mouse_is_down; + key_was_down = key_is_down; + + // Tick the interpreter + bool script_done = interp.tick(clicked); + if (script_done) running = false; + + // Tick subsystems + compositor_tick(compositor, dt); + + if (text_nextframe(name_text, dt)) compositor_mark_content_dirty(compositor, name_id); + if (text_nextframe(dialogue_text, dt)) compositor_mark_content_dirty(compositor, dialogue_id); + + const uint32_t* fb = compositor_compose(compositor); + window_blit(window, fb); + audio_tick(audio, dt); + } + + audio_stop(audio, music_id); + return 0; +} diff --git a/src/text.cpp b/src/text.cpp index 94c90fa..186caf9 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -1,798 +1,798 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define STB_TRUETYPE_IMPLEMENTATION -#ifdef _MSC_VER -#pragma warning(push, 0) -#endif -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC system_header -#endif -#include -#ifdef _MSC_VER -#pragma warning(pop) -#endif - -// Decode the next UTF-8 codepoint from `s` starting at `i` (byte index). -// Advances `i` to the next byte after the decoded codepoint. -// Returns the Unicode codepoint (0..0x10FFFF). Invalid sequences return 0xFFFD. -static uint32_t utf8_next(const std::string& s, size_t& i) { - const unsigned char* bytes = reinterpret_cast(s.data()); - size_t n = s.size(); - if (i >= n) return 0; - - unsigned char b0 = bytes[i]; - - // 1-byte (ASCII) - if (b0 < 0x80) { - ++i; - return (uint32_t)b0; - } - - // multi-byte sequences - if ((b0 & 0xE0) == 0xC0) { - // 2-byte - if (i + 1 >= n) { ++i; return 0xFFFD; } - unsigned char b1 = bytes[i + 1]; - if ((b1 & 0xC0) != 0x80) { ++i; return 0xFFFD; } - uint32_t cp = ((b0 & 0x1F) << 6) | (b1 & 0x3F); - i += 2; - if (cp < 0x80) return 0xFFFD; // overlong - return cp; - } - if ((b0 & 0xF0) == 0xE0) { - // 3-byte - if (i + 2 >= n) { ++i; return 0xFFFD; } - unsigned char b1 = bytes[i + 1], b2 = bytes[i + 2]; - if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80) { ++i; return 0xFFFD; } - uint32_t cp = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F); - i += 3; - if (cp < 0x800) return 0xFFFD; // overlong - return cp; - } - if ((b0 & 0xF8) == 0xF0) { - // 4-byte - if (i + 3 >= n) { ++i; return 0xFFFD; } - unsigned char b1 = bytes[i + 1], b2 = bytes[i + 2], b3 = bytes[i + 3]; - if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) { ++i; return 0xFFFD; } - uint32_t cp = ((b0 & 0x07) << 18) | ((b1 & 0x3F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F); - i += 4; - if (cp < 0x10000 || cp > 0x10FFFF) return 0xFFFD; - return cp; - } - - // Invalid leading byte - ++i; - return 0xFFFD; -} - -// Count codepoints in a UTF-8 string -static size_t utf8_count_codepoints(const std::string& s) { - size_t i = 0; - size_t count = 0; - while (i < s.size()) { - utf8_next(s, i); - ++count; - } - return count; -} - -// Holds the raw file data and stbtt_fontinfo for a cached font. -struct cached_font_t { - std::vector file_data; - stbtt_fontinfo info; -}; - -// Uniquely identifies a specific glyph variant for caching. -// Now uses SDF, so the key includes the size and Unicode codepoint. -struct glyph_key_t { - const cached_font_t* font; - uint32_t codepoint; - float size; - - bool operator<(const glyph_key_t& other) const { - if (font != other.font) return font < other.font; - if (codepoint != other.codepoint) return codepoint < other.codepoint; - if (size != other.size) return size < other.size; - return false; - } -}; - -// Holds the rendered SDF bitmap and metrics for a cached glyph. -struct cached_glyph_t { - std::unique_ptr data; // This now holds SDF data - int w, h; - int xoff; - int yoff; - int advance; -}; - -// Stores the start and end indices for a precomputed line of text. -// For UTF-8 we store both byte ranges and codepoint ranges. -struct line_break_t { - size_t start_byte_index; // byte index of first char in the line - size_t end_byte_index; // byte index *after* last char in the line - size_t start_cp_index; // codepoint index of first char in the line - size_t end_cp_index; // codepoint index *after* last char in the line - float y_offset; // The y-coordinate to start drawing this line -}; - -// Holds the parameters for a single `text_push` call. -struct text_params_t { - std::string text; - float size; - float speed; - std::string font_path; - bool outline; - uint32_t color_main = 0xFFFFFFFF; - uint32_t color_outline = 0xFF000000; - - // Mutable fields for precomputed layout - mutable std::vector lines; - mutable bool lines_computed = false; -}; - -// Shared cache structure that can be referenced by multiple text objects. -struct shared_cache_t { - std::map font_cache; - std::map glyph_cache; - int refcount = 1; // Reference counting for proper cleanup -}; - -// The main internal context structure for the text renderer. -struct text_t { - uint32_t* framebuffer; - int width; - int height; - - // Shared cache (can be shared across multiple text_t instances) - shared_cache_t* cache; - bool owns_cache; // True if this text_t owns the cache and should delete it - - // Default font settings - std::string default_font_path; - float default_font_size = 16.0f; - - // Rendering State (per-instance) - std::vector text_queue; - int queue_index = 0; - int char_index = 0; // Now tracks total codepoints to *show* - float char_progress = 0.0f; // Cumulative progress for animation - - cached_font_t* current_font = nullptr; - float current_x = 0.0f; - float current_y = 0.0f; - float last_line_height = 0.0f; - - const float padding_x = 10.0f; - const float padding_y = 10.0f; - - int last_rendered_char = 0; // codepoint count -}; - -// Loads a font from file or retrieves it from the shared cache. -static cached_font_t* load_font(text_t* text, const std::string& font_path) { - // Check cache (now in shared_cache_t) - auto it = text->cache->font_cache.find(font_path); - if (it != text->cache->font_cache.end()) { - return &it->second; - } - - // Load from file - FILE* file = fopen(font_path.c_str(), "rb"); - if (!file) { - fprintf(stderr, "Error: Could not open font file: %s\n", font_path.c_str()); - return nullptr; - } - - fseek(file, 0, SEEK_END); - long file_size = ftell(file); - fseek(file, 0, SEEK_SET); - - if (file_size <= 0) { - fprintf(stderr, "Error: Invalid font file size: %s\n", font_path.c_str()); - fclose(file); - return nullptr; - } - - std::vector buffer(file_size); - size_t read = fread(buffer.data(), 1, file_size, file); - fclose(file); - - if (read != (size_t)file_size) { - fprintf(stderr, "Error: Could not read font file: %s\n", font_path.c_str()); - return nullptr; - } - - // Init font and add to cache - cached_font_t new_font; - new_font.file_data = std::move(buffer); - - if (!stbtt_InitFont(&new_font.info, new_font.file_data.data(), 0)) { - fprintf(stderr, "Error: stbtt_InitFont failed for: %s\n", font_path.c_str()); - return nullptr; - } - - auto [map_it, success] = text->cache->font_cache.emplace(font_path, std::move(new_font)); - return &map_it->second; -} - -// Gets a cached SDF glyph, generating it if it doesn't exist. -static const cached_glyph_t* get_glyph(text_t* text, uint32_t codepoint, float size) -{ - if (!text->current_font) return nullptr; - - glyph_key_t key = { text->current_font, codepoint, size }; - - // Check cache (now in shared_cache_t) - auto it = text->cache->glyph_cache.find(key); - if (it != text->cache->glyph_cache.end()) { - return &it->second; - } - - // --- Generate SDF Glyph --- - cached_glyph_t new_glyph; - const stbtt_fontinfo* info = &text->current_font->info; - float scale = stbtt_ScaleForPixelHeight(info, size); - - // Get advance metric - int adv; - stbtt_GetCodepointHMetrics(info, (int)codepoint, &adv, nullptr); - new_glyph.advance = static_cast(adv * scale); - - // SDF parameters - const float padding_percent = 0.15f; - const unsigned char onedge_value = 128; // 0-255 range midpoint - const int padding = std::max(1, (int)(size * padding_percent)); // Padding in pixels - float pixel_dist_scale = (float)onedge_value / (float)padding; // 'distance units' per pixel - - // Generate the SDF bitmap - unsigned char* bitmap = stbtt_GetCodepointSDF( - info, scale, (int)codepoint, padding, onedge_value, pixel_dist_scale, - &new_glyph.w, &new_glyph.h, &new_glyph.xoff, &new_glyph.yoff - ); - - if (!bitmap) { - // Glyph has no bitmap (e.g., space) - new_glyph.w = new_glyph.h = 0; - new_glyph.data = nullptr; - } - else { - // Copy the SDF bitmap into our managed cache - new_glyph.data = std::make_unique(new_glyph.w * new_glyph.h); - memcpy(new_glyph.data.get(), bitmap, new_glyph.w * new_glyph.h); - - // Free the stb_truetype allocated bitmap - stbtt_FreeSDF(bitmap, nullptr); - } - - auto [map_it, success] = text->cache->glyph_cache.emplace(key, std::move(new_glyph)); - return &map_it->second; -} - -// Precomputes line breaks for a text block. -static void precompute_line_breaks(text_t* text, const text_params_t* params) { - if (params->lines_computed) return; - - params->lines.clear(); - - // Ensure the correct font is loaded and active for get_glyph - cached_font_t* font = text->current_font; - if (!font || (font && params->font_path != "" && text->cache->font_cache.find(params->font_path) != text->cache->font_cache.end() && &text->cache->font_cache.at(params->font_path) != font)) - { - if (params->font_path.empty()) { - if (text->cache->font_cache.empty()) return; // No default font - font = &text->cache->font_cache.begin()->second; - } - else { - font = load_font(text, params->font_path); - } - if (!font) { - fprintf(stderr, "Error: Failed to load font for line breaking: %s\n", params->font_path.c_str()); - return; // Can't proceed - } - text->current_font = font; // Set it as active - } - - const stbtt_fontinfo* info = &font->info; - float scale = stbtt_ScaleForPixelHeight(info, params->size); - int ascent, descent, line_gap; - stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap); - float line_height = (ascent - descent + line_gap) * scale; - - float x = text->padding_x; - float y = text->current_y; // Start from the current render position - size_t line_start_byte = 0; - size_t line_start_cp = 0; - size_t last_word_break_byte = 0; // byte index *after* the space - size_t last_word_break_cp = 0; - float x_at_last_word_break = text->padding_x; - float max_width = text->width - text->padding_x; - - // We'll iterate the UTF-8 string and maintain both byte index and cp index - const std::string& s = params->text; - size_t byte_i = 0; - size_t cp_index = 0; - - while (byte_i < s.size()) { - size_t prev_byte = byte_i; - uint32_t cp = utf8_next(s, byte_i); - - // handle newline explicitly - if (cp == (uint32_t)'\n') { - // push line: [line_start_byte, prev_byte) -> includes upto but excluding newline - params->lines.push_back({ line_start_byte, prev_byte, line_start_cp, cp_index, y }); - y += line_height; - x = text->padding_x; - line_start_byte = byte_i; - line_start_cp = cp_index + 1; // next cp after newline - last_word_break_byte = byte_i; - last_word_break_cp = line_start_cp; - x_at_last_word_break = x; - ++cp_index; - continue; - } - - // store last word break pos - if (cp == (uint32_t)' ' || cp == (uint32_t)'\t') { - last_word_break_byte = byte_i; // byte index after this space - last_word_break_cp = cp_index + 1; - } - - // get advance for cp - const cached_glyph_t* glyph = get_glyph(text, cp, params->size); - int advance = glyph ? glyph->advance : 0; - - // check wrap before adding the char - if (x + advance > max_width && line_start_byte < prev_byte) { - // wrap - if (last_word_break_byte > line_start_byte) { - // wrap at last word break (exclude trailing space) - // end_byte = last_word_break_byte - bytes of space? last_word_break_byte already points after the space - // we want to exclude final space visually: line end byte = last_word_break_byte - size_of_space_byte_sequence - // But simpler: set end_byte to last_word_break_byte - 1 byte may not be correct for multi-byte, - // so we set end_cp = last_word_break_cp - 1 and then compute end_byte by walking from line_start_byte. - // But we already tracked last_word_break_byte and last_word_break_cp as byte AFTER the space; so we set - // end_byte = last_word_break_byte - bytes_of_space (unknown). To avoid complexity, we'll set end_byte = last_word_break_byte; and render lines excluding trailing space in drawing. - params->lines.push_back({ line_start_byte, last_word_break_byte, line_start_cp, last_word_break_cp, y }); - // re-start from after the space - // set byte_i to last_word_break_byte (already after space) - byte_i = last_word_break_byte; - cp_index = last_word_break_cp; - line_start_byte = last_word_break_byte; - line_start_cp = last_word_break_cp; - } - else { - // force wrap in middle of word: end at previous char - params->lines.push_back({ line_start_byte, prev_byte, line_start_cp, cp_index, y }); - // start new line at prev_byte (reprocess current char on new line) - byte_i = prev_byte; - // cp_index unchanged because we didn't consume this cp for new line - line_start_byte = prev_byte; - line_start_cp = cp_index; - } - - // reset for new line - y += line_height; - x = text->padding_x; - last_word_break_byte = line_start_byte; - last_word_break_cp = line_start_cp; - x_at_last_word_break = x; - continue; // loop will continue, not advancing cp_index here (we set it appropriately) - } - else { - // no wrap, just advance - x += advance; - ++cp_index; - // update x_at_last_word_break if this was space - if (last_word_break_cp == cp_index) { - x_at_last_word_break = x; - } - } - } - - // Add the final line if any text remains - if (line_start_byte < s.size()) { - params->lines.push_back({ line_start_byte, s.size(), line_start_cp, cp_index, y }); - } - - text->last_line_height = line_height; // Store for next block's y_offset - params->lines_computed = true; -} - -// Standard smoothstep function for SDF anti-aliasing. -static float smoothstep(float edge0, float edge1, float x) { - float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); - return t * t * (3.0f - 2.0f * t); -} - -// Standard alpha blending (source-over). -static uint32_t alpha_blend(uint32_t dest, uint32_t src) { - uint32_t src_a = (src >> 24) & 0xFF; - if (src_a == 0) return dest; - if (src_a == 255) return src; - - uint32_t dest_a = (dest >> 24) & 0xFF; - - uint32_t src_r = (src >> 16) & 0xFF; - uint32_t src_g = (src >> 8) & 0xFF; - uint32_t src_b = (src) & 0xFF; - - uint32_t dest_r = (dest >> 16) & 0xFF; - uint32_t dest_g = (dest >> 8) & 0xFF; - uint32_t dest_b = (dest) & 0xFF; - - uint32_t out_a = src_a + (dest_a * (255 - src_a) / 255); - if (out_a == 0) return 0; - - uint32_t out_r = (src_r * src_a + dest_r * dest_a * (255 - src_a) / 255) / out_a; - uint32_t out_g = (src_g * src_a + dest_g * dest_a * (255 - src_a) / 255) / out_a; - uint32_t out_b = (src_b * src_a + dest_b * dest_a * (255 - src_a) / 255) / out_a; - - return (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b; -} - -// Draws a glyph using its cached SDF bitmap. -static void draw_glyph(text_t* text, - int x, int y, - uint32_t color, - const cached_glyph_t* glyph, - float size, - bool outline) -{ - if (!glyph || !glyph->data) return; - - int x0 = x; - int y0 = y; - int x1 = x + glyph->w; - int y1 = y + glyph->h; - - // --- SDF Parameters --- - const float padding_percent = 0.15f; - const float onedge_value = 128.0f; - float padding = std::max(1.0f, size * padding_percent); - float pixel_dist_scale = onedge_value / padding; - // ---------------------- - - float outline_width = std::max(1.0f, size * 0.05f); - float aa_width = 0.75f; - - // Clip to framebuffer - int gx0 = (x0 < 0) ? -x0 : 0; - int gy0 = (y0 < 0) ? -y0 : 0; - int gx1 = (x1 > text->width) ? (glyph->w - (x1 - text->width)) : glyph->w; - int gy1 = (y1 > text->height) ? (glyph->h - (y1 - text->height)) : glyph->h; - - uint32_t color_rgb = color & 0x00FFFFFF; - - for (int gy = gy0; gy < gy1; ++gy) { - int fb_y = y0 + gy; - for (int gx = gx0; gx < gx1; ++gx) { - uint8_t sdf_raw = glyph->data[gy * glyph->w + gx]; - float dist = (sdf_raw - onedge_value) / pixel_dist_scale; - - float alpha = 0.0f; - if (outline) { - float edge_inner_0 = -aa_width; - float edge_inner_1 = +aa_width; - float mask_inner = smoothstep(edge_inner_0, edge_inner_1, dist); - - float edge_outer_0 = -outline_width - aa_width; - float edge_outer_1 = -outline_width + aa_width; - float mask_outer = smoothstep(edge_outer_0, edge_outer_1, dist); - - alpha = std::clamp(mask_outer - mask_inner, 0.0f, 1.0f); - } else { - alpha = smoothstep(-aa_width, aa_width, dist); - } - - if (alpha == 0.0f) continue; - - int fb_x = x0 + gx; - uint32_t* dest_pixel = &text->framebuffer[fb_y * text->width + fb_x]; - uint32_t src_pixel = ((uint32_t)(alpha * 255.0f) << 24) | color_rgb; - - *dest_pixel = alpha_blend(*dest_pixel, src_pixel); - } - } -} - -extern "C" { - -text_t* text_init(int width, int height) { - if (width <= 0 || height <= 0) return nullptr; - - text_t* text = new (std::nothrow) text_t; - if (!text) return nullptr; - - text->width = width; - text->height = height; - - text->framebuffer = new (std::nothrow) uint32_t[width * height]; - if (!text->framebuffer) { - delete text; - return nullptr; - } - - // Create new cache that this text_t owns - text->cache = new (std::nothrow) shared_cache_t; - if (!text->cache) { - delete[] text->framebuffer; - delete text; - return nullptr; - } - text->owns_cache = true; - - text->queue_index = 0; - text->char_index = 0; - text->char_progress = 0.0f; - text->current_font = nullptr; - text->last_line_height = 0.0f; - - text->current_x = text->padding_x; - text->current_y = text->padding_y; - - text_clear(text); - return text; -} - -text_t* text_init_shared(text_t* source, int width, int height) { - if (width <= 0 || height <= 0 || !source || !source->cache) return nullptr; - - text_t* text = new (std::nothrow) text_t; - if (!text) return nullptr; - - text->width = width; - text->height = height; - - text->framebuffer = new (std::nothrow) uint32_t[width * height]; - if (!text->framebuffer) { - delete text; - return nullptr; - } - - // Share the cache from source - text->cache = source->cache; - text->cache->refcount++; - text->owns_cache = false; - - text->queue_index = 0; - text->char_index = 0; - text->char_progress = 0.0f; - text->current_font = nullptr; - text->last_line_height = 0.0f; - - text->current_x = text->padding_x; - text->current_y = text->padding_y; - - text_clear(text); - return text; -} - -void text_destroy(text_t* text) { - if (!text) return; - - delete[] text->framebuffer; - - // Handle cache cleanup with reference counting - if (text->cache) { - text->cache->refcount--; - if (text->cache->refcount == 0) { - delete text->cache; - } - } - - delete text; -} - -// Sets the default font path and size to be used by subsequent text_push calls. -// Pass NULL for font_path to keep the current default font. -// Pass size <= 0 to keep the current default size. -void text_set_font(text_t* text, const char* font_path, float size) { - if (!text) return; - - if (font_path != nullptr) { - text->default_font_path = font_path; - } - - if (size > 0.0f) { - text->default_font_size = size; - } -} - -// Adds a block of text to the rendering queue. -void text_push(text_t* text, - const char* str, - float speed, - bool outline) -{ - if (!text || !str) return; - - text_params_t p; - p.text = str; - - p.size = text->default_font_size; - - p.speed = speed; - p.outline = outline; - - p.font_path = text->default_font_path; - - p.lines_computed = false; // Mark for precomputation - - text->text_queue.push_back(std::move(p)); -} - -// Clears the framebuffer and resets the text queue and all state. -void text_clear(text_t* text) { - if (!text) return; - - memset(text->framebuffer, 0, text->width * text->height * sizeof(uint32_t)); - - text->current_x = text->padding_x; - text->current_y = text->padding_y; - text->text_queue.clear(); - text->queue_index = 0; - text->char_index = 0; - text->char_progress = 0.0f; - text->last_line_height = 0.0f; -} - -// Immediately sets the character index to the end of the current text block. -void text_show_all_current(text_t* text) { - if (!text || text->queue_index >= text->text_queue.size()) return; - - text_params_t* params = &text->text_queue[text->queue_index]; - - size_t text_len_cp = utf8_count_codepoints(params->text); - - // Only update if it's not already complete - if (text->char_index < (int)text_len_cp) { - text->char_index = (int)text_len_cp; - text->char_progress = static_cast(text_len_cp); - } -} - -// Advances the text animation and renders new characters. -// deltatime is the time elapsed since the last frame (in seconds). -bool text_nextframe(text_t* text, float deltatime) { - if (!text) return false; - if (text->queue_index >= text->text_queue.size()) { - return false; // Nothing to do - } - - text_params_t* params = &text->text_queue[text->queue_index]; - - // Load font if needed - if (!params->font_path.empty()) { - cached_font_t* font = load_font(text, params->font_path); - if (font) text->current_font = font; - } - - if (!text->current_font) { - if (text->cache->font_cache.empty()) return false; - text->current_font = &text->cache->font_cache.begin()->second; - } - - // Precompute layout - precompute_line_breaks(text, params); - - const stbtt_fontinfo* info = &text->current_font->info; - float scale = stbtt_ScaleForPixelHeight(info, params->size); - int ascent, descent, line_gap; - stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap); - float baseline = ascent * scale; - - const size_t total_cp = utf8_count_codepoints(params->text); - - bool did_draw = false; - - // Update animation progress - if (params->speed > 0.0f && text->char_index < (int)total_cp) { - text->char_progress += params->speed * deltatime; - int new_index = static_cast(text->char_progress); - if (new_index > text->char_index) { - text->char_index = std::min(new_index, (int)total_cp); - did_draw = true; // Will draw new characters - } - } - else if (params->speed <= 0.0f) { - if (text->char_index < (int)total_cp) { - text->char_index = (int)total_cp; - did_draw = true; // Draw all instantly - } - } - - // Draw only new characters - size_t draw_start = text->last_rendered_char; - size_t draw_end = text->char_index; - - if (draw_start < draw_end) { - for (const auto& line : params->lines) { - if (line.end_cp_index <= draw_start) continue; - if (line.start_cp_index >= draw_end) break; - - text->current_x = text->padding_x; - text->current_y = line.y_offset; - - // Advance current_x to draw_start - size_t byte_i = line.start_byte_index; - size_t cp_i = line.start_cp_index; - while (byte_i < line.end_byte_index && cp_i < draw_start) { - uint32_t cp = utf8_next(params->text, byte_i); - const cached_glyph_t* glyph = get_glyph(text, cp, params->size); - if (glyph) text->current_x += glyph->advance; - ++cp_i; - } - - // Draw new characters - byte_i = line.start_byte_index; - cp_i = line.start_cp_index; - while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) { - utf8_next(params->text, byte_i); - ++cp_i; - } - - while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) { - uint32_t cp = utf8_next(params->text, byte_i); - if (cp == (uint32_t)'\n') { ++cp_i; continue; } - - const cached_glyph_t* glyph = get_glyph(text, cp, params->size); - if (!glyph) { ++cp_i; continue; } - - int x_base = static_cast(text->current_x + glyph->xoff); - int y_base = static_cast(text->current_y + baseline + glyph->yoff); - - if (params->outline) draw_glyph(text, x_base, y_base, params->color_outline, glyph, params->size, true); - draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false); - - text->current_x += glyph->advance; - ++cp_i; - } - } - - text->last_rendered_char = (int)text->char_index; - did_draw = true; - } - - // Handle block transition - if (text->char_index >= (int)total_cp) { - text->queue_index++; - text->char_index = 0; - text->char_progress = 0.0f; - text->last_rendered_char = 0; - if (text->queue_index > 1) text->current_y += text->last_line_height; - } - - // Return true if something was drawn or queue finished - if (did_draw || text->queue_index >= text->text_queue.size()) return true; - - return false; -} - -// Returns true if the currently running text block has not yet displayed all characters. -bool text_is_animating(text_t* text) { - if (!text) return false; - if (text->queue_index >= text->text_queue.size()) return false; - - const text_params_t* params = &text->text_queue[text->queue_index]; - size_t total_cp = utf8_count_codepoints(params->text); - return text->char_index < (int)total_cp; -} - -// Returns a pointer to the internal framebuffer. -const uint32_t* text_getfb(text_t* text) { - return text ? text->framebuffer : nullptr; -} - -} // extern "C" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STB_TRUETYPE_IMPLEMENTATION +#ifdef _MSC_VER +#pragma warning(push, 0) +#endif +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC system_header +#endif +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +// Decode the next UTF-8 codepoint from `s` starting at `i` (byte index). +// Advances `i` to the next byte after the decoded codepoint. +// Returns the Unicode codepoint (0..0x10FFFF). Invalid sequences return 0xFFFD. +static uint32_t utf8_next(const std::string& s, size_t& i) { + const unsigned char* bytes = reinterpret_cast(s.data()); + size_t n = s.size(); + if (i >= n) return 0; + + unsigned char b0 = bytes[i]; + + // 1-byte (ASCII) + if (b0 < 0x80) { + ++i; + return (uint32_t)b0; + } + + // multi-byte sequences + if ((b0 & 0xE0) == 0xC0) { + // 2-byte + if (i + 1 >= n) { ++i; return 0xFFFD; } + unsigned char b1 = bytes[i + 1]; + if ((b1 & 0xC0) != 0x80) { ++i; return 0xFFFD; } + uint32_t cp = ((b0 & 0x1F) << 6) | (b1 & 0x3F); + i += 2; + if (cp < 0x80) return 0xFFFD; // overlong + return cp; + } + if ((b0 & 0xF0) == 0xE0) { + // 3-byte + if (i + 2 >= n) { ++i; return 0xFFFD; } + unsigned char b1 = bytes[i + 1], b2 = bytes[i + 2]; + if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80) { ++i; return 0xFFFD; } + uint32_t cp = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F); + i += 3; + if (cp < 0x800) return 0xFFFD; // overlong + return cp; + } + if ((b0 & 0xF8) == 0xF0) { + // 4-byte + if (i + 3 >= n) { ++i; return 0xFFFD; } + unsigned char b1 = bytes[i + 1], b2 = bytes[i + 2], b3 = bytes[i + 3]; + if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) { ++i; return 0xFFFD; } + uint32_t cp = ((b0 & 0x07) << 18) | ((b1 & 0x3F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F); + i += 4; + if (cp < 0x10000 || cp > 0x10FFFF) return 0xFFFD; + return cp; + } + + // Invalid leading byte + ++i; + return 0xFFFD; +} + +// Count codepoints in a UTF-8 string +static size_t utf8_count_codepoints(const std::string& s) { + size_t i = 0; + size_t count = 0; + while (i < s.size()) { + utf8_next(s, i); + ++count; + } + return count; +} + +// Holds the raw file data and stbtt_fontinfo for a cached font. +struct cached_font_t { + std::vector file_data; + stbtt_fontinfo info; +}; + +// Uniquely identifies a specific glyph variant for caching. +// Now uses SDF, so the key includes the size and Unicode codepoint. +struct glyph_key_t { + const cached_font_t* font; + uint32_t codepoint; + float size; + + bool operator<(const glyph_key_t& other) const { + if (font != other.font) return font < other.font; + if (codepoint != other.codepoint) return codepoint < other.codepoint; + if (size != other.size) return size < other.size; + return false; + } +}; + +// Holds the rendered SDF bitmap and metrics for a cached glyph. +struct cached_glyph_t { + std::unique_ptr data; // This now holds SDF data + int w, h; + int xoff; + int yoff; + int advance; +}; + +// Stores the start and end indices for a precomputed line of text. +// For UTF-8 we store both byte ranges and codepoint ranges. +struct line_break_t { + size_t start_byte_index; // byte index of first char in the line + size_t end_byte_index; // byte index *after* last char in the line + size_t start_cp_index; // codepoint index of first char in the line + size_t end_cp_index; // codepoint index *after* last char in the line + float y_offset; // The y-coordinate to start drawing this line +}; + +// Holds the parameters for a single `text_push` call. +struct text_params_t { + std::string text; + float size; + float speed; + std::string font_path; + bool outline; + uint32_t color_main = 0xFFFFFFFF; + uint32_t color_outline = 0xFF000000; + + // Mutable fields for precomputed layout + mutable std::vector lines; + mutable bool lines_computed = false; +}; + +// Shared cache structure that can be referenced by multiple text objects. +struct shared_cache_t { + std::map font_cache; + std::map glyph_cache; + int refcount = 1; // Reference counting for proper cleanup +}; + +// The main internal context structure for the text renderer. +struct text_t { + uint32_t* framebuffer; + int width; + int height; + + // Shared cache (can be shared across multiple text_t instances) + shared_cache_t* cache; + bool owns_cache; // True if this text_t owns the cache and should delete it + + // Default font settings + std::string default_font_path; + float default_font_size = 16.0f; + + // Rendering State (per-instance) + std::vector text_queue; + int queue_index = 0; + int char_index = 0; // Now tracks total codepoints to *show* + float char_progress = 0.0f; // Cumulative progress for animation + + cached_font_t* current_font = nullptr; + float current_x = 0.0f; + float current_y = 0.0f; + float last_line_height = 0.0f; + + const float padding_x = 10.0f; + const float padding_y = 10.0f; + + int last_rendered_char = 0; // codepoint count +}; + +// Loads a font from file or retrieves it from the shared cache. +static cached_font_t* load_font(text_t* text, const std::string& font_path) { + // Check cache (now in shared_cache_t) + auto it = text->cache->font_cache.find(font_path); + if (it != text->cache->font_cache.end()) { + return &it->second; + } + + // Load from file + FILE* file = fopen(font_path.c_str(), "rb"); + if (!file) { + fprintf(stderr, "Error: Could not open font file: %s\n", font_path.c_str()); + return nullptr; + } + + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + if (file_size <= 0) { + fprintf(stderr, "Error: Invalid font file size: %s\n", font_path.c_str()); + fclose(file); + return nullptr; + } + + std::vector buffer(file_size); + size_t read = fread(buffer.data(), 1, file_size, file); + fclose(file); + + if (read != (size_t)file_size) { + fprintf(stderr, "Error: Could not read font file: %s\n", font_path.c_str()); + return nullptr; + } + + // Init font and add to cache + cached_font_t new_font; + new_font.file_data = std::move(buffer); + + if (!stbtt_InitFont(&new_font.info, new_font.file_data.data(), 0)) { + fprintf(stderr, "Error: stbtt_InitFont failed for: %s\n", font_path.c_str()); + return nullptr; + } + + auto [map_it, success] = text->cache->font_cache.emplace(font_path, std::move(new_font)); + return &map_it->second; +} + +// Gets a cached SDF glyph, generating it if it doesn't exist. +static const cached_glyph_t* get_glyph(text_t* text, uint32_t codepoint, float size) +{ + if (!text->current_font) return nullptr; + + glyph_key_t key = { text->current_font, codepoint, size }; + + // Check cache (now in shared_cache_t) + auto it = text->cache->glyph_cache.find(key); + if (it != text->cache->glyph_cache.end()) { + return &it->second; + } + + // --- Generate SDF Glyph --- + cached_glyph_t new_glyph; + const stbtt_fontinfo* info = &text->current_font->info; + float scale = stbtt_ScaleForPixelHeight(info, size); + + // Get advance metric + int adv; + stbtt_GetCodepointHMetrics(info, (int)codepoint, &adv, nullptr); + new_glyph.advance = static_cast(adv * scale); + + // SDF parameters + const float padding_percent = 0.15f; + const unsigned char onedge_value = 128; // 0-255 range midpoint + const int padding = std::max(1, (int)(size * padding_percent)); // Padding in pixels + float pixel_dist_scale = (float)onedge_value / (float)padding; // 'distance units' per pixel + + // Generate the SDF bitmap + unsigned char* bitmap = stbtt_GetCodepointSDF( + info, scale, (int)codepoint, padding, onedge_value, pixel_dist_scale, + &new_glyph.w, &new_glyph.h, &new_glyph.xoff, &new_glyph.yoff + ); + + if (!bitmap) { + // Glyph has no bitmap (e.g., space) + new_glyph.w = new_glyph.h = 0; + new_glyph.data = nullptr; + } + else { + // Copy the SDF bitmap into our managed cache + new_glyph.data = std::make_unique(new_glyph.w * new_glyph.h); + memcpy(new_glyph.data.get(), bitmap, new_glyph.w * new_glyph.h); + + // Free the stb_truetype allocated bitmap + stbtt_FreeSDF(bitmap, nullptr); + } + + auto [map_it, success] = text->cache->glyph_cache.emplace(key, std::move(new_glyph)); + return &map_it->second; +} + +// Precomputes line breaks for a text block. +static void precompute_line_breaks(text_t* text, const text_params_t* params) { + if (params->lines_computed) return; + + params->lines.clear(); + + // Ensure the correct font is loaded and active for get_glyph + cached_font_t* font = text->current_font; + if (!font || (font && params->font_path != "" && text->cache->font_cache.find(params->font_path) != text->cache->font_cache.end() && &text->cache->font_cache.at(params->font_path) != font)) + { + if (params->font_path.empty()) { + if (text->cache->font_cache.empty()) return; // No default font + font = &text->cache->font_cache.begin()->second; + } + else { + font = load_font(text, params->font_path); + } + if (!font) { + fprintf(stderr, "Error: Failed to load font for line breaking: %s\n", params->font_path.c_str()); + return; // Can't proceed + } + text->current_font = font; // Set it as active + } + + const stbtt_fontinfo* info = &font->info; + float scale = stbtt_ScaleForPixelHeight(info, params->size); + int ascent, descent, line_gap; + stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap); + float line_height = (ascent - descent + line_gap) * scale; + + float x = text->padding_x; + float y = text->current_y; // Start from the current render position + size_t line_start_byte = 0; + size_t line_start_cp = 0; + size_t last_word_break_byte = 0; // byte index *after* the space + size_t last_word_break_cp = 0; + float x_at_last_word_break = text->padding_x; + float max_width = text->width - text->padding_x; + + // We'll iterate the UTF-8 string and maintain both byte index and cp index + const std::string& s = params->text; + size_t byte_i = 0; + size_t cp_index = 0; + + while (byte_i < s.size()) { + size_t prev_byte = byte_i; + uint32_t cp = utf8_next(s, byte_i); + + // handle newline explicitly + if (cp == (uint32_t)'\n') { + // push line: [line_start_byte, prev_byte) -> includes upto but excluding newline + params->lines.push_back({ line_start_byte, prev_byte, line_start_cp, cp_index, y }); + y += line_height; + x = text->padding_x; + line_start_byte = byte_i; + line_start_cp = cp_index + 1; // next cp after newline + last_word_break_byte = byte_i; + last_word_break_cp = line_start_cp; + x_at_last_word_break = x; + ++cp_index; + continue; + } + + // store last word break pos + if (cp == (uint32_t)' ' || cp == (uint32_t)'\t') { + last_word_break_byte = byte_i; // byte index after this space + last_word_break_cp = cp_index + 1; + } + + // get advance for cp + const cached_glyph_t* glyph = get_glyph(text, cp, params->size); + int advance = glyph ? glyph->advance : 0; + + // check wrap before adding the char + if (x + advance > max_width && line_start_byte < prev_byte) { + // wrap + if (last_word_break_byte > line_start_byte) { + // wrap at last word break (exclude trailing space) + // end_byte = last_word_break_byte - bytes of space? last_word_break_byte already points after the space + // we want to exclude final space visually: line end byte = last_word_break_byte - size_of_space_byte_sequence + // But simpler: set end_byte to last_word_break_byte - 1 byte may not be correct for multi-byte, + // so we set end_cp = last_word_break_cp - 1 and then compute end_byte by walking from line_start_byte. + // But we already tracked last_word_break_byte and last_word_break_cp as byte AFTER the space; so we set + // end_byte = last_word_break_byte - bytes_of_space (unknown). To avoid complexity, we'll set end_byte = last_word_break_byte; and render lines excluding trailing space in drawing. + params->lines.push_back({ line_start_byte, last_word_break_byte, line_start_cp, last_word_break_cp, y }); + // re-start from after the space + // set byte_i to last_word_break_byte (already after space) + byte_i = last_word_break_byte; + cp_index = last_word_break_cp; + line_start_byte = last_word_break_byte; + line_start_cp = last_word_break_cp; + } + else { + // force wrap in middle of word: end at previous char + params->lines.push_back({ line_start_byte, prev_byte, line_start_cp, cp_index, y }); + // start new line at prev_byte (reprocess current char on new line) + byte_i = prev_byte; + // cp_index unchanged because we didn't consume this cp for new line + line_start_byte = prev_byte; + line_start_cp = cp_index; + } + + // reset for new line + y += line_height; + x = text->padding_x; + last_word_break_byte = line_start_byte; + last_word_break_cp = line_start_cp; + x_at_last_word_break = x; + continue; // loop will continue, not advancing cp_index here (we set it appropriately) + } + else { + // no wrap, just advance + x += advance; + ++cp_index; + // update x_at_last_word_break if this was space + if (last_word_break_cp == cp_index) { + x_at_last_word_break = x; + } + } + } + + // Add the final line if any text remains + if (line_start_byte < s.size()) { + params->lines.push_back({ line_start_byte, s.size(), line_start_cp, cp_index, y }); + } + + text->last_line_height = line_height; // Store for next block's y_offset + params->lines_computed = true; +} + +// Standard smoothstep function for SDF anti-aliasing. +static float smoothstep(float edge0, float edge1, float x) { + float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +// Standard alpha blending (source-over). +static uint32_t alpha_blend(uint32_t dest, uint32_t src) { + uint32_t src_a = (src >> 24) & 0xFF; + if (src_a == 0) return dest; + if (src_a == 255) return src; + + uint32_t dest_a = (dest >> 24) & 0xFF; + + uint32_t src_r = (src >> 16) & 0xFF; + uint32_t src_g = (src >> 8) & 0xFF; + uint32_t src_b = (src) & 0xFF; + + uint32_t dest_r = (dest >> 16) & 0xFF; + uint32_t dest_g = (dest >> 8) & 0xFF; + uint32_t dest_b = (dest) & 0xFF; + + uint32_t out_a = src_a + (dest_a * (255 - src_a) / 255); + if (out_a == 0) return 0; + + uint32_t out_r = (src_r * src_a + dest_r * dest_a * (255 - src_a) / 255) / out_a; + uint32_t out_g = (src_g * src_a + dest_g * dest_a * (255 - src_a) / 255) / out_a; + uint32_t out_b = (src_b * src_a + dest_b * dest_a * (255 - src_a) / 255) / out_a; + + return (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b; +} + +// Draws a glyph using its cached SDF bitmap. +static void draw_glyph(text_t* text, + int x, int y, + uint32_t color, + const cached_glyph_t* glyph, + float size, + bool outline) +{ + if (!glyph || !glyph->data) return; + + int x0 = x; + int y0 = y; + int x1 = x + glyph->w; + int y1 = y + glyph->h; + + // --- SDF Parameters --- + const float padding_percent = 0.15f; + const float onedge_value = 128.0f; + float padding = std::max(1.0f, size * padding_percent); + float pixel_dist_scale = onedge_value / padding; + // ---------------------- + + float outline_width = std::max(1.0f, size * 0.05f); + float aa_width = 0.75f; + + // Clip to framebuffer + int gx0 = (x0 < 0) ? -x0 : 0; + int gy0 = (y0 < 0) ? -y0 : 0; + int gx1 = (x1 > text->width) ? (glyph->w - (x1 - text->width)) : glyph->w; + int gy1 = (y1 > text->height) ? (glyph->h - (y1 - text->height)) : glyph->h; + + uint32_t color_rgb = color & 0x00FFFFFF; + + for (int gy = gy0; gy < gy1; ++gy) { + int fb_y = y0 + gy; + for (int gx = gx0; gx < gx1; ++gx) { + uint8_t sdf_raw = glyph->data[gy * glyph->w + gx]; + float dist = (sdf_raw - onedge_value) / pixel_dist_scale; + + float alpha = 0.0f; + if (outline) { + float edge_inner_0 = -aa_width; + float edge_inner_1 = +aa_width; + float mask_inner = smoothstep(edge_inner_0, edge_inner_1, dist); + + float edge_outer_0 = -outline_width - aa_width; + float edge_outer_1 = -outline_width + aa_width; + float mask_outer = smoothstep(edge_outer_0, edge_outer_1, dist); + + alpha = std::clamp(mask_outer - mask_inner, 0.0f, 1.0f); + } else { + alpha = smoothstep(-aa_width, aa_width, dist); + } + + if (alpha == 0.0f) continue; + + int fb_x = x0 + gx; + uint32_t* dest_pixel = &text->framebuffer[fb_y * text->width + fb_x]; + uint32_t src_pixel = ((uint32_t)(alpha * 255.0f) << 24) | color_rgb; + + *dest_pixel = alpha_blend(*dest_pixel, src_pixel); + } + } +} + +extern "C" { + +text_t* text_init(int width, int height) { + if (width <= 0 || height <= 0) return nullptr; + + text_t* text = new (std::nothrow) text_t; + if (!text) return nullptr; + + text->width = width; + text->height = height; + + text->framebuffer = new (std::nothrow) uint32_t[width * height]; + if (!text->framebuffer) { + delete text; + return nullptr; + } + + // Create new cache that this text_t owns + text->cache = new (std::nothrow) shared_cache_t; + if (!text->cache) { + delete[] text->framebuffer; + delete text; + return nullptr; + } + text->owns_cache = true; + + text->queue_index = 0; + text->char_index = 0; + text->char_progress = 0.0f; + text->current_font = nullptr; + text->last_line_height = 0.0f; + + text->current_x = text->padding_x; + text->current_y = text->padding_y; + + text_clear(text); + return text; +} + +text_t* text_init_shared(text_t* source, int width, int height) { + if (width <= 0 || height <= 0 || !source || !source->cache) return nullptr; + + text_t* text = new (std::nothrow) text_t; + if (!text) return nullptr; + + text->width = width; + text->height = height; + + text->framebuffer = new (std::nothrow) uint32_t[width * height]; + if (!text->framebuffer) { + delete text; + return nullptr; + } + + // Share the cache from source + text->cache = source->cache; + text->cache->refcount++; + text->owns_cache = false; + + text->queue_index = 0; + text->char_index = 0; + text->char_progress = 0.0f; + text->current_font = nullptr; + text->last_line_height = 0.0f; + + text->current_x = text->padding_x; + text->current_y = text->padding_y; + + text_clear(text); + return text; +} + +void text_destroy(text_t* text) { + if (!text) return; + + delete[] text->framebuffer; + + // Handle cache cleanup with reference counting + if (text->cache) { + text->cache->refcount--; + if (text->cache->refcount == 0) { + delete text->cache; + } + } + + delete text; +} + +// Sets the default font path and size to be used by subsequent text_push calls. +// Pass NULL for font_path to keep the current default font. +// Pass size <= 0 to keep the current default size. +void text_set_font(text_t* text, const char* font_path, float size) { + if (!text) return; + + if (font_path != nullptr) { + text->default_font_path = font_path; + } + + if (size > 0.0f) { + text->default_font_size = size; + } +} + +// Adds a block of text to the rendering queue. +void text_push(text_t* text, + const char* str, + float speed, + bool outline) +{ + if (!text || !str) return; + + text_params_t p; + p.text = str; + + p.size = text->default_font_size; + + p.speed = speed; + p.outline = outline; + + p.font_path = text->default_font_path; + + p.lines_computed = false; // Mark for precomputation + + text->text_queue.push_back(std::move(p)); +} + +// Clears the framebuffer and resets the text queue and all state. +void text_clear(text_t* text) { + if (!text) return; + + memset(text->framebuffer, 0, text->width * text->height * sizeof(uint32_t)); + + text->current_x = text->padding_x; + text->current_y = text->padding_y; + text->text_queue.clear(); + text->queue_index = 0; + text->char_index = 0; + text->char_progress = 0.0f; + text->last_line_height = 0.0f; +} + +// Immediately sets the character index to the end of the current text block. +void text_show_all_current(text_t* text) { + if (!text || text->queue_index >= text->text_queue.size()) return; + + text_params_t* params = &text->text_queue[text->queue_index]; + + size_t text_len_cp = utf8_count_codepoints(params->text); + + // Only update if it's not already complete + if (text->char_index < (int)text_len_cp) { + text->char_index = (int)text_len_cp; + text->char_progress = static_cast(text_len_cp); + } +} + +// Advances the text animation and renders new characters. +// deltatime is the time elapsed since the last frame (in seconds). +bool text_nextframe(text_t* text, float deltatime) { + if (!text) return false; + if (text->queue_index >= text->text_queue.size()) { + return false; // Nothing to do + } + + text_params_t* params = &text->text_queue[text->queue_index]; + + // Load font if needed + if (!params->font_path.empty()) { + cached_font_t* font = load_font(text, params->font_path); + if (font) text->current_font = font; + } + + if (!text->current_font) { + if (text->cache->font_cache.empty()) return false; + text->current_font = &text->cache->font_cache.begin()->second; + } + + // Precompute layout + precompute_line_breaks(text, params); + + const stbtt_fontinfo* info = &text->current_font->info; + float scale = stbtt_ScaleForPixelHeight(info, params->size); + int ascent, descent, line_gap; + stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap); + float baseline = ascent * scale; + + const size_t total_cp = utf8_count_codepoints(params->text); + + bool did_draw = false; + + // Update animation progress + if (params->speed > 0.0f && text->char_index < (int)total_cp) { + text->char_progress += params->speed * deltatime; + int new_index = static_cast(text->char_progress); + if (new_index > text->char_index) { + text->char_index = std::min(new_index, (int)total_cp); + did_draw = true; // Will draw new characters + } + } + else if (params->speed <= 0.0f) { + if (text->char_index < (int)total_cp) { + text->char_index = (int)total_cp; + did_draw = true; // Draw all instantly + } + } + + // Draw only new characters + size_t draw_start = text->last_rendered_char; + size_t draw_end = text->char_index; + + if (draw_start < draw_end) { + for (const auto& line : params->lines) { + if (line.end_cp_index <= draw_start) continue; + if (line.start_cp_index >= draw_end) break; + + text->current_x = text->padding_x; + text->current_y = line.y_offset; + + // Advance current_x to draw_start + size_t byte_i = line.start_byte_index; + size_t cp_i = line.start_cp_index; + while (byte_i < line.end_byte_index && cp_i < draw_start) { + uint32_t cp = utf8_next(params->text, byte_i); + const cached_glyph_t* glyph = get_glyph(text, cp, params->size); + if (glyph) text->current_x += glyph->advance; + ++cp_i; + } + + // Draw new characters + byte_i = line.start_byte_index; + cp_i = line.start_cp_index; + while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) { + utf8_next(params->text, byte_i); + ++cp_i; + } + + while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) { + uint32_t cp = utf8_next(params->text, byte_i); + if (cp == (uint32_t)'\n') { ++cp_i; continue; } + + const cached_glyph_t* glyph = get_glyph(text, cp, params->size); + if (!glyph) { ++cp_i; continue; } + + int x_base = static_cast(text->current_x + glyph->xoff); + int y_base = static_cast(text->current_y + baseline + glyph->yoff); + + if (params->outline) draw_glyph(text, x_base, y_base, params->color_outline, glyph, params->size, true); + draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false); + + text->current_x += glyph->advance; + ++cp_i; + } + } + + text->last_rendered_char = (int)text->char_index; + did_draw = true; + } + + // Handle block transition + if (text->char_index >= (int)total_cp) { + text->queue_index++; + text->char_index = 0; + text->char_progress = 0.0f; + text->last_rendered_char = 0; + if (text->queue_index > 1) text->current_y += text->last_line_height; + } + + // Return true if something was drawn or queue finished + if (did_draw || text->queue_index >= text->text_queue.size()) return true; + + return false; +} + +// Returns true if the currently running text block has not yet displayed all characters. +bool text_is_animating(text_t* text) { + if (!text) return false; + if (text->queue_index >= text->text_queue.size()) return false; + + const text_params_t* params = &text->text_queue[text->queue_index]; + size_t total_cp = utf8_count_codepoints(params->text); + return text->char_index < (int)total_cp; +} + +// Returns a pointer to the internal framebuffer. +const uint32_t* text_getfb(text_t* text) { + return text ? text->framebuffer : nullptr; +} + +} // extern "C" diff --git a/src/window.cpp b/src/window.cpp index 134af69..769cb93 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1,214 +1,214 @@ -// window.cpp -// This file implements the windowing and input system using SDL2. -// It provides a simple API for creating a window, blitting pixel data, and handling input events. -// Since Microsoft is retarded and forces us to live in 1985, we use a thread to manage the window -// to avoid freezing the app during window move or resize operations. Of course, normal OSes don't need that. -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#include -#include -#endif - -struct window_t { - SDL_Window* window; - SDL_Renderer* renderer; - SDL_Texture* texture; - int width; - int height; - - const uint8_t* keyboard_state; - int mouse_x; - int mouse_y; - uint32_t mouse_buttons; - -#ifdef _WIN32 - std::thread worker; - std::atomic running{true}; - std::atomic quit_requested{false}; - - std::mutex buffer_mutex; - uint32_t* shared_fb; - - std::mutex input_mutex; - uint8_t kb_copy[SDL_NUM_SCANCODES]; -#endif -}; - -// Internal helper for cleanup -static void window_cleanup(window_t* s) { - if (!s) return; - if (s->texture) SDL_DestroyTexture(s->texture); - if (s->renderer) SDL_DestroyRenderer(s->renderer); - if (s->window) SDL_DestroyWindow(s->window); -} - -#ifdef _WIN32 -// Internal function to perform the actual blit and present on the worker thread -static void win32_internal_render(window_t* s) { - if (!s->renderer || !s->texture) return; - - void* pixels; - int pitch; - if (SDL_LockTexture(s->texture, nullptr, &pixels, &pitch) == 0) { - { - std::lock_guard lock(s->buffer_mutex); - std::memcpy(pixels, s->shared_fb, s->width * s->height * 4); - } - SDL_UnlockTexture(s->texture); - } - - SDL_RenderClear(s->renderer); - SDL_RenderCopy(s->renderer, s->texture, nullptr, nullptr); - SDL_RenderPresent(s->renderer); -} - -// THE FIX: This callback is triggered by SDL even when Windows is in a modal drag loop. -static int SDLCALL win32_event_watch(void* userdata, SDL_Event* event) { - window_t* s = (window_t*)userdata; - if (event->type == SDL_WINDOWEVENT) { - if (event->window.event == SDL_WINDOWEVENT_MOVED || - event->window.event == SDL_WINDOWEVENT_RESIZED || - event->window.event == SDL_WINDOWEVENT_EXPOSED) { - win32_internal_render(s); - } - } - return 0; -} - -static void win32_window_worker(window_t* s, const char* title) { - SDL_InitSubSystem(SDL_INIT_VIDEO); - - s->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, s->width, s->height, 0); - s->renderer = SDL_CreateRenderer(s->window, -1, SDL_RENDERER_ACCELERATED); - s->texture = SDL_CreateTexture(s->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, s->width, s->height); - - // Register the watch to handle rendering during the drag-loop - SDL_AddEventWatch(win32_event_watch, s); - - while (s->running) { - SDL_Event ev; - // Use a timeout so we don't hog CPU but stay responsive - if (SDL_WaitEventTimeout(&ev, 8)) { - if (ev.type == SDL_QUIT) s->quit_requested = true; - } - - // Sync input state for the main thread - { - std::lock_guard lock(s->input_mutex); - const uint8_t* state = SDL_GetKeyboardState(nullptr); - std::memcpy(s->kb_copy, state, SDL_NUM_SCANCODES); - s->mouse_buttons = SDL_GetMouseState(&s->mouse_x, &s->mouse_y); - } - - // Normal frame render - win32_internal_render(s); - } - - SDL_DelEventWatch(win32_event_watch, s); - window_cleanup(s); - SDL_QuitSubSystem(SDL_INIT_VIDEO); -} -#endif - -extern "C" { - -window_t* window_create(int width, int height, const char* title) { -#ifdef _WIN32 - window_t* s = new window_t{}; - s->width = width; - s->height = height; - s->shared_fb = new uint32_t[width * height]; - s->keyboard_state = s->kb_copy; - - // Spawn the worker thread which owns the SDL context - s->worker = std::thread(win32_window_worker, s, title); - - // Wait briefly for the worker to initialize window/renderer pointers - while (s->running && !s->renderer) { std::this_thread::yield(); } - return s; -#else - // Original non-threaded implementation for Linux/others - if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) return nullptr; - window_t* s = new window_t{}; - s->width = width; - s->height = height; - s->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, 0); - s->renderer = SDL_CreateRenderer(s->window, -1, SDL_RENDERER_ACCELERATED); - s->texture = SDL_CreateTexture(s->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); - s->keyboard_state = SDL_GetKeyboardState(nullptr); - return s; -#endif -} - -void window_destroy(window_t* s) { - if (!s) return; -#ifdef _WIN32 - s->running = false; - if (s->worker.joinable()) s->worker.join(); - delete[] s->shared_fb; -#else - window_cleanup(s); - SDL_QuitSubSystem(SDL_INIT_VIDEO); -#endif - delete s; -} - -bool window_pollevent(window_t* s) { -#ifdef _WIN32 - // Main thread just checks the status reported by the worker - std::lock_guard lock(s->input_mutex); - return s->quit_requested; -#else - SDL_Event ev; - bool quit = false; - while (SDL_PollEvent(&ev)) { - if (ev.type == SDL_QUIT) quit = true; - } - s->mouse_buttons = SDL_GetMouseState(&s->mouse_x, &s->mouse_y); - return quit; -#endif -} - -void window_blit(window_t* s, const uint32_t* fb) { -#ifdef _WIN32 - // Just update the shared buffer; the worker thread will pick it up and present it - std::lock_guard lock(s->buffer_mutex); - std::memcpy(s->shared_fb, fb, s->width * s->height * 4); -#else - // Original immediate blit logic - void* pixels; int pitch; - SDL_LockTexture(s->texture, nullptr, &pixels, &pitch); - std::memcpy(pixels, fb, s->width * s->height * 4); - SDL_UnlockTexture(s->texture); - SDL_RenderClear(s->renderer); - SDL_RenderCopy(s->renderer, s->texture, nullptr, nullptr); - SDL_RenderPresent(s->renderer); -#endif -} - -// Input getters remain unchanged as they read from the struct's state fields -bool window_is_key_down(window_t* s, int sc) { - if (sc < 0 || sc >= SDL_NUM_SCANCODES) return false; - return s->keyboard_state[sc] != 0; -} - -bool window_is_mouse_button_down(window_t* s, int button) { - switch (button) { - case MOUSE_BUTTON_LEFT: return s->mouse_buttons & SDL_BUTTON_LMASK; - case MOUSE_BUTTON_MIDDLE: return s->mouse_buttons & SDL_BUTTON_MMASK; - case MOUSE_BUTTON_RIGHT: return s->mouse_buttons & SDL_BUTTON_RMASK; - } - return false; -} - -mouse_pos_t window_get_mouse_position(window_t* s) { - return { s->mouse_x, s->mouse_y }; -} - -} +// window.cpp +// This file implements the windowing and input system using SDL2. +// It provides a simple API for creating a window, blitting pixel data, and handling input events. +// Since Microsoft is retarded and forces us to live in 1985, we use a thread to manage the window +// to avoid freezing the app during window move or resize operations. Of course, normal OSes don't need that. +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#endif + +struct window_t { + SDL_Window* window; + SDL_Renderer* renderer; + SDL_Texture* texture; + int width; + int height; + + const uint8_t* keyboard_state; + int mouse_x; + int mouse_y; + uint32_t mouse_buttons; + +#ifdef _WIN32 + std::thread worker; + std::atomic running{true}; + std::atomic quit_requested{false}; + + std::mutex buffer_mutex; + uint32_t* shared_fb; + + std::mutex input_mutex; + uint8_t kb_copy[SDL_NUM_SCANCODES]; +#endif +}; + +// Internal helper for cleanup +static void window_cleanup(window_t* s) { + if (!s) return; + if (s->texture) SDL_DestroyTexture(s->texture); + if (s->renderer) SDL_DestroyRenderer(s->renderer); + if (s->window) SDL_DestroyWindow(s->window); +} + +#ifdef _WIN32 +// Internal function to perform the actual blit and present on the worker thread +static void win32_internal_render(window_t* s) { + if (!s->renderer || !s->texture) return; + + void* pixels; + int pitch; + if (SDL_LockTexture(s->texture, nullptr, &pixels, &pitch) == 0) { + { + std::lock_guard lock(s->buffer_mutex); + std::memcpy(pixels, s->shared_fb, s->width * s->height * 4); + } + SDL_UnlockTexture(s->texture); + } + + SDL_RenderClear(s->renderer); + SDL_RenderCopy(s->renderer, s->texture, nullptr, nullptr); + SDL_RenderPresent(s->renderer); +} + +// THE FIX: This callback is triggered by SDL even when Windows is in a modal drag loop. +static int SDLCALL win32_event_watch(void* userdata, SDL_Event* event) { + window_t* s = (window_t*)userdata; + if (event->type == SDL_WINDOWEVENT) { + if (event->window.event == SDL_WINDOWEVENT_MOVED || + event->window.event == SDL_WINDOWEVENT_RESIZED || + event->window.event == SDL_WINDOWEVENT_EXPOSED) { + win32_internal_render(s); + } + } + return 0; +} + +static void win32_window_worker(window_t* s, const char* title) { + SDL_InitSubSystem(SDL_INIT_VIDEO); + + s->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, s->width, s->height, 0); + s->renderer = SDL_CreateRenderer(s->window, -1, SDL_RENDERER_ACCELERATED); + s->texture = SDL_CreateTexture(s->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, s->width, s->height); + + // Register the watch to handle rendering during the drag-loop + SDL_AddEventWatch(win32_event_watch, s); + + while (s->running) { + SDL_Event ev; + // Use a timeout so we don't hog CPU but stay responsive + if (SDL_WaitEventTimeout(&ev, 8)) { + if (ev.type == SDL_QUIT) s->quit_requested = true; + } + + // Sync input state for the main thread + { + std::lock_guard lock(s->input_mutex); + const uint8_t* state = SDL_GetKeyboardState(nullptr); + std::memcpy(s->kb_copy, state, SDL_NUM_SCANCODES); + s->mouse_buttons = SDL_GetMouseState(&s->mouse_x, &s->mouse_y); + } + + // Normal frame render + win32_internal_render(s); + } + + SDL_DelEventWatch(win32_event_watch, s); + window_cleanup(s); + SDL_QuitSubSystem(SDL_INIT_VIDEO); +} +#endif + +extern "C" { + +window_t* window_create(int width, int height, const char* title) { +#ifdef _WIN32 + window_t* s = new window_t{}; + s->width = width; + s->height = height; + s->shared_fb = new uint32_t[width * height]; + s->keyboard_state = s->kb_copy; + + // Spawn the worker thread which owns the SDL context + s->worker = std::thread(win32_window_worker, s, title); + + // Wait briefly for the worker to initialize window/renderer pointers + while (s->running && !s->renderer) { std::this_thread::yield(); } + return s; +#else + // Original non-threaded implementation for Linux/others + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) return nullptr; + window_t* s = new window_t{}; + s->width = width; + s->height = height; + s->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, 0); + s->renderer = SDL_CreateRenderer(s->window, -1, SDL_RENDERER_ACCELERATED); + s->texture = SDL_CreateTexture(s->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); + s->keyboard_state = SDL_GetKeyboardState(nullptr); + return s; +#endif +} + +void window_destroy(window_t* s) { + if (!s) return; +#ifdef _WIN32 + s->running = false; + if (s->worker.joinable()) s->worker.join(); + delete[] s->shared_fb; +#else + window_cleanup(s); + SDL_QuitSubSystem(SDL_INIT_VIDEO); +#endif + delete s; +} + +bool window_pollevent(window_t* s) { +#ifdef _WIN32 + // Main thread just checks the status reported by the worker + std::lock_guard lock(s->input_mutex); + return s->quit_requested; +#else + SDL_Event ev; + bool quit = false; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_QUIT) quit = true; + } + s->mouse_buttons = SDL_GetMouseState(&s->mouse_x, &s->mouse_y); + return quit; +#endif +} + +void window_blit(window_t* s, const uint32_t* fb) { +#ifdef _WIN32 + // Just update the shared buffer; the worker thread will pick it up and present it + std::lock_guard lock(s->buffer_mutex); + std::memcpy(s->shared_fb, fb, s->width * s->height * 4); +#else + // Original immediate blit logic + void* pixels; int pitch; + SDL_LockTexture(s->texture, nullptr, &pixels, &pitch); + std::memcpy(pixels, fb, s->width * s->height * 4); + SDL_UnlockTexture(s->texture); + SDL_RenderClear(s->renderer); + SDL_RenderCopy(s->renderer, s->texture, nullptr, nullptr); + SDL_RenderPresent(s->renderer); +#endif +} + +// Input getters remain unchanged as they read from the struct's state fields +bool window_is_key_down(window_t* s, int sc) { + if (sc < 0 || sc >= SDL_NUM_SCANCODES) return false; + return s->keyboard_state[sc] != 0; +} + +bool window_is_mouse_button_down(window_t* s, int button) { + switch (button) { + case MOUSE_BUTTON_LEFT: return s->mouse_buttons & SDL_BUTTON_LMASK; + case MOUSE_BUTTON_MIDDLE: return s->mouse_buttons & SDL_BUTTON_MMASK; + case MOUSE_BUTTON_RIGHT: return s->mouse_buttons & SDL_BUTTON_RMASK; + } + return false; +} + +mouse_pos_t window_get_mouse_position(window_t* s) { + return { s->mouse_x, s->mouse_y }; +} + +}