From ca73b12180ad0dd38e73f19bab81807a43460451 Mon Sep 17 00:00:00 2001 From: Mitya Selivanov Date: Sun, 20 Apr 2025 18:23:01 +0200 Subject: Rename to Runtime --- runtime.c | 4998 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4998 insertions(+) create mode 100755 runtime.c (limited to 'runtime.c') diff --git a/runtime.c b/runtime.c new file mode 100755 index 0000000..03a50eb --- /dev/null +++ b/runtime.c @@ -0,0 +1,4998 @@ +#if 0 /* +#/ ================================================================ +#/ +#/ runtime.c +#/ +#/ This is a reduced system layer. +#/ It allows you to create a window, draw graphics in it, handle +#/ input events, write samples to audio output, send and receive +#/ UDP packets, etc. All code is single-threaded. +#/ +#/ Primary target platforms: Linux (X11), Windows, Web. +#/ +#/ ---------------------------------------------------------------- +#/ +#/ DESIGN PRINCIPLES +#/ +#/ - Minimalistic feature set. For graphics, you have access to the +#/ pixel buffer, and that's it. +#/ +#/ - No implicit control flow. No callbacks. You write your own +#/ main and call everything explicitly. But the number of things +#/ you have to call to do something is as little as possible. +#/ +#/ - Optimized to use in a single source file. +#/ Installation process? Ctrl+C, Ctrl+V, done. +#/ +#/ STYLE CONVENTIONS +#/ +#/ - Pascal_Snake_Case - Type name. +#/ - snake_case - Non-type name. +#/ - UPPER_SNAKE_CASE - Macro or constant. +#/ +#/ - g_ prefix - Global variable name. +#/ - _ prefix - Name of a global variable that is not part of the user API. +#/ - _ suffix - Name of a procedure that is not part of the user API. +#/ +#/ Most procedures have long and descriptive names. +#/ Some procedures have prefixes according to their domain. +#/ +#/ There may be exceptions if convenient. +#/ +#/ ---------------------------------------------------------------- +#/ +#/ To-Do list +#/ +#/ - Work in progress +#/ - Graphics perf - request cache +#/ Requires: +#/ + [ ] Graphics tests +#/ + [x] Graphics requests +#/ + [x] Memory buffer allocator +#/ + [x] Blake2 hash +#/ + [x] Requests cache +#/ + [x] Global anti-aliasing +#/ + [x] Fill triangles +#/ - Examples +#/ - Conway's Game of Life +#/ - Julia Set +#/ - Labyrinth +#/ - Chat +#/ - Graphics +#/ - Gamma correction +#/ - Custom fonts - integrate stb_truetype.h +#/ - UI +#/ - Icons +#/ - Folder widget +#/ - Clipboard +#/ - Images - BMP, PPM +#/ - Sound - WAV +#/ - Clipboard +#/ - Images - BMP, PPM +#/ - Sound - WAV +#/ - Dynamic libraries - load dependencies conditionally +#/ - X11 +#/ - Wayland +#/ - ALSA +#/ - Sockets +#/ - Windows +#/ - Cross-platform networking - UDP + TCP + WebSocket +#/ Requires: +#/ - [ ] Sockets - UDP, TCP +#/ - [ ] HTTP client +#/ - [ ] HTTP server +#/ - [ ] Web sockets +#/ - [ ] Key exchange +#/ - [ ] Cipher suite - TLS_AES_128_GCM_SHA256 +#/ - [ ] TLS +#/ - [ ] Web sockets over TLS +#/ - Long term +#/ - Utility +#/ - Improve microbenchmarks library +#/ - Parsing +#/ - Printing +#/ - Logging +#/ - Terminal colors +#/ - Big integer +#/ - Mersenne Twister 64 +#/ - Arithmetic coding +#/ - A* search +#/ - Graphics +#/ - Vector math +#/ - Bezier curves +#/ - CMYK color +#/ - Textures +#/ - Vulkan boilerplate +#/ - System +#/ - Window +#/ - Windows graphics +#/ - Wayland +#/ - Sound +#/ - Windows audio +#/ - Recording +#/ - Device selection +#/ - Networking +#/ - Windows sockets +#/ - fetch - via libcurl on native platforms +#/ - Lattice-based cipher suite +#/ - Switching canvas - Web +#/ - File system +#/ - Secure random +#/ - Process +#/ - Shared memory +#/ - Shared mutex +#/ - Threads - https://nachtimwald.com/2019/04/05/cross-platform-thread-wrapper +#/ - Cryptography - https://github.com/jedisct1/supercop +#/ - macOS support +#/ - Mobile devices support +#/ +#/ Done +#/ +#/ - Examples +#/ - Echo +#/ - UI +#/ - Particles +#/ - Graph +#/ - Sine Wave +#/ - Utility +#/ - UTF-8 +#/ - Testing +#/ - Stackless coroutines +#/ - Allocator +#/ - Profiling +#/ - Graphics +#/ - Font +#/ - Adaptive resolution +#/ - Oklab color +#/ - Relative coordinates +#/ - Alpha blending +#/ - Self-contained impl +#/ - Anti-aliasing +#/ - System +#/ - Window - X11, Web +#/ - Screenshot - X11, Wayland +#/ - Clipboard +#/ - Text - X11, Web +#/ - Sound - ALSA, Web +#/ - Networking +#/ - Unix UDP sockets +#/ - Drop files - X11, Web +#/ +#/ ---------------------------------------------------------------- +#/ +#/ (C) 2025 Mitya Selivanov +#/ +#/ Any use of this code is prohibited. +#/ +#/ ================================================================ +#/ +#/ Self-testing shell script +#/ +#/ ================================================================ + +SRC=${0##*./} +BIN=${SRC%.*} +gcc \ + -Wall -Wextra -Werror -pedantic \ + -Wno-missing-braces \ + -Wno-old-style-declaration \ + -Wno-overlength-strings \ + -Wno-unused-parameter \ + -Wno-unused-variable \ + -Wno-unused-but-set-variable \ + -O3 -D NDEBUG \ + -fsanitize=undefined,address,leak \ + -D RUNTIME_TEST_SUITE \ + -lm -lX11 -lasound \ + -lwayland-client \ + -o $BIN $SRC && \ + ./$BIN $@ +STATUS=$? +rm -f $BIN +exit $STATUS # */ +#endif + +// ================================================================ +// +// Types +// +// ================================================================ + +#ifndef TYPES_HEADER_GUARD_ +#define TYPES_HEADER_GUARD_ + +typedef signed char i8; +typedef signed short i16; +typedef signed i32; +typedef signed long long i64; +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned u32; +typedef unsigned long long u64; +typedef char c8; +typedef int c32; +typedef unsigned char b8; +typedef float f32; +typedef double f64; + +typedef union { struct { f64 v[ 2]; }; struct { f64 x, y; }; } vec2; +typedef union { struct { f64 v[ 3]; }; struct { f64 x, y, z; }; } vec3; +typedef union { struct { f64 v[ 4]; }; struct { f64 x, y, z, w; }; } vec4; +typedef union { struct { f32 v[ 2]; }; struct { f32 x, y; }; } vec2_f32; +typedef union { struct { f32 v[ 3]; }; struct { f32 x, y, z; }; } vec3_f32; +typedef union { struct { f32 v[ 4]; }; struct { f32 x, y, z, w; }; } vec4_f32; +typedef union { struct { i64 v[ 2]; }; struct { i64 x, y; }; } vec2_i64; +typedef union { struct { i64 v[ 3]; }; struct { i64 x, y, z; }; } vec3_i64; +typedef union { struct { i64 v[ 4]; }; struct { i64 x, y, z, w; }; } vec4_i64; +typedef union { struct { f64 v[ 4]; }; struct { f64 m[2][2]; }; } mat2; +typedef union { struct { f64 v[ 9]; }; struct { f64 m[3][3]; }; } mat3; +typedef union { struct { f64 v[16]; }; struct { f64 m[4][4]; }; } mat4; +typedef union { struct { f32 v[ 4]; }; struct { f32 m[2][2]; }; } mat2_f32; +typedef union { struct { f32 v[ 9]; }; struct { f32 m[3][3]; }; } mat3_f32; +typedef union { struct { f32 v[16]; }; struct { f32 m[4][4]; }; } mat4_f32; + +#endif // TYPES_HEADER_GUARD_ + +// ================================================================ + +#ifndef RUNTIME_HEADER_GUARD_ +#define RUNTIME_HEADER_GUARD_ + +#ifdef EVERY_TEST_SUITE +#define RUNTIME_TEST_SUITE +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// ================================================================ +// +// User program interface +// +// ================================================================ + +// NOTE: This procedure is required for the Web compatibility. +void update_and_render_frame(void); + +#if defined(__wasm__) +i32 main(i32 argc, c8 **argv); +#endif + +// ================================================================ +// +// Options +// +// ================================================================ + +#ifndef ENABLE_WAYLAND +#define ENABLE_WAYLAND 1 +#endif + +#ifndef ENABLE_X11 +#define ENABLE_X11 1 +#endif + +#ifndef ENABLE_ALSA +#define ENABLE_ALSA 1 +#endif + +#ifndef STATIC_MEMORY_BUFFER_SIZE +#define STATIC_MEMORY_BUFFER_SIZE (400 * 1024 * 1024) +#endif + +#ifndef MEMORY_CHUNK_SIZE +#define MEMORY_CHUNK_SIZE 1024 +#endif + +#ifndef DEFAULT_ANTIALIASING_SCALE +#define DEFAULT_ANTIALIASING_SCALE 2 +#endif + +#ifndef MIN_PIXEL_SIZE +#define MIN_PIXEL_SIZE (1.0) +#endif + +#ifndef MAX_PIXEL_SIZE +#define MAX_PIXEL_SIZE (40.0) +#endif + +#ifndef DEFAULT_PIXEL_SIZE +#define DEFAULT_PIXEL_SIZE (8.0) +#endif + +#ifndef PIXEL_SIZE_DELTA +#define PIXEL_SIZE_DELTA (0.05) +#endif + +// The pixel size will decrease when the frame duration is lower. +#ifndef MIN_FRAME_DURATION +#define MIN_FRAME_DURATION 17 +#endif + +// The pixel size will increase when the frame duration is higher. +#ifndef MAX_FRAME_DURATION +#if defined(__wasm__) +#define MAX_FRAME_DURATION 33 +#else +#define MAX_FRAME_DURATION 83 +#endif +#endif + +// The pixel size value will reset if the frame duration is higher. +#ifndef FRAME_DURATION_HARD_LIMIT +#define FRAME_DURATION_HARD_LIMIT 200 +#endif + +#ifndef NUM_FRAMES_AVERAGED +#define NUM_FRAMES_AVERAGED 600 +#endif + +#ifndef AVERAGE_FRAME_BIAS +#define AVERAGE_FRAME_BIAS (0.03) +#endif + +#ifndef MAX_NUM_KEYS +#define MAX_NUM_KEYS 512 +#endif + +#ifndef NUM_PRIMARY_SOUND_CHANNELS +#define NUM_PRIMARY_SOUND_CHANNELS 2 +#endif + +#ifndef PRIMARY_SOUND_SAMPLE_RATE +#define PRIMARY_SOUND_SAMPLE_RATE 44100 +#endif + +// TODO: Implement dynamic primary sound buffer. +#ifndef MAX_NUM_PRIMARY_SOUND_FRAMES +#define MAX_NUM_PRIMARY_SOUND_FRAMES (10 * PRIMARY_SOUND_SAMPLE_RATE * NUM_PRIMARY_SOUND_CHANNELS) +#endif + +#ifndef PRIMARY_SOUND_AVAIL_MIN +#define PRIMARY_SOUND_AVAIL_MIN 512 +#endif + +#ifndef MOUSE_WHEEL_FACTOR +#define MOUSE_WHEEL_FACTOR (1.0) +#endif + +// ================================================================ +// +// Basic declarations +// +// ================================================================ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#ifndef NULL +#define NULL ((void *) 0) +#endif + +// ================================================================ +// +// Math +// +// ================================================================ + +#if !defined(__wasm__) +#include +#else +f64 floor(f64 x); +f64 ceil(f64 x); +f64 trunc(f64 x); +f64 sqrt(f64 x); +f64 cbrt(f64 x); +f64 pow(f64 x, f64 y); +f64 log(f64 x); +f64 log2(f64 x); +f64 log10(f64 x); +f64 exp(f64 x); +f64 sin(f64 x); +f64 cos(f64 x); +f64 tan(f64 x); +f64 asin(f64 x); +f64 acos(f64 x); +f64 atan(f64 x); +f64 atan2(f64 y, f64 x); +#endif + +#ifndef M_PI +#define M_PI (3.14159265358979323846) +#endif + +#ifndef M_E +#define M_E (2.71828182845904523536) +#endif + +#ifndef M_LOG_2 +#define M_LOG_2 (0.69314718055994530941) +#endif + +#ifndef M_LOG_10 +#define M_LOG_10 (2.30258509299404568402) +#endif + +#ifndef M_LOG2_E +#define M_LOG2_E (1.44269504088896340736) +#endif + +#ifndef M_LOG10_E +#define M_LOG10_E (0.43429448190325182765) +#endif + +#ifndef M_LOG_PI +#define M_LOG_PI (1.14472988584940017414) +#endif + +#ifndef M_SQRT_2 +#define M_SQRT_2 (1.41421356237309504880) +#endif + +#ifndef M_SQRT_3 +#define M_SQRT_3 (1.73205080756887729352) +#endif + +#ifndef M_SQRT_PI +#define M_SQRT_PI (1.77245385090551602730) +#endif + +#ifndef M_SQRT_E +#define M_SQRT_E (1.64872127070012814685) +#endif + +#ifndef M_CBRT_2 +#define M_CBRT_2 (1.25992104989487316476) +#endif + +#ifndef M_CBRT_3 +#define M_CBRT_3 (1.44224957030740838232) +#endif + +#ifndef M_CBRT_PI +#define M_CBRT_PI (1.46459188756152326302) +#endif + +#ifndef M_CBRT_E +#define M_CBRT_E (1.39561242508608952863) +#endif + +// ================================================================ +// +// PLATFORM API +// +// ================================================================ + +enum { + IPv4_UDP = 1, + IPv6_UDP = 2, +}; + +enum { + MOD_CTRL = 1, + MOD_SHIFT = 2, + MOD_ALT = 3, + MOD_CAPS = 4, + MOD_NUM = 5, + MOD_SCROLL = 6, + MOD_META = 7, + KEY_BACKSPACE = 8, + KEY_TAB = 9, + KEY_ENTER = 10, + KEY_LCTRL = 11, + KEY_RCTRL = 12, + KEY_LSHIFT = 13, + KEY_RSHIFT = 14, + KEY_LALT = 15, + KEY_RALT = 16, + KEY_LEFT = 17, + KEY_RIGHT = 18, + KEY_UP = 19, + KEY_DOWN = 20, + KEY_PAUSE = 21, + KEY_INSERT = 22, + KEY_HOME = 23, + KEY_END = 24, + KEY_PAGEUP = 25, + KEY_PAGEDOWN = 26, + KEY_ESCAPE = 27, + KEY_PRINTSCREEN = 28, + BUTTON_LEFT = 29, + BUTTON_MIDDLE = 30, + BUTTON_RIGHT = 31, + KEY_SPACE = 32, + KEY_LMETA = 33, + KEY_RMETA = 34, + KEY_QUOTE = 39, + KEY_COMMA = 44, + KEY_MINUS = 45, + KEY_PERIOD = 46, + KEY_SLASH = 47, + KEY_0 = 48, + KEY_1 = 49, + KEY_2 = 50, + KEY_3 = 51, + KEY_4 = 52, + KEY_5 = 53, + KEY_6 = 54, + KEY_7 = 55, + KEY_8 = 56, + KEY_9 = 57, + KEY_COLON = 59, + KEY_EQUAL = 61, + KEY_LBRACE = 91, + KEY_BACKSLASH = 92, + KEY_RBRACE = 93, + KEY_TILDE = 96, + KEY_A = 97, + KEY_B = 98, + KEY_C = 99, + KEY_D = 100, + KEY_E = 101, + KEY_F = 102, + KEY_G = 103, + KEY_H = 104, + KEY_I = 105, + KEY_J = 106, + KEY_K = 107, + KEY_L = 108, + KEY_M = 109, + KEY_N = 110, + KEY_O = 111, + KEY_P = 112, + KEY_Q = 113, + KEY_R = 114, + KEY_S = 115, + KEY_T = 116, + KEY_U = 117, + KEY_V = 118, + KEY_W = 119, + KEY_X = 120, + KEY_Y = 121, + KEY_Z = 122, + KEY_DELETE = 127, + KEY_KP_0 = 128, + KEY_KP_1 = 129, + KEY_KP_2 = 130, + KEY_KP_3 = 131, + KEY_KP_4 = 132, + KEY_KP_5 = 133, + KEY_KP_6 = 134, + KEY_KP_7 = 135, + KEY_KP_8 = 136, + KEY_KP_9 = 137, + KEY_KP_ENTER = 138, + KEY_KP_DIVIDE = 139, + KEY_KP_MULTIPLY = 140, + KEY_KP_ADD = 141, + KEY_KP_SUBTRACT = 142, + KEY_KP_DECIMAL = 143, + KEY_KP_SEPARATOR = 144, + KEY_F1 = 145, + KEY_F2 = 146, + KEY_F3 = 147, + KEY_F4 = 148, + KEY_F5 = 149, + KEY_F6 = 150, + KEY_F7 = 151, + KEY_F8 = 152, + KEY_F9 = 153, + KEY_F10 = 154, + KEY_F11 = 155, + KEY_F12 = 156, + KEY_F13 = 157, + KEY_F14 = 158, + KEY_F15 = 159, + KEY_F16 = 160, + KEY_F17 = 161, + KEY_F18 = 162, + KEY_F19 = 163, + KEY_F20 = 164, + KEY_F21 = 165, + KEY_F22 = 166, + KEY_F23 = 167, + KEY_F24 = 168, +}; + +typedef struct { + b8 ctrl : 1; + b8 shift : 1; + b8 alt : 1; + b8 caps : 1; + b8 num : 1; + b8 scroll : 1; + b8 meta : 1; + b8 repeat : 1; + u16 key; + c32 c; +} Input_Key; + +typedef struct { + i64 name_len; + c8 *name; + i64 data_size; + u8 *data; +} Drop_File; + +typedef struct { + c32 code; + i8 len; +} UTF8_Char; + +typedef struct { + u16 protocol; + u16 port; + union { + u32 v4_address_as_u32; + u8 v4_address[4]; + u8 v6_address[16]; + }; +} Network_Address; + +typedef struct { + c8 *title; + i32 frame_width; + i32 frame_height; + f64 pixel_size; + i32 antialiasing_scale; + + b8 exact_resolution : 1; + b8 disable_gamma : 1; // TODO + b8 graceful_shutdown : 1; + b8 enable_clipboard_text : 1; + b8 enable_clipboard_image : 1; + b8 enable_clipboard_sound : 1; + + b8 done : 1; + b8 has_focus : 1; + b8 has_cursor : 1; + b8 files_dragged : 1; + b8 files_dropped : 1; + + i32 real_width; + i32 real_height; + i32 cursor_x; + i32 cursor_y; + i32 cursor_dx; + i32 cursor_dy; + f64 wheel_dx; + f64 wheel_dy; + i64 num_sound_samples_elapsed; + + i64 num_pixels; + i64 input_len; + i64 input_capacity; + i64 num_drop_files; + i64 clipboard_text_len; + i64 clipboard_image_width; + i64 clipboard_image_height; + i64 clipboard_image_len; + i64 num_clipboard_sound_samples; + i64 clipboard_sound_len; + + vec4_f32 *pixels; + Input_Key *input; + Drop_File *drop_files; + c8 *clipboard_text; + vec4_f32 *clipboard_image; + f32 *clipboard_sound; + + b8 key_down [MAX_NUM_KEYS]; + b8 key_pressed [MAX_NUM_KEYS]; + + i64 memory_buffer_size; + u8 *memory_buffer; +} Platform; + +// UTF-8 +// NOTE: We need UTF-8 because we use Xutf8LookupString on Xlib. +i8 utf8_size (c32 c); +UTF8_Char utf8_read (i64 len, c8 *s); +i8 utf8_write(c32 c, c8 *buffer); + +// Time and sleep +i64 current_utc_time_in_milliseconds(void); +void current_utc_time_in_sec_and_nsec(i64 *seconds, i64 *nanoseconds); +void yield_thread_execution(void); +void suspend_thread_for_milliseconds(i64 duration); + +// Window +void init_main_window(void); +i32 handle_main_window_events(void); +i32 wait_main_window_events(void); +void render_main_window_frame(void); + +// Convenient helper procedure for the event loop +void run_main_window_event_loop(void); + +// Clipboard +void write_clipboard_text(i64 size, c8 *data); +void write_clipboard_image(i64 width, i64 height, vec4_f32 *pixels); +void write_clipboard_sound(i8 num_channels, i64 num_samples, f32 *frames); + +// Take a screenshot +void take_screenshot(i64 *width, i64 *height, i64 max_num_pixels, vec4_f32 *pixels); + +// Sound +void handle_primary_sound(void); +void queue_primary_sound(i64 delay_in_samples, i64 num_samples, f32 *frames); + +// Networking +b8 network_open(u16 slot, Network_Address address, u16 *local_port); +i64 network_recv(u16 slot, Network_Address address, i64 size, u8 *data, u16 *local_port, Network_Address *remote_address); +i64 network_send(u16 slot, Network_Address address, i64 size, u8 *data, u16 *local_port); + +// Dynamic libraries +void dynamic_library_open (u16 slot, c8 *name); +void *dynamic_library_get_proc_address(u16 slot, c8 *proc); + +// Profiling +void PROFILER_init(u32 slot, c8 *name); +void PROFILER_begin(u32 slot); +void PROFILER_end(u32 slot); + +// NOTE: This will shutdown main window, networking, audio, +// and close all opened dynamic libraries. +// If g_platform.graceful_shutdown is 0, this does nothing. +void shutdown_all_systems(void); + +// Memory +void *memory_buffer_allocate(i64 size, i64 alignment, i64 previous_size, void *previous_data); +void resize_dynamic_array_exact(i64 *num, void **data, i64 element_size, i64 new_num); +void resize_dynamic_array_capacity(i64 *num, i64 *capacity, void **data, i64 element_size, i64 new_num); + +#ifdef RUNTIME_HEADER +extern Platform g_platform; +#endif + +// ================================================================ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // RUNTIME_HEADER_GUARD_ + +// ================================================================ +// +// PLATFORM INDEPENDENT CODE +// +// ================================================================ + +#ifndef RUNTIME_HEADER +#ifndef RUNTIME_IMPL_GUARD_ +#define RUNTIME_IMPL_GUARD_ + +Platform g_platform = {0}; + +enum { + PROFILE_MEMORY, + PROFILE_DOWNSCALE, + PROFILE_RESIZE, + PROFILE_WAITING, + PROFILE_FRAME, + PROFILE_DRAW_PIXELS, + PROFILE_FILL_RECTANGLE, + PROFILE_FILL_TRIANGLE, + PROFILE_FILL_TRIANGLES, + PROFILE_FILL_QUAD, + PROFILE_FILL_ELLIPSE, + PROFILE_FILL_LINE, + PROFILE_DRAW_TEXT_AREA, + PROFILE_DRAW_TEXT_CURSOR, +}; + +// ================================================================ +// +// Misc. +// +// ================================================================ + +static void mem_set_(void *dst, u8 x, i64 size) { + for (i64 i = 0; i < size; ++i) + ((u8 *) dst)[i] = x; +} + +static void mem_cpy_(void *dst, void *src, i64 size) { + for (i64 i = 0; i < size; ++i) + ((u8 *) dst)[i] = ((u8 *) src)[i]; +} + +static i32 min2_i32_(i32 x, i32 y) { + return x < y ? x : y; +} + +static i64 max2_i64_(i64 x, i64 y) { + return x > y ? x : y; +} + +// ================================================================ +// +// Log +// +// ================================================================ + +// TODO: +// - Use print formatting after we implement it. +// - Print time. + +enum { + LOG_Level_None = 0, + LOG_Level_Fatal = 1, + LOG_Level_Error = 2, + LOG_Level_Warning = 3, + LOG_Level_Info = 4, + LOG_Level_Verbose = 5, + LOG_Level_Trace = 6, +}; + +static u8 LOG_level = LOG_Level_Info; + +#if defined(__wasm__) +void log_impl(i32 mode, i32 file_len, c8 const *file, i32 line, i32 func_len, c8 const *func, i32 text_len, c8 const *text); +#define LOG_trace() \ + do { \ + if (LOG_level >= LOG_Level_Trace) \ + log_impl( \ + 0, \ + sizeof(__FILE__) - 1, __FILE__, \ + __LINE__, \ + sizeof(__func__) - 1, __func__, \ + 0, NULL); \ + } while (0) +#define LOG_print(text_, ...) \ + do { \ + if (LOG_level >= LOG_Level_Info) { \ + i32 len = 0; \ + while ((text_)[len] != '\0') ++len; \ + log_impl( \ + 0, \ + sizeof(__FILE__) - 1, __FILE__, \ + __LINE__, \ + sizeof(__func__) - 1, __func__, \ + len, text_); \ + } \ + } while (0) +#define LOG_error(text_, ...) \ + do { \ + if (LOG_level >= LOG_Level_Error) { \ + i32 len = 0; \ + while ((text_)[len] != '\0') ++len; \ + log_impl( \ + 1, \ + sizeof(__FILE__) - 1, __FILE__, \ + __LINE__, \ + sizeof(__func__) - 1, __func__, \ + len, text_); \ + } \ + } while (0) +#else +#include +#define LOG_trace() \ + do { \ + if (LOG_level >= LOG_Level_Trace) \ + fprintf(stdout, "%s:%d, %s\n", __FILE__, __LINE__, __func__); \ + } while (0) +#define LOG_print(...) \ + do { \ + if (LOG_level >= LOG_Level_Info) { \ + fprintf(stdout, __VA_ARGS__); \ + fprintf(stdout, "\n"); \ + } \ + } while (0) +#define LOG_error(...) \ + do { \ + if (LOG_level >= LOG_Level_Error) { \ + fprintf(stderr, "%s:%d, %s: ", __FILE__, __LINE__, __func__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } \ + } while (0) +#endif + +// ================================================================ +// +// BLAKE2B +// +// ================================================================ + +enum blake2b_constant { + BLAKE2B_BLOCKBYTES = 128, + BLAKE2B_OUTBYTES = 64, + BLAKE2B_KEYBYTES = 64, + BLAKE2B_SALTBYTES = 16, + BLAKE2B_PERSONALBYTES = 16 +}; + +typedef struct { + u8 digest_length; + u8 key_length; + u8 fanout; + u8 depth; + u32 leaf_length; + u64 node_offset; + u8 node_depth; + u8 inner_length; + u8 reserved[14]; + u8 salt[BLAKE2B_SALTBYTES]; + u8 personal[BLAKE2B_PERSONALBYTES]; +} Blake2b_Param; + +typedef struct { + u64 h[8]; + u64 t[2]; + u64 f[2]; + u8 buf[2 * BLAKE2B_BLOCKBYTES]; + u64 buflen; + u8 last_node; +} Blake2b_State; + +static u64 blake2b_IV[8] = { + 0x6a09e667f3bcc908ull, 0xbb67ae8584caa73bull, + 0x3c6ef372fe94f82bull, 0xa54ff53a5f1d36f1ull, + 0x510e527fade682d1ull, 0x9b05688c2b3e6c1full, + 0x1f83d9abfb41bd6bull, 0x5be0cd19137e2179ull +}; + +static u8 blake2b_sigma[12][16] = { + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } , + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } , + { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 } , + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 } , + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 } , + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 } , + { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 } , + { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 } , + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 } , + { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13 , 0 } , + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } , + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } +}; + +static b8 native_little_endian_(void) { + return ((u8 *) &(u32) { 1 })[0] == 1; +} + +static u64 load64_(void *src) { + if (native_little_endian_()) + return *( u64 * )( src ); + else { + u8 *p = ( u8 * )src; + u64 w = *p++; + w |= ( u64 )( *p++ ) << 8; + w |= ( u64 )( *p++ ) << 16; + w |= ( u64 )( *p++ ) << 24; + w |= ( u64 )( *p++ ) << 32; + w |= ( u64 )( *p++ ) << 40; + w |= ( u64 )( *p++ ) << 48; + w |= ( u64 )( *p++ ) << 56; + return w; + } +} + +static void store32_(void *dst, u32 w) { + if (native_little_endian_()) + *( u32 * )( dst ) = w; + else { + u8 *p = ( u8 * )dst; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; + } +} + +static void store64_(void *dst, u64 w) { + if (native_little_endian_()) + *( u64 * )( dst ) = w; + else { + u8 *p = ( u8 * )dst; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; w >>= 8; + *p++ = ( u8 )w; + } +} + +static u64 rotr64_(u64 w, u32 c) { + return ( w >> c ) | ( w << ( 64 - c ) ); +} + +static void secure_zero_memory_(void *v, u64 n) { + volatile u8 *p = ( volatile u8 * )v; + while( n-- ) *p++ = 0; +} + +static i32 blake2b_set_lastnode_(Blake2b_State *S) { + S->f[1] = ~0ull; + return 0; +} + +static i32 blake2b_set_lastblock_(Blake2b_State *S) { + if( S->last_node ) blake2b_set_lastnode_( S ); + + S->f[0] = ~0ull; + return 0; +} + +static i32 blake2b_increment_counter_(Blake2b_State *S, u64 inc) { + S->t[0] += inc; + S->t[1] += ( S->t[0] < inc ); + return 0; +} + +static i32 blake2b_init0_(Blake2b_State *S) { + i32 i; + mem_set_( S, 0, sizeof( Blake2b_State ) ); + + for(i = 0; i < 8; ++i ) S->h[i] = blake2b_IV[i]; + + return 0; +} + +static i32 blake2b_init_param_(Blake2b_State *S, Blake2b_Param *P) { + blake2b_init0_( S ); + u8 *p = ( u8 * )( P ); + u64 i; + + for( i = 0; i < 8; ++i ) + S->h[i] ^= load64_( p + sizeof( S->h[i] ) * i ); + + return 0; +} + +i32 blake2b_init(Blake2b_State *S, u8 outlen) { + Blake2b_Param P[1]; + + if ( ( !outlen ) || ( outlen > BLAKE2B_OUTBYTES ) ) return -1; + + P->digest_length = outlen; + P->key_length = 0; + P->fanout = 1; + P->depth = 1; + store32_( &P->leaf_length, 0 ); + store64_( &P->node_offset, 0 ); + P->node_depth = 0; + P->inner_length = 0; + mem_set_( P->reserved, 0, sizeof( P->reserved ) ); + mem_set_( P->salt, 0, sizeof( P->salt ) ); + mem_set_( P->personal, 0, sizeof( P->personal ) ); + return blake2b_init_param_( S, P ); +} + +static i32 blake2b_compress_(Blake2b_State *S, u8 block[BLAKE2B_BLOCKBYTES]) { + u64 m[16]; + u64 v[16]; + i32 i; + + for( i = 0; i < 16; ++i ) + m[i] = load64_( block + i * sizeof( m[i] ) ); + + for( i = 0; i < 8; ++i ) + v[i] = S->h[i]; + + v[ 8] = blake2b_IV[0]; + v[ 9] = blake2b_IV[1]; + v[10] = blake2b_IV[2]; + v[11] = blake2b_IV[3]; + v[12] = S->t[0] ^ blake2b_IV[4]; + v[13] = S->t[1] ^ blake2b_IV[5]; + v[14] = S->f[0] ^ blake2b_IV[6]; + v[15] = S->f[1] ^ blake2b_IV[7]; +#define G(r,i,a,b,c,d) \ + do { \ + a = a + b + m[blake2b_sigma[r][2*i+0]]; \ + d = rotr64_(d ^ a, 32); \ + c = c + d; \ + b = rotr64_(b ^ c, 24); \ + a = a + b + m[blake2b_sigma[r][2*i+1]]; \ + d = rotr64_(d ^ a, 16); \ + c = c + d; \ + b = rotr64_(b ^ c, 63); \ + } while(0) +#define ROUND(r) \ + do { \ + G(r,0,v[ 0],v[ 4],v[ 8],v[12]); \ + G(r,1,v[ 1],v[ 5],v[ 9],v[13]); \ + G(r,2,v[ 2],v[ 6],v[10],v[14]); \ + G(r,3,v[ 3],v[ 7],v[11],v[15]); \ + G(r,4,v[ 0],v[ 5],v[10],v[15]); \ + G(r,5,v[ 1],v[ 6],v[11],v[12]); \ + G(r,6,v[ 2],v[ 7],v[ 8],v[13]); \ + G(r,7,v[ 3],v[ 4],v[ 9],v[14]); \ + } while(0) + ROUND( 0 ); + ROUND( 1 ); + ROUND( 2 ); + ROUND( 3 ); + ROUND( 4 ); + ROUND( 5 ); + ROUND( 6 ); + ROUND( 7 ); + ROUND( 8 ); + ROUND( 9 ); + ROUND( 10 ); + ROUND( 11 ); + + for( i = 0; i < 8; ++i ) + S->h[i] = S->h[i] ^ v[i] ^ v[i + 8]; + +#undef G +#undef ROUND + return 0; +} + +i32 blake2b_update(Blake2b_State *S, u8 *in, u64 inlen) { + while( inlen > 0 ) { + u64 left = S->buflen; + u64 fill = 2 * BLAKE2B_BLOCKBYTES - left; + + if( inlen > fill ) { + mem_cpy_( S->buf + left, in, fill ); + S->buflen += fill; + blake2b_increment_counter_( S, BLAKE2B_BLOCKBYTES ); + blake2b_compress_( S, S->buf ); + mem_cpy_( S->buf, S->buf + BLAKE2B_BLOCKBYTES, BLAKE2B_BLOCKBYTES ); + S->buflen -= BLAKE2B_BLOCKBYTES; + in += fill; + inlen -= fill; + } else { + mem_cpy_( S->buf + left, in, inlen ); + S->buflen += inlen; + in += inlen; + inlen -= inlen; + } + } + + return 0; +} + +i32 blake2b_init_key(Blake2b_State *S, u8 outlen, void *key, u8 keylen) { + Blake2b_Param P[1]; + + if ( ( !outlen ) || ( outlen > BLAKE2B_OUTBYTES ) ) return -1; + + if ( !key || !keylen || keylen > BLAKE2B_KEYBYTES ) return -1; + + P->digest_length = outlen; + P->key_length = keylen; + P->fanout = 1; + P->depth = 1; + store32_( &P->leaf_length, 0 ); + store64_( &P->node_offset, 0 ); + P->node_depth = 0; + P->inner_length = 0; + mem_set_( P->reserved, 0, sizeof( P->reserved ) ); + mem_set_( P->salt, 0, sizeof( P->salt ) ); + mem_set_( P->personal, 0, sizeof( P->personal ) ); + + if( blake2b_init_param_( S, P ) < 0 ) return -1; + + { + u8 block[BLAKE2B_BLOCKBYTES]; + mem_set_( block, 0, BLAKE2B_BLOCKBYTES ); + mem_cpy_( block, key, keylen ); + blake2b_update( S, block, BLAKE2B_BLOCKBYTES ); + secure_zero_memory_( block, BLAKE2B_BLOCKBYTES ); + } + return 0; +} + +i32 blake2b_final(Blake2b_State *S, u8 *out, u8 outlen) { + u8 buffer[BLAKE2B_OUTBYTES]; + i32 i; + + if( S->buflen > BLAKE2B_BLOCKBYTES ) { + blake2b_increment_counter_( S, BLAKE2B_BLOCKBYTES ); + blake2b_compress_( S, S->buf ); + S->buflen -= BLAKE2B_BLOCKBYTES; + mem_cpy_( S->buf, S->buf + BLAKE2B_BLOCKBYTES, S->buflen ); + } + + blake2b_increment_counter_( S, S->buflen ); + blake2b_set_lastblock_( S ); + mem_set_( S->buf + S->buflen, 0, 2 * BLAKE2B_BLOCKBYTES - S->buflen ); + blake2b_compress_( S, S->buf ); + + for( i = 0; i < 8; ++i ) + store64_( buffer + sizeof( S->h[i] ) * i, S->h[i] ); + + mem_cpy_( out, buffer, outlen ); + return 0; +} + +i32 blake2b(u8 *out, void *in, void *key, u8 outlen, u64 inlen, u8 keylen) { + Blake2b_State S[1]; + + if ( NULL == in ) return -1; + if ( NULL == out ) return -1; + if ( NULL == key ) keylen = 0; + + if ( keylen > 0 ) { + if ( blake2b_init_key( S, outlen, key, keylen ) < 0 ) + return -1; + } else { + if ( blake2b_init( S, outlen ) < 0 ) + return -1; + } + + blake2b_update( S, ( u8 * )in, inlen ); + blake2b_final( S, out, outlen ); + return 0; +} + +// ================================================================ +// +// UTF-8 +// +// ================================================================ + +i8 utf8_size(c32 c) { + if ((c & 0x00007f) == c) return 1; + if ((c & 0x0007ff) == c) return 2; + if ((c & 0x00ffff) == c) return 3; + if ((c & 0x1fffff) == c) return 4; + return 0; +} + +UTF8_Char utf8_read(i64 len, c8 *s) { + if (len >= 1 && + (s[0] & 0x80) == 0) + return (UTF8_Char) { + .code = s[0], + .len = 1, + }; + if (len >= 2 && + (s[0] & 0xe0) == 0xc0 && + (s[1] & 0xc0) == 0x80) + return (UTF8_Char) { + .code = (s[1] & 0x3f) + | ((s[0] & 0x1f) << 6), + .len = 2, + }; + if (len >= 3 && + (s[0] & 0xf0) == 0xe0 && + (s[1] & 0xc0) == 0x80 && + (s[2] & 0xc0) == 0x80) + return (UTF8_Char) { + .code = (s[2] & 0x3f) + | ((s[1] & 0x3f) << 6) + | ((s[0] & 0x0f) << 12), + .len = 3, + }; + if (len >= 4 && + (s[0] & 0xf8) == 0xf0 && + (s[1] & 0xc0) == 0x80 && + (s[2] & 0xc0) == 0x80 && + (s[3] & 0xc0) == 0x80) + return (UTF8_Char) { + .code = (s[3] & 0x3f) + | ((s[2] & 0x3f) << 6) + | ((s[1] & 0x3f) << 12) + | ((s[0] & 0x07) << 18), + .len = 4, + }; + return (UTF8_Char) {0}; +} + +i8 utf8_write(c32 c, c8 *buffer) { + if ((c & 0x7f) == c) { + buffer[0] = (c8) c; + return 1; + } + + if ((c & 0x7ff) == c) { + buffer[0] = 0xc0 | ((c >> 6) & 0x1f); + buffer[1] = 0x80 | ( c & 0x3f); + return 2; + } + + if ((c & 0xffff) == c) { + buffer[0] = 0xc0 | ((c >> 12) & 0x0f); + buffer[1] = 0x80 | ((c >> 6) & 0x3f); + buffer[2] = 0x80 | ( c & 0x3f); + return 3; + } + + if ((c & 0x1fffff) == c) { + buffer[0] = 0xc0 | ((c >> 18) & 0x07); + buffer[1] = 0x80 | ((c >> 12) & 0x3f); + buffer[2] = 0x80 | ((c >> 6) & 0x3f); + buffer[3] = 0x80 | ( c & 0x3f); + return 4; + } + + return 0; +} + +// ================================================================ +// +// Colors +// +// ================================================================ + +static u32 rgb_u32_from_f32_(vec3_f32 c) { + i32 ir = (i32) floor(c.x * 255. + .5); + i32 ig = (i32) floor(c.y * 255. + .5); + i32 ib = (i32) floor(c.z * 255. + .5); + + u32 r = ir < 0 ? 0u : ir > 255 ? 255u : (u32) ir; + u32 g = ig < 0 ? 0u : ig > 255 ? 255u : (u32) ig; + u32 b = ib < 0 ? 0u : ib > 255 ? 255u : (u32) ib; + + return (r << 16) | (g << 8) | b; +} + +static vec3_f32 rgb_f32_from_u32_(u32 c) { + return (vec3_f32) { + .x = ((c >> 16) & 0xff) / 255.f, + .y = ((c >> 8) & 0xff) / 255.f, + .z = ( c & 0xff) / 255.f, + }; +} + +// ================================================================ +// +// Memory buffer allocator +// +// ================================================================ + +static i64 align_(i64 x, i64 alignment) { + return ((x + (alignment - 1)) / alignment) * alignment; +} + +static i64 memory_buffer_occupied_len_(i64 buffer_size) { + return (buffer_size + 64 * MEMORY_CHUNK_SIZE - 1) / (64 * MEMORY_CHUNK_SIZE); +} + +static void *memory_buffer_allocate_from_(i64 memory_buffer_size, u8 *memory_buffer, i64 size, i64 alignment, i64 previous_size, void *previous_data) { + if (memory_buffer_size <= 0) { + LOG_error("Invalid memory buffer size."); + return NULL; + } + + if (memory_buffer == NULL) { + LOG_error("Invalid memory buffer."); + return NULL; + } + + u8 *dst = NULL; + + PROFILER_begin(PROFILE_MEMORY); + + i64 occupied_len = memory_buffer_occupied_len_(memory_buffer_size); + i64 occupied_len_bytes = occupied_len * 8; + i64 occupied_len_bits = occupied_len * 64; + + u64 *occupied = (u64 *) memory_buffer; + + if (memory_buffer_size <= occupied_len_bytes) { + LOG_error("Memory buffer too small."); + goto _finish; + } + + if (alignment <= 0) { + LOG_error("Invalid alignment: %lld", alignment); + goto _finish; + } + + if (size < 0) { + LOG_error("Invalid size: %lld", size); + goto _finish; + } + + if (previous_size < 0) { + LOG_error("Invalid previous size: %lld", previous_size); + goto _finish; + } + + if (previous_size > 0 && previous_data == NULL) { + LOG_error("Invalid previous data."); + goto _finish; + } + + u8 *data = memory_buffer + occupied_len_bytes; + + i64 prev_num_chunks = 0; + i64 prev_chunk = 0; + + if (previous_size > 0) { + prev_num_chunks = (align_(previous_size, alignment) + MEMORY_CHUNK_SIZE - 1) / MEMORY_CHUNK_SIZE; + prev_chunk = ((u8 *) previous_data - data) / MEMORY_CHUNK_SIZE; + } + + i64 num_chunks = (align_(size, alignment) + MEMORY_CHUNK_SIZE - 1) / MEMORY_CHUNK_SIZE; + i64 chunk = (memory_buffer_size + MEMORY_CHUNK_SIZE - 1) / MEMORY_CHUNK_SIZE; + + if (num_chunks == prev_num_chunks) { + dst = (u8 *) previous_data; + goto _finish; + } + + if (num_chunks < prev_num_chunks) { + // Reuse previously allocated space. + chunk = prev_chunk; + dst = data + chunk * MEMORY_CHUNK_SIZE; + } else { + // Search free space + for (i64 i = 0;; ++i) { + while ((i % 64) == 0 + && i < occupied_len_bits + && (i + 64 <= prev_chunk || i >= prev_chunk + prev_num_chunks) + && occupied[i / 64] == ~0ull) + i += 64; + while ((i % 8) == 0 + && i < occupied_len_bits + && (i + 8 <= prev_chunk || i >= prev_chunk + prev_num_chunks) + && ((u8 *) occupied)[i / 8] == 255u) + i += 8; + if (i >= occupied_len_bits) break; + b8 is_occupied = 0; + for (i64 j = i; j < i + num_chunks; ++j) { + if (j >= prev_chunk && j < prev_chunk + prev_num_chunks) continue; + if ((occupied[j / 64] & (1ull << (j % 64))) == 0) continue; + is_occupied = 1; + break; + } + if (!is_occupied) { + chunk = i; + break; + } + } + + // Check if out of memory + dst = data + chunk * MEMORY_CHUNK_SIZE; + if (dst + size > memory_buffer + memory_buffer_size) { + LOG_error("Out of memory: %lld bytes", size - previous_size); + return NULL; + } + + // Claim required space + for (i64 j = chunk; j < chunk + num_chunks; ++j) + occupied[j / 64] |= 1ull << (j % 64); + } + + if (chunk != prev_chunk) { + // Copy data + i64 len = size < previous_size ? size : previous_size; + u8 *s0 = data + prev_chunk * MEMORY_CHUNK_SIZE; + u8 *s1 = s0 + len - 1; + u8 *d0 = dst; + u8 *d1 = dst + len - 1; + if (d0 > s0) + for (u8 *d = d1, *s = s1; d >= d0; --d, --s) + *d = *s; + else + for (u8 *d = d0, *s = s0; d <= d1; ++d, ++s) + *d = *s; + } + + // Free previous space + for (i64 j = prev_chunk; j < prev_chunk + prev_num_chunks; ++j) { + if (j >= chunk && j < chunk + num_chunks) continue; + occupied[j / 64] &= ~(1ull << (j % 64)); + } + + if (num_chunks == 0) + dst = NULL; + +_finish: + PROFILER_end(PROFILE_MEMORY); + return dst; +} + +static u8 _memory_buffer[STATIC_MEMORY_BUFFER_SIZE] = {0}; + +void *memory_buffer_allocate(i64 size, i64 alignment, i64 previous_size, void *previous_data) { + if (g_platform.memory_buffer == NULL || g_platform.memory_buffer_size <= 0) { + g_platform.memory_buffer_size = sizeof _memory_buffer; + g_platform.memory_buffer = _memory_buffer; + } + + return memory_buffer_allocate_from_(g_platform.memory_buffer_size, g_platform.memory_buffer, size, alignment, previous_size, previous_data); +} + +void resize_dynamic_array_exact(i64 *num, void **data, i64 element_size, i64 new_num) { + if (num == NULL || data == NULL) { + LOG_error("Sanity"); + return; + } + + if (*num == new_num) + return; + + i64 alignment = max2_i64_(8, element_size); + + u8 *new_data = (u8 *) memory_buffer_allocate( + new_num * element_size, + alignment, + *num * element_size, + *data + ); + + if (new_data == NULL && new_num > 0) + // Out of memory. + return; + + if (new_num > *num) + mem_set_(new_data + element_size * *num, 0, new_num - *num); + + *num = new_num; + *data = new_data; +} + +void resize_dynamic_array_capacity(i64 *num, i64 *capacity, void **data, i64 element_size, i64 new_num) { + if (num == NULL || capacity == NULL || data == NULL) { + LOG_error("Sanity"); + return; + } + + if (*num == new_num) + return; + + if (new_num > *capacity) + resize_dynamic_array_exact(capacity, data, element_size, new_num); + + if (new_num <= *capacity) + *num = new_num; +} + +// ================================================================ +// +// Profiling +// +// ================================================================ + +typedef struct { + c8 *name; + i64 amount; + i64 count; + i64 time_sec; + i64 time_nsec; + i64 begin_sec; + i64 begin_nsec; +} Profiler_Slot_; + +b8 _profiler_ready = 0; +i64 _profiler_num_slots = 0; +Profiler_Slot_ *_profiler_slots = NULL; + +void PROFILER_init(u32 slot, c8 *name) { + if ((i64) (u64) slot >= _profiler_num_slots) + resize_dynamic_array_exact(&_profiler_num_slots, (void **) &_profiler_slots, sizeof *_profiler_slots, (i64) (u64) slot + 1); + + if ((i64) (u64) slot >= _profiler_num_slots) { + LOG_error("Invalid slot: %d", (i32) slot); + return; + } + + mem_set_(_profiler_slots + slot, 0, sizeof *_profiler_slots); + _profiler_slots[slot].name = name; + + _profiler_ready = 1; +} + +void PROFILER_begin(u32 slot) { + if (!_profiler_ready) + return; + + if ((i64) (u64) slot >= _profiler_num_slots) { + LOG_error("Invalid slot: %d", (i32) slot); + return; + } + + if (_profiler_slots[slot].count < 0) { + LOG_error("Sanity"); + return; + } + + ++_profiler_slots[slot].count; + if (_profiler_slots[slot].count != 1) + return; + + ++_profiler_slots[slot].amount; + + current_utc_time_in_sec_and_nsec( + &_profiler_slots[slot].begin_sec, + &_profiler_slots[slot].begin_nsec + ); +} + +static void normalize_time_(i64 *sec, i64 *nsec) { + if (sec == NULL || nsec == NULL) { + LOG_error("Sanity"); + return; + } + + if (*nsec < 0) { + *sec -= 1; + *nsec += 1000000000; + } + + if (*nsec >= 1000000000) { + *sec += *nsec / 1000000000; + *nsec = *nsec % 1000000000; + } +} + +void PROFILER_end(u32 slot) { + if (!_profiler_ready) + return; + if ((i64) (u64) slot >= _profiler_num_slots) + return; + + if (_profiler_slots[slot].count <= 0) { + LOG_error("Sanity"); + return; + } + + --_profiler_slots[slot].count; + if (_profiler_slots[slot].count != 0) + return; + + i64 sec, nsec; + current_utc_time_in_sec_and_nsec(&sec, &nsec); + + sec -= _profiler_slots[slot].begin_sec; + nsec -= _profiler_slots[slot].begin_nsec; + + normalize_time_(&sec, &nsec); + + for (i64 i = 0; i < _profiler_num_slots; ++i) { + if (_profiler_slots[i].count == 0) + continue; + + _profiler_slots[i].time_sec -= sec; + _profiler_slots[i].time_nsec -= nsec; + + normalize_time_(&_profiler_slots[i].time_sec, &_profiler_slots[i].time_nsec); + } + + _profiler_slots[slot].begin_sec = 0; + _profiler_slots[slot].begin_nsec = 0; + + _profiler_slots[slot].time_sec += sec; + _profiler_slots[slot].time_nsec += nsec; + + normalize_time_(&_profiler_slots[slot].time_sec, &_profiler_slots[slot].time_nsec); +} + +void PROFILER_report_(void) { + if (_profiler_num_slots > 0) { + LOG_print("PROFILER REPORT"); + for (i64 i = 0; i < _profiler_num_slots; ++i) { + if (_profiler_slots[i].name == NULL || _profiler_slots[i].amount == 0) + continue; + f64 k = _profiler_slots[i].amount == 0 ? 0. : 1. / _profiler_slots[i].amount; + f64 f = ((f64) _profiler_slots[i].time_sec) * k + ((f64 )_profiler_slots[i].time_nsec * .000000001) * k; + LOG_print("%-31s %3lld.%09lld / %4lld = %.6f", _profiler_slots[i].name, _profiler_slots[i].time_sec, _profiler_slots[i].time_nsec, _profiler_slots[i].amount, f); + } + } +} + +// ================================================================ +// +// Platform +// +// ================================================================ + +void run_main_window_event_loop(void) { + init_main_window(); +#if !defined(__wasm__) + while (!g_platform.done) { + PROFILER_begin(PROFILE_FRAME); + update_and_render_frame(); + PROFILER_end(PROFILE_FRAME); + } + shutdown_all_systems(); +#endif +} + +// ================================================================ +// +// Frame +// +// ================================================================ + +static i64 _frame_index = 0; +static i64 _frame_duration[NUM_FRAMES_AVERAGED] = {0}; + +static i64 average_frame_duration_(i64 duration) { + _frame_duration[_frame_index] = duration; + _frame_index = (_frame_index + 1) % NUM_FRAMES_AVERAGED; + + i64 durs[NUM_FRAMES_AVERAGED + 1]; + for (i64 i = 0; i < NUM_FRAMES_AVERAGED; ++i) + durs[i] = _frame_duration[i]; + + // FIXME, PERF: Use better sorting algorithm, e.g. merge sort. + for (i64 i = 0; i < (i64) NUM_FRAMES_AVERAGED; ++i) + for (i64 j = i + 1; j < NUM_FRAMES_AVERAGED; ++j) + if (durs[i] < durs[j]) { + i64 t = durs[i]; + durs[i] = durs[j]; + durs[j] = t; + } + + if (AVERAGE_FRAME_BIAS < 0.0 || AVERAGE_FRAME_BIAS > 1.0) + return durs[0]; + + return durs[(i64) ((NUM_FRAMES_AVERAGED - 1) * AVERAGE_FRAME_BIAS)]; +} + +// ================================================================ +// +// Graphics +// +// ================================================================ + +static i32 _internal_width = 0; +static i32 _internal_height = 0; +static i64 _internal_pixels_len = 0; +static u32 *_internal_pixels = NULL; +static i64 _internal_row_len = 0; +static vec3_f32 *_internal_row = NULL; + +static b8 pixel_size_update_(i64 real_width, i64 real_height) { + b8 size_changed = 0; + + if (g_platform.antialiasing_scale <= 0) g_platform.antialiasing_scale = DEFAULT_ANTIALIASING_SCALE; + if (g_platform.pixel_size <= 0.0) g_platform.pixel_size = DEFAULT_PIXEL_SIZE; + if (g_platform.pixel_size < MIN_PIXEL_SIZE) g_platform.pixel_size = MIN_PIXEL_SIZE; + if (g_platform.pixel_size > MAX_PIXEL_SIZE) g_platform.pixel_size = MAX_PIXEL_SIZE; + + i64 width = (i64) floor(((f64) real_width) / g_platform.pixel_size + .5) * g_platform.antialiasing_scale; + i64 height = (i64) floor(((f64) real_height) / g_platform.pixel_size + .5) * g_platform.antialiasing_scale; + + if (g_platform.real_width != real_width || g_platform.real_height != real_height) { + size_changed = 1; + g_platform.real_width = real_width; + g_platform.real_height = real_height; + } + + resize_dynamic_array_exact(&g_platform.num_pixels, (void **) &g_platform.pixels, sizeof *g_platform.pixels, width * height); + if (g_platform.num_pixels < width * height) + LOG_error("Failed to allocate %lld x %lld pixel buffer.", width, height); + height = width <= 0 ? 0 : g_platform.num_pixels / width; + + if (g_platform.frame_width != width || g_platform.frame_height != height) { + size_changed = 1; + g_platform.frame_width = width; + g_platform.frame_height = height; + } + + i64 internal_width = max2_i64_(real_width, width / g_platform.antialiasing_scale); + i64 internal_height = max2_i64_(real_height, height / g_platform.antialiasing_scale); + + resize_dynamic_array_exact(&_internal_pixels_len, (void **) &_internal_pixels, sizeof *_internal_pixels, internal_width * internal_height); + if (_internal_pixels_len < internal_width * internal_height) + LOG_error("Failed to allocate %lld x %lld internal pixel buffer.", internal_width, internal_height); + + resize_dynamic_array_exact(&_internal_row_len, (void **) &_internal_row, sizeof *_internal_row, internal_width); + if (_internal_row_len < internal_width) + LOG_error("Failed to allocate %lld internal immediate pixel buffer.", internal_width); + + _internal_width = real_width; + _internal_height = real_width <= 0 ? 0 : min2_i32_(_internal_pixels_len / real_width, real_height); + + return size_changed; +} + +static void convert_pixels_for_window_(void) { + u32 aa_scale = g_platform.antialiasing_scale; + + if (aa_scale == 0 || aa_scale > 1024) { + LOG_error("Sanity"); + return; + } + + // Downscale pixels from vec4_f32 pixel buffer to internal u32 pixel buffer. + // + // FIXME: Check if the scale is 1:1. + { + u32 dst_width = g_platform.frame_width / aa_scale; + u32 dst_height = g_platform.frame_height / aa_scale; + u32 src_width = g_platform.frame_width; + + if (g_platform.num_pixels < src_width * g_platform.frame_height) { + LOG_error("Sanity"); + return; + } + + if (_internal_pixels_len < dst_width * dst_height) { + LOG_error("Sanity"); + return; + } + + if (_internal_row_len < dst_width) { + LOG_error("Sanity"); + return; + } + + PROFILER_begin(PROFILE_DOWNSCALE); + + f32 k = 1.f / (aa_scale * aa_scale); + vec4_f32 *s = g_platform.pixels; + u32 *d = _internal_pixels; + + for (u32 j = 0; j < dst_height; ++j) { + mem_set_(_internal_row, 0, dst_width * sizeof *_internal_row); + + for (u32 jj = 0; jj < aa_scale; ++jj) { + vec3_f32 *im = _internal_row; + vec3_f32 *im_end = im + dst_width; + + for (; im < im_end; ++im) { + vec4_f32 *s_end = s + aa_scale; + + for (; s < s_end; ++s) { + im->x += s->x; + im->y += s->y; + im->z += s->z; + } + } + } + + u32 *d_end = d + dst_width; + vec3_f32 *im = _internal_row; + + for (; d < d_end; ++d, ++im) + *d = rgb_u32_from_f32_((vec3_f32) { .x = im->x * k, .y = im->y * k, .z = im->z * k, }) | 0xff000000; + } + + PROFILER_end(PROFILE_DOWNSCALE); + } + + // Resize internal pixel buffer in-place. + { + i64 src_width = g_platform.frame_width / aa_scale; + i64 src_height = g_platform.frame_height / aa_scale; + i64 src_len = src_width * src_height; + i64 dst_width = _internal_width; + i64 dst_height = _internal_height; + i64 dst_len = dst_width * dst_height; + + if (_internal_pixels_len < src_len) { + LOG_error("Sanity"); + return; + } + + if (_internal_pixels_len < dst_len) { + LOG_error("Sanity"); + return; + } + + PROFILER_begin(PROFILE_RESIZE); + + // TODO, PERF: Improve performance. + + u32 x_ratio = (src_width << 16) / dst_width; + u32 y_ratio = (src_height << 16) / dst_height; + + if (src_len < dst_len) + for (u32 j = dst_height - 1;; --j) { + u32 jj_w = ((j * y_ratio) >> 16) * src_width; + u32 j_w = j * dst_width; + for (u32 i = dst_width - 1;; --i) { + u32 ii = (i * x_ratio) >> 16; + _internal_pixels[j_w + i] = _internal_pixels[jj_w + ii]; + if (i == 0) break; + } + if (j == 0) break; + } + else + for (u32 j = 0; j < dst_height; ++j) { + u32 jj_w = ((j * y_ratio) >> 16) * src_width; + u32 j_w = j * dst_width; + for (u32 i = 0; i < dst_width; ++i) { + u32 ii = (i * x_ratio) >> 16; + _internal_pixels[j_w + i] = _internal_pixels[jj_w + ii]; + } + } + + PROFILER_end(PROFILE_RESIZE); + } +} + +static void pixel_size_calibrate_(i64 current_frame_duration) { + if (g_platform.exact_resolution) + return; + + i64 frame_duration = average_frame_duration_(current_frame_duration); + + if (current_frame_duration > FRAME_DURATION_HARD_LIMIT) + g_platform.pixel_size = MAX_PIXEL_SIZE; + else if (g_platform.pixel_size < MAX_PIXEL_SIZE && frame_duration > MAX_FRAME_DURATION) + g_platform.pixel_size += PIXEL_SIZE_DELTA * (frame_duration - MAX_FRAME_DURATION); + else if (g_platform.pixel_size > MIN_PIXEL_SIZE && frame_duration < MIN_FRAME_DURATION) + g_platform.pixel_size -= PIXEL_SIZE_DELTA * (MIN_FRAME_DURATION - frame_duration); +} + +static void cleanup_pixel_buffers_(void) { + resize_dynamic_array_exact(&_internal_pixels_len, (void **) &_internal_pixels, sizeof *_internal_pixels, 0); + resize_dynamic_array_exact(&_internal_row_len, (void **) &_internal_row, sizeof *_internal_row, 0); + resize_dynamic_array_exact(&g_platform.num_pixels, (void **) &g_platform.pixels, sizeof *g_platform.pixels, 0); +} + +// ================================================================ +// +// Sound +// +// ================================================================ + +static i64 _sound_clock_time = 0; +static i64 _sound_clock_carry = 0; +static i64 _sound_position = 0; + +static f32 _sound_ring[MAX_NUM_PRIMARY_SOUND_FRAMES] = {0}; + +static i64 sound_samples_elapsed_(void) { + if (_sound_clock_time == 0) { + _sound_clock_time = current_utc_time_in_milliseconds(); + _sound_clock_carry = 0; + return 0; + } + + i64 time_elapsed = current_utc_time_in_milliseconds() - _sound_clock_time; + i64 delta = time_elapsed * PRIMARY_SOUND_SAMPLE_RATE + _sound_clock_carry; + i64 num_samples = delta / 1000; + + _sound_clock_time += time_elapsed; + _sound_clock_carry = delta % 1000; + + return num_samples; +} + +// ================================================================ +// +// Drop files +// +// ================================================================ + +static void drop_files_clean_(void) { + g_platform.files_dragged = 0; + g_platform.files_dropped = 0; + + for (i64 i = 0; i < g_platform.num_drop_files; ++i) { + resize_dynamic_array_exact(&g_platform.drop_files[i].name_len, (void **) &g_platform.drop_files[i].name, 1, 0); + resize_dynamic_array_exact(&g_platform.drop_files[i].data_size, (void **) &g_platform.drop_files[i].data, 1, 0); + } + + resize_dynamic_array_exact(&g_platform.num_drop_files, (void **) &g_platform.drop_files, sizeof *g_platform.drop_files, 0); +} + +static void drop_files_set_num_(i64 num) { + if (num <= g_platform.num_drop_files) { + LOG_error("Sanity"); + return; + } + + resize_dynamic_array_exact(&g_platform.num_drop_files, (void **) &g_platform.drop_files, sizeof *g_platform.drop_files, num); +} + +static void drop_files_set_name_(i64 index, i64 name_len, c8 *name) { + if (g_platform.drop_files == NULL) { + LOG_error("Sanity"); + return; + } + + if (index < 0 || index >= g_platform.num_drop_files) { + LOG_error("Sanity"); + return; + } + + Drop_File *f = g_platform.drop_files + index; + + resize_dynamic_array_exact(&f->name_len, (void **) &f->name, 1, name_len); + if (name != NULL) + mem_cpy_(f->name, name, f->name_len); +} + +static void drop_files_set_data_(i64 index, i64 data_size) { + if (g_platform.drop_files == NULL) { + LOG_error("Sanity"); + return; + } + + if (index < 0 || index >= g_platform.num_drop_files) { + LOG_error("Sanity"); + return; + } + + Drop_File *f = g_platform.drop_files + index; + + resize_dynamic_array_exact(&f->data_size, (void **) &f->data, 1, data_size); +} + +// ================================================================ +// +// Clipboard +// +// ================================================================ + +static void store_clipboard_text_(i64 size, c8 *data) { + if (size < 0 || data == NULL) + return; + + resize_dynamic_array_exact(&g_platform.clipboard_text_len, (void **) &g_platform.clipboard_text, 1, size + 1); + + i64 len = g_platform.clipboard_text_len - 1; + mem_cpy_(g_platform.clipboard_text, data, len); + if (len >= 0) + g_platform.clipboard_text[len] = '\0'; +} + +// ================================================================ +// +// Dynamic libraries +// +// ================================================================ + +#if defined(__unix__) +#include + +typedef struct { + void *handle; +} Dynamic_Library_Slot_; + +typedef struct { + i64 num_slots; + Dynamic_Library_Slot_ *slots; +} Dynamic_Libraries_; + +static Dynamic_Libraries_ _dynamic_libraries = {0}; + +void dynamic_library_open(u16 slot, c8 *name) { + if (name == NULL) { + LOG_error("Sanity"); + return; + } + + if ((i64) (u64) slot >= _dynamic_libraries.num_slots) + resize_dynamic_array_exact(&_dynamic_libraries.num_slots, (void **) &_dynamic_libraries.slots, sizeof *_dynamic_libraries.slots, (i64) slot + 1); + + if ((i64) (u64) slot >= _dynamic_libraries.num_slots) { + LOG_error("Invalid slot: %d", (i32) slot); + return; + } + + if (_dynamic_libraries.slots[slot].handle != NULL) { + dlclose(_dynamic_libraries.slots[slot].handle); + _dynamic_libraries.slots[slot].handle = NULL; + } + + void *h = dlopen(name, RTLD_LAZY); + + if (h == NULL) { + LOG_error("Failed to open: %s", name); + LOG_error("%s", dlerror()); + return; + } + + _dynamic_libraries.slots[slot].handle = h; +} + +void *dynamic_library_get_proc_address(u16 slot, c8 *proc) { + if ((i64) (u64) slot >= _dynamic_libraries.num_slots) { + LOG_error("Invalid slot: %d", (i32) slot); + return NULL; + } + + if (proc == NULL) { + LOG_error("Sanity"); + return NULL; + } + + if (_dynamic_libraries.slots[slot].handle == NULL) { + LOG_error("Slot is closed: %d", (i32) slot); + return NULL; + } + + void *proc_address = dlsym(_dynamic_libraries.slots[slot].handle, proc); + + if (proc_address == NULL) { + LOG_error("Failed to get: %s", proc); + LOG_error("%s", dlerror()); + } + + return proc_address; +} + +static void close_all_dynamic_libraries_(void) { + for (i64 i = 0; i < _dynamic_libraries.num_slots; ++i) + if (_dynamic_libraries.slots[i].handle != NULL) + dlclose(_dynamic_libraries.slots[i].handle); + resize_dynamic_array_exact(&_dynamic_libraries.num_slots, (void **) &_dynamic_libraries.slots, sizeof *_dynamic_libraries.slots, 0); +} +#endif // defined(__unix__) + +#if defined(__wasm__) +void dynamic_library_open(u16 slot, c8 *name) { + (void) slot; + (void) name; + + LOG_error("Dynamic library not found: %s", name); +} + +void *dynamic_library_get_proc_address(u16 slot, c8 *proc) { + (void) slot; + (void) proc; + + LOG_error("Proc address not found: %s", proc); + return NULL; +} +#endif // defined(__wasm__) + +// ================================================================ +// +// PLATFORM-SPECIFIC CODE +// +// ================================================================ +// +// Time +// +// ================================================================ + +#if defined(__unix__) + +#include +#include +#include + +void current_utc_time_in_sec_and_nsec(i64 *seconds, i64 *nanoseconds) { + struct timespec t; + timespec_get(&t, TIME_UTC); + if (seconds != NULL) *seconds = t.tv_sec; + if (nanoseconds != NULL) *nanoseconds = t.tv_nsec; +} + +i64 current_utc_time_in_milliseconds(void) { + struct timespec t; + timespec_get(&t, TIME_UTC); + return 1000ll * t.tv_sec + t.tv_nsec / 1000000ll; +} + +void yield_thread_execution(void) { + sched_yield(); +} + +void suspend_thread_for_milliseconds(i64 duration) { + if (duration == 0) + usleep(0); + if (duration <= 0) + return; + + if (duration >= 1000) + // seconds + sleep(duration / 1000); + + if ((duration % 1000) > 0) + // microseconds + usleep((duration % 1000) * 1000); +} + +#endif // defined(__unix__) + +// ================================================================ +// +// Networking +// +// ================================================================ + +#if defined(__unix__) + +#include +#include +#include +#include +#include + +typedef struct { + b8 ready; + i32 socket; + u16 local_port; + Network_Address address; +} Socket_Slot; + +static b8 _network_ready = 0; +static i64 _num_sockets = 0; +static Socket_Slot *_sockets = NULL; + +static void network_init_(u16 slot) { + if (!_network_ready) { + signal(SIGPIPE, SIG_IGN); + _network_ready = 1; + } + + i64 num = (i64) (u64) slot; + i64 prev_num = _num_sockets; + + if (num > prev_num) + resize_dynamic_array_exact(&_num_sockets, (void **) &_sockets, sizeof *_sockets, num); +} + +static void network_cleanup_(void) { + for (i64 i = 0; i < _num_sockets; ++i) + if (_sockets[i].ready) { + close(_sockets[i].socket); + _sockets[i].ready = 0; + } + + resize_dynamic_array_exact(&_num_sockets, (void **) &_sockets, sizeof *_sockets, 0); +} + +b8 network_open(u16 slot, Network_Address address, u16 *local_port) { + network_init_(slot); + + if ((i64) (u64) slot >= _num_sockets) { + LOG_error("Invalid slot: %d", (i32) slot); + return 0; + } + + b8 change_address = + !_sockets[slot].ready + || _sockets[slot].address.protocol != address.protocol + || (address.port != 0 && _sockets[slot].local_port != address.port) + || (memcmp(_sockets[slot].address.v6_address, &(u8[sizeof address.v6_address]) {0}, sizeof address.v6_address) != 0 && + memcmp(_sockets[slot].address.v6_address, address.v6_address, sizeof address.v6_address) != 0); + + if (change_address && _sockets[slot].ready) { + close(_sockets[slot].socket); + _sockets[slot].ready = 0; + } + + struct sockaddr *p; + i32 p_len; + + struct sockaddr_in a4 = {0}; + struct sockaddr_in6 a6 = {0}; + + if (address.protocol == IPv4_UDP) { + p = (struct sockaddr *) &a4; + p_len = sizeof a4; + } else { + p = (struct sockaddr *) &a6; + p_len = sizeof a6; + } + + if (!_sockets[slot].ready) { + _sockets[slot].socket = socket(address.protocol == IPv4_UDP ? AF_INET : AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + + if (_sockets[slot].socket == -1) { + LOG_error("socket failed (errno %d).", errno); + return 0; + } + + if (address.protocol == IPv4_UDP) { + a4.sin_family = AF_INET; + a4.sin_port = htons(address.port); + a4.sin_addr.s_addr = address.v4_address_as_u32; + } else { + a6.sin6_family = AF_INET6; + a6.sin6_port = htons(address.port); + mem_cpy_(a6.sin6_addr.s6_addr, address.v6_address, sizeof a6.sin6_addr.s6_addr); + } + + if (bind(_sockets[slot].socket, p, p_len) == -1) { + close(_sockets[slot].socket); + + LOG_error("bind failed (errno %d).", errno); + return 0; + } + + if (getsockname(_sockets[slot].socket, p, &(socklen_t) {p_len}) == -1) { + close(_sockets[slot].socket); + + LOG_error("getsockname failed (errno %d).", errno); + return 0; + } + + if (p->sa_family == AF_INET) + _sockets[slot].local_port = ntohs(a4.sin_port); + else + _sockets[slot].local_port = ntohs(a6.sin6_port); + + _sockets[slot].ready = 1; + _sockets[slot].address = address; + } + + if (local_port != NULL) + *local_port = _sockets[slot].local_port; + + return 1; +} + +i64 network_recv(u16 slot, Network_Address address, i64 size, u8 *data, u16 *local_port, Network_Address *remote_address) { + if (address.protocol != IPv4_UDP && address.protocol != IPv6_UDP) { + LOG_error("Invalid address protocol: %d", (i32) (u32) address.protocol); + return -1; + } + + if (!network_open(slot, address, local_port)) + return -1; + if (size <= 0) + return 0; + + struct sockaddr *p; + i32 p_len; + + struct sockaddr_in a4 = {0}; + struct sockaddr_in6 a6 = {0}; + + if (address.protocol == IPv4_UDP) { + p = (struct sockaddr *) &a4; + p_len = sizeof a4; + } else { + p = (struct sockaddr *) &a6; + p_len = sizeof a6; + } + + i64 received = recvfrom( + _sockets[slot].socket, + data, + size, + MSG_DONTWAIT, + p, + &(socklen_t) {p_len} + ); + + if (received < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return 0; + + LOG_error("recvfrom failed (errno %d).", errno); + return -1; + } + + if (remote_address != NULL) { + mem_set_(remote_address, 0, sizeof *remote_address); + remote_address->protocol = address.protocol; + + if (address.protocol == IPv4_UDP) { + remote_address->port = ntohs(a4.sin_port); + remote_address->v4_address_as_u32 = a4.sin_addr.s_addr; + } else { + remote_address->port = ntohs(a6.sin6_port); + mem_cpy_(remote_address->v6_address, a6.sin6_addr.s6_addr, sizeof remote_address->v6_address); + } + } + + return received; +} + +i64 network_send(u16 slot, Network_Address address, i64 size, u8 *data, u16 *local_port) { + if (address.protocol != IPv4_UDP && address.protocol != IPv6_UDP) { + LOG_error("Invalid address protocol: %d", (i32) (u32) address.protocol); + return -1; + } + + Network_Address local_address = address; + local_address.port = 0; + + if (!network_open(slot, local_address, local_port)) + return -1; + if (size <= 0) + return 0; + + struct sockaddr *p; + i32 p_len; + + struct sockaddr_in a4 = {0}; + struct sockaddr_in6 a6 = {0}; + + if (address.protocol == IPv4_UDP) { + p = (struct sockaddr *) &a4; + p_len = sizeof a4; + + a4.sin_family = AF_INET; + a4.sin_port = htons(address.port); + a4.sin_addr.s_addr = address.v4_address_as_u32; + } else { + p = (struct sockaddr *) &a6; + p_len = sizeof a6; + + a6.sin6_family = AF_INET6; + a6.sin6_port = htons(address.port); + mem_cpy_(a6.sin6_addr.s6_addr, address.v6_address, sizeof a6.sin6_addr.s6_addr); + } + + i64 sent = sendto( + _sockets[0].socket, + data, + size, + MSG_DONTWAIT, + p, + p_len + ); + + if (sent < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return 0; + + LOG_error("sendto failed (errno %d).", errno); + return -1; + } + + return sent; +} + +#endif // defined(__unix__) + +// ================================================================ +// +// ALSA +// +// ================================================================ + +#if defined(__linux__) && ENABLE_ALSA + +#include + +static b8 _sound_ready = 0; +static snd_pcm_t *_sound_out = NULL; + +static void sound_init_(void) { + if (_sound_ready) + return; + + i32 s; + + s = snd_pcm_open(&_sound_out, "default", SND_PCM_STREAM_PLAYBACK, 0); + if (s < 0) { + LOG_error("snd_pcm_open failed: %s", snd_strerror(s)); + return; + } + + snd_pcm_hw_params_t *hw_params; + snd_pcm_sw_params_t *sw_params; + + snd_pcm_hw_params_alloca(&hw_params); + + s = snd_pcm_hw_params_any(_sound_out, hw_params); + if (s < 0) + LOG_error("snd_pcm_hw_params_any failed: %s", snd_strerror(s)); + + s = snd_pcm_hw_params_set_access(_sound_out, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (s < 0) + LOG_error("snd_pcm_hw_params_set_access failed: %s", snd_strerror(s)); + + s = snd_pcm_hw_params_set_format(_sound_out, hw_params, SND_PCM_FORMAT_FLOAT_LE); + if (s < 0) + LOG_error("snd_pcm_hw_params_set_format failed: %s", snd_strerror(s)); + + s = snd_pcm_hw_params_set_rate(_sound_out, hw_params, PRIMARY_SOUND_SAMPLE_RATE, 0); + if (s < 0) + LOG_error("snd_pcm_hw_params_set_rate failed: %s", snd_strerror(s)); + + s = snd_pcm_hw_params_set_channels(_sound_out, hw_params, NUM_PRIMARY_SOUND_CHANNELS); + if (s < 0) + LOG_error("snd_pcm_hw_params_set_channels failed: %s", snd_strerror(s)); + + s = snd_pcm_hw_params(_sound_out, hw_params); + if (s < 0) + LOG_error("snd_pcm_hw_params failed: %s", snd_strerror(s)); + + snd_pcm_sw_params_alloca(&sw_params); + + s = snd_pcm_sw_params_current(_sound_out, sw_params); + if (s < 0) + LOG_error("snd_pcm_sw_params_current failed: %s", snd_strerror(s)); + + s = snd_pcm_sw_params_set_avail_min(_sound_out, sw_params, PRIMARY_SOUND_AVAIL_MIN); + if (s < 0) + LOG_error("snd_pcm_sw_params_set_avail_min failed: %s", snd_strerror(s)); + + s = snd_pcm_sw_params(_sound_out, sw_params); + if (s < 0) + LOG_error("snd_pcm_sw_params failed: %s", snd_strerror(s)); + + s = snd_pcm_prepare(_sound_out); + if (s < 0) + LOG_error("snd_pcm_prepare failed: %s", snd_strerror(s)); + + _sound_ready = 1; +} + +static void sound_cleanup_(void) { + if (!_sound_ready) + return; + + i32 s; + + s = snd_pcm_nonblock(_sound_out, 0); + if (s < 0) + LOG_error("snd_pcm_nonblock failed: %s", snd_strerror(s)); + + s = snd_pcm_drain(_sound_out); + if (s < 0) + LOG_error("snd_pcm_drain failed: %s", snd_strerror(s)); + + // FIXME: Memory leaks, seems to be an ALSA bug. + // snd_pcm_close(_sound_out); + // snd_config_update_free_global(); + + _sound_ready = 0; +} + +void handle_primary_sound(void) { + sound_init_(); + + g_platform.num_sound_samples_elapsed = sound_samples_elapsed_(); + i64 num_frames = g_platform.num_sound_samples_elapsed * NUM_PRIMARY_SOUND_CHANNELS; + + if (num_frames > MAX_NUM_PRIMARY_SOUND_FRAMES) { + LOG_error("Sound buffer overflow."); + num_frames %= MAX_NUM_PRIMARY_SOUND_FRAMES; + } + + i32 s; + + if (num_frames <= MAX_NUM_PRIMARY_SOUND_FRAMES - _sound_position) { + s = snd_pcm_writei(_sound_out, _sound_ring + _sound_position, num_frames / NUM_PRIMARY_SOUND_CHANNELS); + if (s < 0) + LOG_error("snd_pcm_writei failed: %s", snd_strerror(s)); + + mem_set_(_sound_ring + _sound_position, 0, num_frames * sizeof *_sound_ring); + } else { + i64 part_one = MAX_NUM_PRIMARY_SOUND_FRAMES - _sound_position; + i64 part_two = num_frames - part_one; + + s = snd_pcm_writei(_sound_out, _sound_ring + _sound_position, part_one / NUM_PRIMARY_SOUND_CHANNELS); + if (s < 0) + LOG_error("snd_pcm_writei failed: %s", snd_strerror(s)); + + s = snd_pcm_writei(_sound_out, _sound_ring, part_two / NUM_PRIMARY_SOUND_CHANNELS); + if (s < 0) + LOG_error("snd_pcm_writei failed: %s", snd_strerror(s)); + + mem_set_(_sound_ring + _sound_position, 0, part_one * sizeof *_sound_ring); + mem_set_(_sound_ring, 0, part_two * sizeof *_sound_ring); + } + + _sound_position = (_sound_position + num_frames) % MAX_NUM_PRIMARY_SOUND_FRAMES; +} + +void queue_primary_sound(i64 delay_in_samples, i64 num_samples, f32 *frames) { + if (num_samples < 0) + LOG_error("Invalid num samples %lld.", num_samples); + if (frames == NULL) + return; + + if (delay_in_samples < 0) { + frames += -delay_in_samples * NUM_PRIMARY_SOUND_CHANNELS; + num_samples -= delay_in_samples; + delay_in_samples = 0; + } + + if (num_samples <= 0) + return; + + i64 num_frames = num_samples * NUM_PRIMARY_SOUND_CHANNELS; + + if (num_frames > MAX_NUM_PRIMARY_SOUND_FRAMES) { + LOG_error("Sound buffer overflow."); + return; + } + + sound_init_(); + + i64 begin = (_sound_position + delay_in_samples) % MAX_NUM_PRIMARY_SOUND_FRAMES; + + if (num_frames <= MAX_NUM_PRIMARY_SOUND_FRAMES - begin) + for (i64 i = 0; i < num_frames; ++i) + _sound_ring[begin + i] += frames[i]; + else { + i64 part_one = MAX_NUM_PRIMARY_SOUND_FRAMES - begin; + i64 part_two = num_frames - part_one; + + for (i64 i = 0; i < part_one; ++i) + _sound_ring[begin + i] += frames[i]; + for (i64 i = 0; i < part_two; ++i) + _sound_ring[i] += frames[part_one + i]; + } +} + +#endif // defined(__linux__) && ENABLE_ALSA + +// ================================================================ +// +// Wayland +// +// FIXME: Remove dynamic memory management. +// Use arena allocator for a static buffer. +// +// ================================================================ + +#if defined(__linux__) && ENABLE_WAYLAND + +#include +#include +#include + +struct zxdg_output_v1_listener { + void (*logical_position) (void *data, void *zxdg_output_v1, i32 x, i32 y); + void (*logical_size) (void *data, void *zxdg_output_v1, i32 width, i32 height); + void (*done) (void *data, void *zxdg_output_v1); + void (*name) (void *data, void *zxdg_output_v1, c8 *name); + void (*description) (void *data, void *zxdg_output_v1, c8 *description); +}; + +struct zwlr_screencopy_frame_v1_listener { + void (*buffer) (void *data, void *zwlr_screencopy_frame_v1, u32 format, u32 width, u32 height, u32 stride); + void (*flags) (void *data, void *zwlr_screencopy_frame_v1, u32 flags); + void (*ready) (void *data, void *zwlr_screencopy_frame_v1, u32 tv_sec_hi, u32 tv_sec_lo, u32 tv_nsec); + void (*failed) (void *data, void *zwlr_screencopy_frame_v1); +}; + +extern struct wl_interface zwlr_screencopy_frame_v1_interface; +extern struct wl_interface zxdg_output_v1_interface; + +static struct wl_interface const *wlr_screencopy_unstable_v1_types[] = { + NULL, + NULL, + NULL, + NULL, + &zwlr_screencopy_frame_v1_interface, + NULL, + &wl_output_interface, + &zwlr_screencopy_frame_v1_interface, + NULL, + &wl_output_interface, + NULL, + NULL, + NULL, + NULL, + &wl_buffer_interface, +}; + +static struct wl_message zwlr_screencopy_frame_v1_requests[] = { + { "copy", "o", wlr_screencopy_unstable_v1_types + 14 }, + { "destroy", "", wlr_screencopy_unstable_v1_types }, +}; + +static struct wl_message zwlr_screencopy_frame_v1_events[] = { + { "buffer", "uuuu", wlr_screencopy_unstable_v1_types }, + { "flags", "u", wlr_screencopy_unstable_v1_types }, + { "ready", "uuu", wlr_screencopy_unstable_v1_types }, + { "failed", "", wlr_screencopy_unstable_v1_types }, +}; + +struct wl_interface zwlr_screencopy_frame_v1_interface = { + "zwlr_screencopy_frame_v1", 1, + 2, zwlr_screencopy_frame_v1_requests, + 4, zwlr_screencopy_frame_v1_events, +}; + +static struct wl_message zwlr_screencopy_manager_v1_requests[] = { + { "capture_output", "nio", wlr_screencopy_unstable_v1_types + 4 }, + { "capture_output_region", "nioiiii", wlr_screencopy_unstable_v1_types + 7 }, + { "destroy", "", wlr_screencopy_unstable_v1_types }, +}; + +static struct wl_interface const *xdg_output_unstable_v1_types[] = { + NULL, + NULL, + &zxdg_output_v1_interface, + &wl_output_interface, +}; + +static struct wl_message zxdg_output_manager_v1_requests[] = { + { "destroy", "", xdg_output_unstable_v1_types }, + { "get_xdg_output", "no", xdg_output_unstable_v1_types + 2 }, +}; + +static struct wl_interface zxdg_output_manager_v1_interface = { + "zxdg_output_manager_v1", 3, + 2, zxdg_output_manager_v1_requests, + 0, NULL, +}; + +static struct wl_message zxdg_output_v1_requests[] = { + { "destroy", "", xdg_output_unstable_v1_types }, +}; + +static struct wl_message zxdg_output_v1_events[] = { + { "logical_position", "ii", xdg_output_unstable_v1_types }, + { "logical_size", "ii", xdg_output_unstable_v1_types }, + { "done", "", xdg_output_unstable_v1_types }, + { "name", "2s", xdg_output_unstable_v1_types }, + { "description", "2s", xdg_output_unstable_v1_types }, +}; + +struct wl_interface zwlr_screencopy_manager_v1_interface = { + "zwlr_screencopy_manager_v1", 1, + 3, zwlr_screencopy_manager_v1_requests, + 0, NULL, +}; + +struct wl_interface zxdg_output_v1_interface = { + "zxdg_output_v1", 3, + 1, zxdg_output_v1_requests, + 5, zxdg_output_v1_events, +}; + +static void *zwlr_screencopy_manager_v1_capture_output(void *zwlr_screencopy_manager_v1, i32 overlay_cursor, struct wl_output *output) { + struct wl_proxy *frame; + frame = wl_proxy_marshal_flags((struct wl_proxy *) zwlr_screencopy_manager_v1, 0, &zwlr_screencopy_frame_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwlr_screencopy_manager_v1), 0, NULL, overlay_cursor, output); + return (void *) frame; +} + +static void zxdg_output_v1_destroy(void *zxdg_output_v1) { + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_v1, 0, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_output_v1), WL_MARSHAL_FLAG_DESTROY); +} + +static void zxdg_output_manager_v1_destroy(void *zxdg_output_manager_v1) { + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_manager_v1, 0, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +static void zwlr_screencopy_frame_v1_destroy(void *zwlr_screencopy_frame_v1) { + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_screencopy_frame_v1, 1, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_screencopy_frame_v1), WL_MARSHAL_FLAG_DESTROY); +} + +static void zwlr_screencopy_frame_v1_copy(void *zwlr_screencopy_frame_v1, struct wl_buffer *buffer) { + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_screencopy_frame_v1, 0, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_screencopy_frame_v1), 0, buffer); +} + +static i32 zwlr_screencopy_frame_v1_add_listener(void *zwlr_screencopy_frame_v1, struct zwlr_screencopy_frame_v1_listener *listener, void *data) { + return wl_proxy_add_listener((struct wl_proxy *) zwlr_screencopy_frame_v1, (void (**)(void)) listener, data); +} + +static void zwlr_screencopy_manager_v1_destroy(void *zwlr_screencopy_manager_v1) { + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_screencopy_manager_v1, 2, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_screencopy_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +// ================================================================ + +typedef struct { + struct wl_display * display; + struct wl_registry *registry; + struct wl_shm * shm; + void * xdg_output_manager; + void * screencopy_manager; + struct wl_list outputs; + u64 n_done; + b8 ok; +} WL_State_; + +typedef struct { + struct wl_buffer *buffer; + void * data; + i32 width; + i32 height; + i32 stride; + u64 size; + i32 format; +} WL_Buffer_; + +typedef struct { + i32 x, y; + i32 width, height; +} WL_Box_; + +typedef struct { + WL_State_ * state; + struct wl_output *output; + void * xdg_output; + struct wl_list link; + c8 * name; + WL_Buffer_ * buffer; + void * screencopy_frame; + u32 screencopy_frame_flags; +} WL_Output_; + +static i32 anonymous_shm_open(void) { + c8 name[13] = "scr_XXXXXX"; + i32 retries = 1000000; + + for (i32 i = 0; i < retries; ++i) { + snprintf(name + 4, 7, "%06d", i); + + i32 fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + return fd; + } + + if (errno != EEXIST) + break; + } + + LOG_error("shm_open failed.\n"); + return -1; +} + +static i32 create_shm_file(off_t size) { + int fd = anonymous_shm_open(); + if (fd < 0) { + return fd; + } + + if (ftruncate(fd, size) < 0) { + close(fd); + + LOG_error("ftruncate failed.\n"); + return -1; + } + + return fd; +} + +static WL_Buffer_ *create_buffer(struct wl_shm *shm, enum wl_shm_format format, i32 width, i32 height, i32 stride) { + u64 size = stride * height; + + i32 fd = create_shm_file(size); + if (fd == -1) { + return NULL; + } + + void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) { + close(fd); + + LOG_error("mmap failed.\n"); + return NULL; + } + + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); + struct wl_buffer * wl_buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, format); + wl_shm_pool_destroy(pool); + + close(fd); + + WL_Buffer_ *buffer = calloc(1, sizeof(WL_Buffer_)); + + if (buffer == NULL) { + LOG_error("calloc failed.\n"); + return NULL; + } + + *buffer = (WL_Buffer_) { + .buffer = wl_buffer, + .data = data, + .width = width, + .height = height, + .stride = stride, + .size = size, + .format = format, + }; + + return buffer; +} + +static void destroy_buffer(WL_Buffer_ *buffer) { + if (buffer == NULL) + return; + munmap(buffer->data, buffer->size); + wl_buffer_destroy(buffer->buffer); + free(buffer); +} + +static void screencopy_frame_handle_buffer( + void *data, + void *frame, + u32 format, + u32 width, + u32 height, + u32 stride +) { + WL_Output_ *output = data; + + output->buffer = create_buffer(output->state->shm, format, width, height, stride); + + if (output->buffer == NULL) { + output->state->ok = 0; + return; + } + + zwlr_screencopy_frame_v1_copy(frame, output->buffer->buffer); +} + +static void screencopy_frame_handle_flags( + void *data, + void *frame, + u32 flags +) { + (void) frame; + + WL_Output_ *output = data; + output->screencopy_frame_flags = flags; +} + +static void screencopy_frame_handle_ready( + void *data, + void *frame, + u32 tv_sec_hi, + u32 tv_sec_lo, + u32 tv_nsec +) { + (void) frame; + (void) tv_sec_hi; + (void) tv_sec_lo; + (void) tv_nsec; + + WL_Output_ *output = data; + ++output->state->n_done; +} + +static void screencopy_frame_handle_failed( + void *data, + void *frame +) { + (void) frame; + + WL_Output_ *output = data; + output->state->ok = 0; + LOG_error("Screenshot copy failed."); +} + +static struct zwlr_screencopy_frame_v1_listener screencopy_frame_listener = { + .buffer = screencopy_frame_handle_buffer, + .flags = screencopy_frame_handle_flags, + .ready = screencopy_frame_handle_ready, + .failed = screencopy_frame_handle_failed, +}; + +static void handle_global( + void * data, + struct wl_registry *registry, + u32 name, + c8 const * interface, + u32 version +) { + WL_State_ *state = data; + + if (strcmp(interface, wl_shm_interface.name) == 0) + state->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + u32 bind_version = (version > 2) ? 2 : version; + state->xdg_output_manager = wl_registry_bind( + registry, + name, + &zxdg_output_manager_v1_interface, + bind_version + ); + } else if (strcmp(interface, wl_output_interface.name) == 0) { + WL_Output_ *output = calloc(1, sizeof(WL_Output_)); + output->state = state; + output->output = wl_registry_bind(registry, name, &wl_output_interface, 3); + wl_list_insert(&state->outputs, &output->link); + } else if (strcmp(interface, zwlr_screencopy_manager_v1_interface.name) == 0) + state->screencopy_manager = wl_registry_bind( + registry, + name, + &zwlr_screencopy_manager_v1_interface, + 1 + ); +} + +static void handle_global_remove(void *data, struct wl_registry *registry, u32 name) { + (void) data; + (void) registry; + (void) name; +} + +static struct wl_registry_listener _wayland_registry_listener = { + .global = handle_global, + .global_remove = handle_global_remove, +}; + +b8 check_format_(i32 format) { + switch (format) { + case WL_SHM_FORMAT_ARGB8888: + case WL_SHM_FORMAT_XRGB8888: + case WL_SHM_FORMAT_ABGR8888: + case WL_SHM_FORMAT_XBGR8888: + case WL_SHM_FORMAT_BGRA8888: + case WL_SHM_FORMAT_BGRX8888: + case WL_SHM_FORMAT_RGBA8888: + case WL_SHM_FORMAT_RGBX8888: + return 1; + default:; + } + return 0; +} + +b8 wayland_screenshot_(i64 *width, i64 *height, i64 max_num_pixels, vec4_f32 *pixels) { + b8 ok = 0; + + WL_State_ state = { .ok = 1, }; + wl_list_init(&state.outputs); + + state.display = wl_display_connect(NULL); + + if (state.display == NULL) { + LOG_error("wl_display_connect failed."); + goto _finalize; + } + + state.registry = wl_display_get_registry(state.display); + wl_registry_add_listener( + state.registry, + &_wayland_registry_listener, + &state + ); + wl_display_roundtrip(state.display); + + if (state.shm == NULL) { + LOG_error("Compositor does not support wl_shm."); + goto _finalize; + } + + if (wl_list_empty(&state.outputs)) { + LOG_error("No wl_output."); + goto _finalize; + } + + if (state.screencopy_manager == NULL) { + LOG_error("Compositor does not support wlr-screencopy-unstable-v1."); + goto _finalize; + } + + u64 n_pending = 0; + WL_Output_ *output; + wl_list_for_each(output, &state.outputs, link) { + output->screencopy_frame = zwlr_screencopy_manager_v1_capture_output( + state.screencopy_manager, + 1, + output->output + ); + + zwlr_screencopy_frame_v1_add_listener( + output->screencopy_frame, + &screencopy_frame_listener, + output + ); + + ++n_pending; + } + + if (n_pending == 0) { + LOG_error("Supplied geometry did not intersect with any outputs."); + goto _finalize; + } + + b8 done = 0; + + while (!done && wl_display_dispatch(state.display) != -1) { + if (!state.ok) + goto _finalize; + usleep(0); + done = (state.n_done == n_pending); + } + + if (!done) { + LOG_error("Failed to screenshoot all outputs."); + goto _finalize; + } + + *width = 0; + *height = 0; + + // NOTE: We grab only one output. + + // TODO: Allow to choose the desired output. + + wl_list_for_each(output, &state.outputs, link) { + WL_Buffer_ *buffer = output->buffer; + if (buffer == NULL) continue; + if (!check_format_(buffer->format)) continue; + + *width = buffer->width; + *height = buffer->height; + break; + } + + if (pixels == NULL || *width * *height > max_num_pixels) { + ok = 1; + goto _finalize; + } + + for (i32 i = 0; i < *width * *height; ++i) + pixels[i] = (vec4_f32) { 0.f, 0.f, 0.f, 1.f }; + + wl_list_for_each(output, &state.outputs, link) { + WL_Buffer_ *buffer = output->buffer; + if (buffer == NULL) continue; + if (!check_format_(buffer->format)) continue; + + if (*width != buffer->width) { + LOG_error("Sanity"); + break; + } + + for (i32 j = 0; j < buffer->height; ++j) { + vec4_f32 *d = pixels + j * *width; + vec4_f32 *d_end = d + *width; + u32 *s = ((u32 *) buffer->data) + j * (buffer->stride / 4); + + for (; d < d_end; ++d, ++s) { + vec3_f32 c = rgb_f32_from_u32_(*s); + *d = (vec4_f32) { + .x = c.x, + .y = c.y, + .z = c.z, + .w = 1.f, + }; + } + } + + break; + } + + ok = 1; + + WL_Output_ *output_tmp; + +_finalize: + wl_list_for_each_safe(output, output_tmp, &state.outputs, link) { + wl_list_remove(&output->link); + free(output->name); + if (output->screencopy_frame != NULL) + zwlr_screencopy_frame_v1_destroy(output->screencopy_frame); + destroy_buffer(output->buffer); + if (output->xdg_output != NULL) + zxdg_output_v1_destroy(output->xdg_output); + wl_output_release(output->output); + free(output); + } + zwlr_screencopy_manager_v1_destroy(state.screencopy_manager); + if (state.xdg_output_manager != NULL) + zxdg_output_manager_v1_destroy(state.xdg_output_manager); + + if (state.shm != NULL) wl_shm_destroy(state.shm); + if (state.registry != NULL) wl_registry_destroy(state.registry); + if (state.display != NULL) wl_display_disconnect(state.display); + + return ok; +} + +#endif // defined(__linux__) && ENABLE_WAYLAND + +// ================================================================ +// +// X11 +// +// ================================================================ + +#if defined(__linux__) && ENABLE_X11 + +#include +#include +#include +#include + +#define FILE_PATH_PREFIX_ "file://" +#define FILE_PATH_PREFIX_LEN_ ((sizeof FILE_PATH_PREFIX_) - 1) + +static i64 _frame_time = 0; +static XImage _image = {0}; +static Display *_display = NULL; +static Window _root_window = 0; +static GC _gc = NULL; +static XIM _im = NULL; +static XIC _ic = NULL; +static Window _window = 0; +static Window _drop_source = 0; +static Atom _wm_delete_window = 0; +static Atom _clipboard = 0; +static Atom _targets = 0; +static Atom _text_plain = 0; +static Atom _utf8_string = 0; +static Atom _image_bmp = 0; +static Atom _image_ppm = 0; +static Atom _audio_wav = 0; +static Atom _dnd_aware = 0; +static Atom _dnd_enter = 0; +static Atom _dnd_position = 0; +static Atom _dnd_status = 0; +static Atom _dnd_leave = 0; +static Atom _dnd_drop = 0; +static Atom _dnd_finished = 0; +static Atom _dnd_action_copy = 0; +static Atom _dnd_selection = 0; +static Atom _text_uri_list = 0; +static Window _dnd_source = 0; +static b8 _mapped = 0; +static b8 _requested_clipboard = 0; + +static i16 _key_table [MAX_NUM_KEYS] = {0}; +static b8 _key_repeat [MAX_NUM_KEYS] = {0}; +static c8 _error_buffer [4096] = {0}; + +static b8 sub_str_eq_(c8 *a, c8 *b, i64 len) { + for (i64 i = 0; i < len; ++i) + if (a[i] != b[i]) + return 0; + return 1; +} + +static i32 x11_error_handler_(Display *display, XErrorEvent *event) { + XGetErrorText(display, event->error_code, _error_buffer, sizeof _error_buffer - 1); + LOG_error("%s", _error_buffer); + return 0; +} + +void put_image_to_main_window_(void) { + if (_image.width <= 0 || _image.height <= 0) + return; + + XPutImage(_display, _window, _gc, &_image, 0, 0, 0, 0, _image.width, _image.height); + XFlush(_display); +} + +static void init_profiler_slots(void) { + PROFILER_init(PROFILE_MEMORY, "Memory allocator"); + PROFILER_init(PROFILE_DOWNSCALE, "Downscale anti-aliasing"); + PROFILER_init(PROFILE_RESIZE, "Resize in-place"); + PROFILER_init(PROFILE_WAITING, "Waiting for events"); + PROFILER_init(PROFILE_FRAME, "Update frame"); + PROFILER_init(PROFILE_DRAW_PIXELS, "Draw pixels"); + PROFILER_init(PROFILE_FILL_RECTANGLE, "Fill rectangle"); + PROFILER_init(PROFILE_FILL_TRIANGLE, "Fill triangle"); + PROFILER_init(PROFILE_FILL_TRIANGLES, "Fill triangles"); + PROFILER_init(PROFILE_FILL_QUAD, "Fill quad"); + PROFILER_init(PROFILE_FILL_ELLIPSE, "Fill ellipse"); + PROFILER_init(PROFILE_FILL_LINE, "Fill line"); + PROFILER_init(PROFILE_DRAW_TEXT_AREA, "Draw text area"); + PROFILER_init(PROFILE_DRAW_TEXT_CURSOR, "Draw text cursor"); +} + +void init_main_window(void) { + init_profiler_slots(); + + for (i64 i = 0; i < NUM_FRAMES_AVERAGED; ++i) + _frame_duration[i] = (MIN_FRAME_DURATION + MAX_FRAME_DURATION) / 2; + + XSetErrorHandler(x11_error_handler_); + + _display = XOpenDisplay(NULL); + + if (_display == NULL) { + LOG_error("XOpenDisplay failed."); + return; + } + + _root_window = XDefaultRootWindow(_display); + + _key_table[XKeysymToKeycode(_display, XK_Left)] = KEY_LEFT; + _key_table[XKeysymToKeycode(_display, XK_Right)] = KEY_RIGHT; + _key_table[XKeysymToKeycode(_display, XK_Up)] = KEY_UP; + _key_table[XKeysymToKeycode(_display, XK_Down)] = KEY_DOWN; + _key_table[XKeysymToKeycode(_display, XK_1)] = KEY_1; + _key_table[XKeysymToKeycode(_display, XK_2)] = KEY_2; + _key_table[XKeysymToKeycode(_display, XK_3)] = KEY_3; + _key_table[XKeysymToKeycode(_display, XK_4)] = KEY_4; + _key_table[XKeysymToKeycode(_display, XK_5)] = KEY_5; + _key_table[XKeysymToKeycode(_display, XK_6)] = KEY_6; + _key_table[XKeysymToKeycode(_display, XK_7)] = KEY_7; + _key_table[XKeysymToKeycode(_display, XK_8)] = KEY_8; + _key_table[XKeysymToKeycode(_display, XK_9)] = KEY_9; + _key_table[XKeysymToKeycode(_display, XK_0)] = KEY_0; + _key_table[XKeysymToKeycode(_display, XK_A)] = KEY_A; + _key_table[XKeysymToKeycode(_display, XK_B)] = KEY_B; + _key_table[XKeysymToKeycode(_display, XK_C)] = KEY_C; + _key_table[XKeysymToKeycode(_display, XK_D)] = KEY_D; + _key_table[XKeysymToKeycode(_display, XK_E)] = KEY_E; + _key_table[XKeysymToKeycode(_display, XK_F)] = KEY_F; + _key_table[XKeysymToKeycode(_display, XK_G)] = KEY_G; + _key_table[XKeysymToKeycode(_display, XK_H)] = KEY_H; + _key_table[XKeysymToKeycode(_display, XK_I)] = KEY_I; + _key_table[XKeysymToKeycode(_display, XK_J)] = KEY_J; + _key_table[XKeysymToKeycode(_display, XK_K)] = KEY_K; + _key_table[XKeysymToKeycode(_display, XK_L)] = KEY_L; + _key_table[XKeysymToKeycode(_display, XK_M)] = KEY_M; + _key_table[XKeysymToKeycode(_display, XK_N)] = KEY_N; + _key_table[XKeysymToKeycode(_display, XK_O)] = KEY_O; + _key_table[XKeysymToKeycode(_display, XK_P)] = KEY_P; + _key_table[XKeysymToKeycode(_display, XK_Q)] = KEY_Q; + _key_table[XKeysymToKeycode(_display, XK_R)] = KEY_R; + _key_table[XKeysymToKeycode(_display, XK_S)] = KEY_S; + _key_table[XKeysymToKeycode(_display, XK_T)] = KEY_T; + _key_table[XKeysymToKeycode(_display, XK_U)] = KEY_U; + _key_table[XKeysymToKeycode(_display, XK_V)] = KEY_V; + _key_table[XKeysymToKeycode(_display, XK_W)] = KEY_W; + _key_table[XKeysymToKeycode(_display, XK_X)] = KEY_X; + _key_table[XKeysymToKeycode(_display, XK_Y)] = KEY_Y; + _key_table[XKeysymToKeycode(_display, XK_Z)] = KEY_Z; + _key_table[XKeysymToKeycode(_display, XK_space)] = KEY_SPACE; + _key_table[XKeysymToKeycode(_display, XK_braceleft)] = KEY_LBRACE; + _key_table[XKeysymToKeycode(_display, XK_braceright)] = KEY_RBRACE; + _key_table[XKeysymToKeycode(_display, XK_colon)] = KEY_COLON; + _key_table[XKeysymToKeycode(_display, XK_quotedbl)] = KEY_QUOTE; + _key_table[XKeysymToKeycode(_display, XK_asciitilde)] = KEY_TILDE; + _key_table[XKeysymToKeycode(_display, XK_backslash)] = KEY_BACKSLASH; + _key_table[XKeysymToKeycode(_display, XK_comma)] = KEY_COMMA; + _key_table[XKeysymToKeycode(_display, XK_greater)] = KEY_PERIOD; + _key_table[XKeysymToKeycode(_display, XK_question)] = KEY_SLASH; + _key_table[XKeysymToKeycode(_display, XK_minus)] = KEY_MINUS; + _key_table[XKeysymToKeycode(_display, XK_equal)] = KEY_EQUAL; + _key_table[XKeysymToKeycode(_display, XK_F1)] = KEY_F1; + _key_table[XKeysymToKeycode(_display, XK_F2)] = KEY_F2; + _key_table[XKeysymToKeycode(_display, XK_F3)] = KEY_F3; + _key_table[XKeysymToKeycode(_display, XK_F4)] = KEY_F4; + _key_table[XKeysymToKeycode(_display, XK_F5)] = KEY_F5; + _key_table[XKeysymToKeycode(_display, XK_F6)] = KEY_F6; + _key_table[XKeysymToKeycode(_display, XK_F7)] = KEY_F7; + _key_table[XKeysymToKeycode(_display, XK_F8)] = KEY_F8; + _key_table[XKeysymToKeycode(_display, XK_F9)] = KEY_F9; + _key_table[XKeysymToKeycode(_display, XK_F10)] = KEY_F10; + _key_table[XKeysymToKeycode(_display, XK_F11)] = KEY_F11; + _key_table[XKeysymToKeycode(_display, XK_F12)] = KEY_F12; + _key_table[XKeysymToKeycode(_display, XK_Control_L)] = KEY_LCTRL; + _key_table[XKeysymToKeycode(_display, XK_Control_R)] = KEY_RCTRL; + _key_table[XKeysymToKeycode(_display, XK_Shift_L)] = KEY_LSHIFT; + _key_table[XKeysymToKeycode(_display, XK_Shift_R)] = KEY_RSHIFT; + _key_table[XKeysymToKeycode(_display, XK_Alt_L)] = KEY_LALT; + _key_table[XKeysymToKeycode(_display, XK_Alt_R)] = KEY_RALT; + _key_table[XKeysymToKeycode(_display, XK_Meta_L)] = KEY_LMETA; + _key_table[XKeysymToKeycode(_display, XK_Meta_R)] = KEY_RMETA; + _key_table[XKeysymToKeycode(_display, XK_Escape)] = KEY_ESCAPE; + _key_table[XKeysymToKeycode(_display, XK_BackSpace)] = KEY_BACKSPACE; + _key_table[XKeysymToKeycode(_display, XK_Tab)] = KEY_TAB; + _key_table[XKeysymToKeycode(_display, XK_Return)] = KEY_ENTER; + _key_table[XKeysymToKeycode(_display, XK_Print)] = KEY_PRINTSCREEN; + _key_table[XKeysymToKeycode(_display, XK_Delete)] = KEY_DELETE; + _key_table[XKeysymToKeycode(_display, XK_Pause)] = KEY_PAUSE; + _key_table[XKeysymToKeycode(_display, XK_Insert)] = KEY_INSERT; + _key_table[XKeysymToKeycode(_display, XK_Home)] = KEY_HOME; + _key_table[XKeysymToKeycode(_display, XK_End)] = KEY_END; + _key_table[XKeysymToKeycode(_display, XK_Page_Up)] = KEY_PAGEUP; + _key_table[XKeysymToKeycode(_display, XK_Page_Down)] = KEY_PAGEDOWN; + _key_table[XKeysymToKeycode(_display, XK_KP_0)] = KEY_KP_0; + _key_table[XKeysymToKeycode(_display, XK_KP_1)] = KEY_KP_1; + _key_table[XKeysymToKeycode(_display, XK_KP_2)] = KEY_KP_2; + _key_table[XKeysymToKeycode(_display, XK_KP_3)] = KEY_KP_3; + _key_table[XKeysymToKeycode(_display, XK_KP_4)] = KEY_KP_4; + _key_table[XKeysymToKeycode(_display, XK_KP_5)] = KEY_KP_5; + _key_table[XKeysymToKeycode(_display, XK_KP_6)] = KEY_KP_6; + _key_table[XKeysymToKeycode(_display, XK_KP_7)] = KEY_KP_7; + _key_table[XKeysymToKeycode(_display, XK_KP_8)] = KEY_KP_8; + _key_table[XKeysymToKeycode(_display, XK_KP_9)] = KEY_KP_9; + _key_table[XKeysymToKeycode(_display, XK_KP_Enter)] = KEY_KP_ENTER; + _key_table[XKeysymToKeycode(_display, XK_KP_Divide)] = KEY_KP_DIVIDE; + _key_table[XKeysymToKeycode(_display, XK_KP_Multiply)] = KEY_KP_MULTIPLY; + _key_table[XKeysymToKeycode(_display, XK_KP_Add)] = KEY_KP_ADD; + _key_table[XKeysymToKeycode(_display, XK_KP_Subtract)] = KEY_KP_SUBTRACT; + _key_table[XKeysymToKeycode(_display, XK_KP_Decimal)] = KEY_KP_DECIMAL; + _key_table[XKeysymToKeycode(_display, XK_KP_Separator)] = KEY_KP_SEPARATOR; + + i32 screen = DefaultScreen(_display); + i32 depth = DefaultDepth (_display, screen); + Visual *visual = DefaultVisual(_display, screen); + + _gc = DefaultGC(_display, screen); + + if (_gc == NULL) { + LOG_error("DefaultGC failed."); + return; + } + + XSetGraphicsExposures(_display, _gc, False); + + i32 display_width = DisplayWidth (_display, screen); + i32 display_height = DisplayHeight(_display, screen); + + if (g_platform.frame_width <= 0 || g_platform.frame_height <= 0) { + if (display_width >= 1680 && display_height >= 1020) { + g_platform.frame_width = 1280; + g_platform.frame_height = 720; + } else if (display_width >= 800 && display_height >= 600) { + g_platform.frame_width = display_width - 400; + g_platform.frame_height = display_height - 300; + } else if (display_width >= 400 && display_height >= 300) { + g_platform.frame_width = 400; + g_platform.frame_height = 300; + } else { + g_platform.frame_width = display_width; + g_platform.frame_height = display_height; + } + } + + i32 x = (display_width - g_platform.frame_width) / 2; + i32 y = (display_height - g_platform.frame_height) / 2; + + _window = XCreateWindow(_display, _root_window, x, y, g_platform.frame_width, g_platform.frame_height, 0, depth, InputOutput, visual, CWEventMask, &(XSetWindowAttributes) { .event_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask | PointerMotionMask | VisibilityChangeMask | FocusChangeMask | StructureNotifyMask | SubstructureNotifyMask, }); + + if (_window == 0) { + LOG_error("XCreateWindow failed."); + return; + } + + _im = XOpenIM(_display, NULL, NULL, NULL); + + if (_im == NULL) { + LOG_error("XOpenIM failed."); + return; + } + + _ic = XCreateIC(_im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, _window, NULL); + + if (_ic == NULL) { + LOG_error("XCreateIC failed."); + return; + } + + pixel_size_update_(g_platform.frame_width, g_platform.frame_height); + + _image = (XImage) { + .width = _internal_width, + .height = _internal_height, + .depth = depth, + .format = ZPixmap, + .data = (c8 *) _internal_pixels, + .byte_order = LSBFirst, + .bitmap_unit = 32, + .bitmap_bit_order = LSBFirst, + .bitmap_pad = 32, + .bits_per_pixel = 32, + .bytes_per_line = _image.width * 4, + .red_mask = 0xff0000, + .green_mask = 0x00ff00, + .blue_mask = 0x0000ff, + }; + + XInitImage(&_image); + + _wm_delete_window = XInternAtom(_display, "WM_DELETE_WINDOW", False); + _clipboard = XInternAtom(_display, "CLIPBOARD", False); + _targets = XInternAtom(_display, "TARGETS", False); + _text_plain = XInternAtom(_display, "text/plain", False); + _utf8_string = XInternAtom(_display, "UTF8_STRING", False); + _image_bmp = XInternAtom(_display, "image/bmp", False); + _image_ppm = XInternAtom(_display, "image/ppm", False); + _audio_wav = XInternAtom(_display, "audio/wav", False); + + XSetICFocus(_ic); + XSetWMProtocols(_display, _window, &_wm_delete_window, 1); + + if (g_platform.title != NULL) + XStoreName(_display, _window, g_platform.title); + + _dnd_aware = XInternAtom(_display, "XdndAware", False); + _dnd_enter = XInternAtom(_display, "XdndEnter", False); + _dnd_position = XInternAtom(_display, "XdndPosition", False); + _dnd_status = XInternAtom(_display, "XdndStatus", False); + _dnd_leave = XInternAtom(_display, "XdndLeave", False); + _dnd_drop = XInternAtom(_display, "XdndDrop", False); + _dnd_finished = XInternAtom(_display, "XdndFinished", False); + _dnd_action_copy = XInternAtom(_display, "XdndActionCopy", False); + _dnd_selection = XInternAtom(_display, "XdndSelection", False); + _text_uri_list = XInternAtom(_display, "text/uri-list", False); + + XChangeProperty(_display, _window, _dnd_aware, 4, 32, PropModeReplace, &(u8) {5}, 1); + + _mapped = 0; +} + +void shutdown_all_systems(void) { + PROFILER_report_(); + + g_platform.done = 1; + + if (!g_platform.graceful_shutdown) + return; + + if (_window != 0) + XDestroyWindow(_display, _window); + if (_display != NULL) + XCloseDisplay (_display); + + _window = 0; + _display = NULL; + + drop_files_clean_(); + cleanup_pixel_buffers_(); + network_cleanup_(); + sound_cleanup_(); + close_all_dynamic_libraries_(); + + g_platform.input_len = 0; + resize_dynamic_array_exact(&g_platform.input_capacity, (void **) &g_platform.input, sizeof *g_platform.input, 0); + resize_dynamic_array_exact(&g_platform.clipboard_text_len, (void **) &g_platform.clipboard_text, 1, 0); +} + +void request_clipboard_(void) { + if (_requested_clipboard) + return; + if (!g_platform.enable_clipboard_text && !g_platform.enable_clipboard_image && !g_platform.enable_clipboard_sound) + return; + + XConvertSelection(_display, _clipboard, _targets, _clipboard, _window, CurrentTime); + _requested_clipboard = 1; +} + +i32 handle_main_window_events(void) { + if (_display == NULL) + return 0; + + _frame_time = current_utc_time_in_milliseconds(); + + i32 num_events = 0; + + if (!_mapped) + // We have to make sure that the user won't be waiting for events indefinitely. + ++num_events; + + if (g_platform.files_dropped) + drop_files_clean_(); + + mem_set_(g_platform.key_pressed, 0, sizeof g_platform.key_pressed); + mem_set_(_key_repeat, 0, sizeof _key_repeat); + + resize_dynamic_array_capacity(&g_platform.input_len, &g_platform.input_capacity, (void **) &g_platform.input, sizeof *g_platform.input, 0); + + g_platform.cursor_dx = 0; + g_platform.cursor_dy = 0; + g_platform.wheel_dx = 0; + g_platform.wheel_dy = 0; + + XEvent ev; + + while (XEventsQueued(_display, QueuedAlready) > 0) { + ++num_events; + + XNextEvent(_display, &ev); + XFilterEvent(&ev, _window); + + switch (ev.type) { + case DestroyNotify: + g_platform.done = 1; + break; + + case MotionNotify: + g_platform.cursor_dx += ev.xmotion.x - g_platform.cursor_x; + g_platform.cursor_dy += ev.xmotion.y - g_platform.cursor_y; + g_platform.cursor_x = ev.xmotion.x; + g_platform.cursor_y = ev.xmotion.y; + break; + + case ButtonPress: + g_platform.cursor_x = ev.xbutton.x; + g_platform.cursor_y = ev.xbutton.y; + switch (ev.xbutton.button) { + case Button1: + g_platform.key_down [BUTTON_LEFT] = 1; + g_platform.key_pressed[BUTTON_LEFT] = 1; + break; + case Button2: + g_platform.key_down [BUTTON_MIDDLE] = 1; + g_platform.key_pressed[BUTTON_MIDDLE] = 1; + break; + case Button3: + g_platform.key_down [BUTTON_RIGHT] = 1; + g_platform.key_pressed[BUTTON_RIGHT] = 1; + break; + case Button4: g_platform.wheel_dy += MOUSE_WHEEL_FACTOR; break; + case Button5: g_platform.wheel_dy -= MOUSE_WHEEL_FACTOR; break; + case Button5 + 1: g_platform.wheel_dx -= MOUSE_WHEEL_FACTOR; break; + case Button5 + 2: g_platform.wheel_dx += MOUSE_WHEEL_FACTOR; break; + default:; + } + break; + + case ButtonRelease: + g_platform.cursor_x = ev.xbutton.x; + g_platform.cursor_y = ev.xbutton.y; + switch (ev.xbutton.button) { + case Button1: g_platform.key_down[BUTTON_LEFT] = 0; break; + case Button2: g_platform.key_down[BUTTON_MIDDLE] = 0; break; + case Button3: g_platform.key_down[BUTTON_RIGHT] = 0; break; + default:; + } + break; + + case KeyPress: { + i16 k = _key_table[ev.xkey.keycode]; + g_platform.cursor_x = ev.xkey.x; + g_platform.cursor_y = ev.xkey.y; + + g_platform.key_down[k] = 1; + if (!_key_repeat[k]) + g_platform.key_pressed[k] = 1; + + g_platform.key_down[MOD_CTRL] = !!(ev.xkey.state & ControlMask); + g_platform.key_down[MOD_SHIFT] = !!(ev.xkey.state & ShiftMask); + g_platform.key_down[MOD_ALT] = !!(ev.xkey.state & Mod1Mask); + g_platform.key_down[MOD_CAPS] = !!(ev.xkey.state & LockMask); + g_platform.key_down[MOD_NUM] = !!(ev.xkey.state & Mod2Mask); + g_platform.key_down[MOD_SCROLL] = !!(ev.xkey.state & Mod3Mask); + g_platform.key_down[MOD_META] = !!(ev.xkey.state & Mod4Mask); + + i64 n = g_platform.input_len; + resize_dynamic_array_capacity(&g_platform.input_len, &g_platform.input_capacity, (void **) &g_platform.input, sizeof *g_platform.input, n + 1); + + if (n < g_platform.input_len) { + if (k < 32 || k >= 128) + g_platform.input[n] = (Input_Key) { + .ctrl = !!(ev.xkey.state & ControlMask), + .shift = !!(ev.xkey.state & ShiftMask), + .alt = !!(ev.xkey.state & Mod1Mask), + .caps = !!(ev.xkey.state & LockMask), + .num = !!(ev.xkey.state & Mod2Mask), + .scroll = !!(ev.xkey.state & Mod3Mask), + .meta = !!(ev.xkey.state & Mod4Mask), + .repeat = _key_repeat[k], + .key = k, + .c = 0, + }; + else { + c8 buf[16]; + i32 len = Xutf8LookupString(_ic, &ev.xkey, buf, sizeof buf - 1, NULL, NULL); + if (len > 0) + g_platform.input[n] = (Input_Key) { + .ctrl = !!(ev.xkey.state & ControlMask), + .shift = !!(ev.xkey.state & ShiftMask), + .alt = !!(ev.xkey.state & Mod1Mask), + .caps = !!(ev.xkey.state & LockMask), + .num = !!(ev.xkey.state & Mod2Mask), + .scroll = !!(ev.xkey.state & Mod3Mask), + .meta = !!(ev.xkey.state & Mod4Mask), + .repeat = _key_repeat[k], + .key = k, + .c = utf8_read(len, buf).code, + }; + } + } + } break; + + case KeyRelease: { + i16 k = _key_table[ev.xkey.keycode]; + + g_platform.cursor_x = ev.xkey.x; + g_platform.cursor_y = ev.xkey.y; + + g_platform.key_down[k] = 0; + _key_repeat[k] = 1; + + g_platform.key_down[MOD_CTRL] = !!(ev.xkey.state & ControlMask); + g_platform.key_down[MOD_SHIFT] = !!(ev.xkey.state & ShiftMask); + g_platform.key_down[MOD_ALT] = !!(ev.xkey.state & Mod1Mask); + g_platform.key_down[MOD_CAPS] = !!(ev.xkey.state & LockMask); + g_platform.key_down[MOD_NUM] = !!(ev.xkey.state & Mod2Mask); + g_platform.key_down[MOD_SCROLL] = !!(ev.xkey.state & Mod3Mask); + g_platform.key_down[MOD_META] = !!(ev.xkey.state & Mod4Mask); + } break; + + case SelectionRequest: + if (ev.xselectionrequest.requestor != _window && + ev.xselectionrequest.selection == _clipboard && + XGetSelectionOwner(_display, _clipboard) == _window) { + if (ev.xselectionrequest.property != None) { + if (ev.xselectionrequest.target == _targets) { + b8 has_text = g_platform.clipboard_text_len > 0; + b8 has_image = g_platform.clipboard_image_width > 0 && g_platform.clipboard_image_height > 0; + b8 has_sound = g_platform.num_clipboard_sound_samples > 0; + + if (!has_text && !has_image && !has_sound) + XDeleteProperty( + ev.xselectionrequest.display, + ev.xselectionrequest.requestor, + ev.xselectionrequest.property + ); + else + XChangeProperty( + ev.xselectionrequest.display, + ev.xselectionrequest.requestor, + ev.xselectionrequest.property, + XA_ATOM, + 32, + PropModeReplace, + has_text && has_image && has_sound ? (u8 *) &(Atom[]) { XA_STRING, _text_plain, _utf8_string, _image_ppm, _image_bmp, _audio_wav, } + : has_text && has_image ? (u8 *) &(Atom[]) { XA_STRING, _text_plain, _utf8_string, _image_ppm, _image_bmp, } + : has_text && has_sound ? (u8 *) &(Atom[]) { XA_STRING, _text_plain, _utf8_string, _audio_wav, } + : has_image && has_sound ? (u8 *) &(Atom[]) { _image_ppm, _image_bmp, _audio_wav, } + : has_text ? (u8 *) &(Atom[]) { XA_STRING, _text_plain, _utf8_string, } + : has_image ? (u8 *) &(Atom[]) { _image_ppm, _image_bmp, } + : (u8 *) &(Atom[]) { _audio_wav, }, + has_text * 3 + has_image * 2 + has_sound * 1 + ); + } else if (ev.xselectionrequest.target == XA_STRING + || ev.xselectionrequest.target == _text_plain + || ev.xselectionrequest.target == _utf8_string) { + XChangeProperty( + ev.xselectionrequest.display, + ev.xselectionrequest.requestor, + ev.xselectionrequest.property, + ev.xselectionrequest.target, + 8, + PropModeReplace, + (u8 *) g_platform.clipboard_text, + g_platform.clipboard_text_len + ); + } else if (ev.xselectionrequest.target == _image_bmp) { + // TODO + LOG_error("Send BMP image - not implemented."); + } else if (ev.xselectionrequest.target == _image_ppm) { + // TODO + LOG_error("Send PPM image - not implemented."); + } else if (ev.xselectionrequest.target == _audio_wav) { + // TODO + LOG_error("Send WAV audio - not implemented."); + } + } + + XSendEvent(_display, ev.xselectionrequest.requestor, 0, 0, (XEvent *) &(XSelectionEvent) { + .type = SelectionNotify, + .serial = ev.xselectionrequest.serial, + .send_event = ev.xselectionrequest.send_event, + .display = ev.xselectionrequest.display, + .requestor = ev.xselectionrequest.requestor, + .selection = ev.xselectionrequest.selection, + .target = ev.xselectionrequest.target, + .property = ev.xselectionrequest.property, + .time = ev.xselectionrequest.time, + }); + } + break; + + case SelectionNotify: + if (ev.xselection.property == _dnd_selection) { + u8 *data; + i32 len; + + XGetWindowProperty( + _display, + ev.xselection.requestor, + _dnd_selection, + 0, + STATIC_MEMORY_BUFFER_SIZE, // FIXME + False, + ev.xselection.target, + &(Atom) {0}, + &(i32) {0}, + (void *) &len, + &(unsigned long) {0}, + &data + ); + + if (len == 0) { + LOG_error("Unable to retrieve drag and drop info."); + break; + } + + if (data) { + i64 num = 0; + for (i64 i = 0; i < len; ++i) + if (data[i] == '\n' || i + 1 == len) + ++num; + drop_files_set_num_(num); + + i64 index = 0; + i64 offset = 0; + + for (i64 i = 0; i < len; ++i) + if (data[i] == '\n' || i + 1 == len) { + c8 *name = (c8 *) data + offset; + + if (sub_str_eq_(name, FILE_PATH_PREFIX_, FILE_PATH_PREFIX_LEN_)) + name = name + FILE_PATH_PREFIX_LEN_; + + i64 name_len = 0; + while (name_len < (c8 *) data + len - name + && name[name_len] != '\n' + && name[name_len] != '\r') + ++name_len; + + drop_files_set_name_(index, name_len, name); + + ++index; + offset = i + 1; + } + + XFree(data); + } + } else if (ev.xselection.property != None) { + i64 len = 0; + u8 *data = NULL; + + XGetWindowProperty( + _display, + _window, + _clipboard, + 0, + STATIC_MEMORY_BUFFER_SIZE, // FIXME + False, + AnyPropertyType, + &(Atom) {0}, + &(i32) {0}, + (void *) &len, + &(unsigned long) {0}, + &data + ); + + if (ev.xselection.target == _targets) { + Atom *list = (Atom *) data; + Atom target_text = None; + Atom target_image = None; + Atom target_sound = None; + len /= 4; + + for (i64 i = 0; i < len; i++) + if (list[i] == XA_STRING) + target_text = XA_STRING; + else if (list[i] == _text_plain) + target_text = _text_plain; + else if (list[i] == _utf8_string) { + target_text = _utf8_string; + break; + } + + for (i64 i = 0; i < len; i++) + if (list[i] == _image_ppm) + target_image = _image_ppm; + else if (list[i] == _image_bmp) { + target_image = _image_bmp; + break; + } + + for (i64 i = 0; i < len; i++) + if (list[i] == _audio_wav) { + target_sound = _audio_wav; + break; + } + + if (g_platform.enable_clipboard_text && target_text != None) + XConvertSelection(_display, _clipboard, target_text, _clipboard, _window, CurrentTime); + + if (g_platform.enable_clipboard_image && target_image != None) + XConvertSelection(_display, _clipboard, target_image, _clipboard, _window, CurrentTime); + + if (g_platform.enable_clipboard_sound && target_sound != None) + XConvertSelection(_display, _clipboard, target_sound, _clipboard, _window, CurrentTime); + + if (target_text == None && target_image == None && target_sound == None) + _requested_clipboard = 0; + } else if (ev.xselection.target == XA_STRING || ev.xselection.target == _text_plain || ev.xselection.target == _utf8_string) { + if (g_platform.enable_clipboard_text) { + resize_dynamic_array_exact(&g_platform.clipboard_text_len, (void **) &g_platform.clipboard_text, 1, len + 1); + if (g_platform.clipboard_text_len == len + 1) { + mem_cpy_(g_platform.clipboard_text, data, len); + g_platform.clipboard_text[len] = '\0'; + } + } + _requested_clipboard = 0; + } else if (ev.xselection.target == _image_bmp) { + if (g_platform.enable_clipboard_image) { + // TODO + LOG_error("Receive BMP image - not implemented."); + } + _requested_clipboard = 0; + } else if (ev.xselection.target == _image_ppm) { + if (g_platform.enable_clipboard_image) { + // TODO + LOG_error("Receive PPM image - not implemented."); + } + _requested_clipboard = 0; + } else if (ev.xselection.target == _audio_wav) { + if (g_platform.enable_clipboard_sound) { + // TODO + LOG_error("Receive WAV audio - not implemented."); + } + _requested_clipboard = 0; + } + + if (data) + XFree(data); + } + + break; + + case EnterNotify: g_platform.has_cursor = 1; break; + case LeaveNotify: g_platform.has_cursor = 0; break; + case FocusOut: g_platform.has_focus = 0; break; + + case FocusIn: + g_platform.has_focus = 1; + request_clipboard_(); + break; + + case MappingNotify: + XRefreshKeyboardMapping(&ev.xmapping); + break; + + case ClientMessage: + if ((Atom) ev.xclient.data.l[0] == _wm_delete_window) + g_platform.done = 1; + else { + if (ev.xclient.message_type == _dnd_enter) + _dnd_source = ev.xclient.data.l[0]; + else if (ev.xclient.message_type == _dnd_position) { + i32 xabs = (ev.xclient.data.l[2] >> 16) & 0xffff; + i32 yabs = ev.xclient.data.l[2] & 0xffff; + + Window dummy; + i32 xpos; + i32 ypos; + + XTranslateCoordinates( + _display, + XDefaultRootWindow(_display), + _window, + xabs, yabs, + &xpos, &ypos, + &dummy + ); + + g_platform.cursor_dx += xpos - g_platform.cursor_x; + g_platform.cursor_dy += ypos - g_platform.cursor_y; + g_platform.cursor_x = xpos; + g_platform.cursor_y = ypos; + + if (!g_platform.files_dragged) { + XConvertSelection( + _display, + _dnd_selection, + _text_uri_list, + _dnd_selection, + _window, + ev.xclient.data.l[2] + ); + + g_platform.files_dragged = 1; + } + + XSendEvent( + _display, + _dnd_source, + False, + NoEventMask, + &(XEvent) { + .xclient = { + .type = ClientMessage, + .message_type = _dnd_status, + .window = _drop_source, + .format = 32, + .data = { + .l = { _window, 1, 0, 0, _dnd_action_copy, }, + }, + }, + } + ); + + XFlush(_display); + } else if (ev.xclient.message_type == _dnd_drop) { + if (g_platform.drop_files == NULL) + LOG_error("No drop files."); + else + for (i64 i = 0; i < g_platform.num_drop_files; ++i) { + FILE *f = fopen(g_platform.drop_files[i].name, "rb"); + + if (f == NULL) + LOG_error("Unable to open: %s", g_platform.drop_files[i].name); + + i64 size = 0; + + if (f != NULL) { + struct stat info; + if (fstat(fileno(f), &info) == 0 && S_ISREG(info.st_mode)) + size = (i64) info.st_size; + else + LOG_error("Unable to get the file info: %s", g_platform.drop_files[i].name); + } + + if (size > 0) { + drop_files_set_data_(i, size); + + i64 num = (i64) fread(g_platform.drop_files[i].data, 1, g_platform.drop_files[i].data_size + 1, f); + fclose(f); + + if (num != g_platform.drop_files[i].data_size) { + LOG_error("Unable to read: %s", g_platform.drop_files[i].name); + drop_files_set_data_(i, 0); + } + } + } + + g_platform.files_dropped = 1; + + XSendEvent( + _display, + _dnd_source, + False, + NoEventMask, + &(XEvent) { + .xclient = { + .type = ClientMessage, + .message_type = _dnd_finished, + .window = _drop_source, + .format = 32, + .data = { + .l = { _window, 1, _dnd_action_copy, }, + }, + }, + } + ); + + XFlush(_display); + } else if (ev.xclient.message_type == _dnd_leave) { + if (!g_platform.files_dropped) + drop_files_clean_(); + } + } + + break; + + default:; + } + } + + XWindowAttributes attrs; + XGetWindowAttributes(_display, _window, &attrs); + + num_events += pixel_size_update_(attrs.width, attrs.height); + + _image.width = _internal_width; + _image.height = _internal_height; + _image.data = (c8 *) _internal_pixels; + + _image.bytes_per_line = _image.width * 4; + + return num_events; +} + +i32 wait_main_window_events(void) { + PROFILER_begin(PROFILE_WAITING); + + i32 num_events = 0; + + for (;;) { + num_events = handle_main_window_events(); + if (num_events != 0) + break; + usleep(0); + } + + PROFILER_end(PROFILE_WAITING); + return num_events; +} + +void render_main_window_frame(void) { + if (g_platform.done) + return; + + if (!_mapped) { + XMapWindow(_display, _window); + request_clipboard_(); + _mapped = 1; + } + + convert_pixels_for_window_(); + + put_image_to_main_window_(); + + pixel_size_calibrate_(current_utc_time_in_milliseconds() - _frame_time); +} + +void write_clipboard_text(i64 size, c8 *data) { + if (size < 0 || data == NULL) + return; + + store_clipboard_text_(size, data); + + if (g_platform.clipboard_text_len > 0) + XSetSelectionOwner(_display, _clipboard, _window, CurrentTime); +} + +b8 x11_get_root_(void) { + Window root, parent, *children; + u32 num_children; + + if(!XQueryTree(_display, _window, &root, &parent, &children, &num_children)) + return 0; + + if (children != NULL) + XFree((c8 *) children); + + _root_window = root; + return 1; +} + +b8 x11_screenshot_(i64 *width, i64 *height, i64 max_num_pixels, vec4_f32 *pixels) { + if (_display == NULL) { + LOG_error("No display."); + return 0; + } + + i32 screen = DefaultScreen(_display); + i32 display_width = DisplayWidth (_display, screen); + i32 display_height = DisplayHeight(_display, screen); + + x11_get_root_(); + + XImage *image = XGetImage(_display, _root_window, 0, 0, display_width, display_height, AllPlanes, ZPixmap); + + if (image == NULL) { + LOG_error("XGetImage failed."); + return 0; + } + + *width = image->width; + *height = image->height; + + if (pixels != NULL && image->width * image->height <= max_num_pixels) + for (i64 j = 0; j < image->height; ++j) { + vec4_f32 *d = pixels + j * image->width; + vec4_f32 *d_first = d; + vec4_f32 *d_end = d + image->width; + + for (; d < d_end; ++d) { + vec3_f32 c = rgb_f32_from_u32_(XGetPixel(image, d - d_first, j)); + + *d = (vec4_f32) { + .x = c.x, + .y = c.y, + .z = c.z, + .w = 1.f, + }; + } + } + + XDestroyImage(image); + return 1; +} + +#endif // defined(__linux__) && ENABLE_X11 + +// ================================================================ + +#if defined(__linux__) && (ENABLE_X11 || ENABLE_WAYLAND) +void take_screenshot(i64 *width, i64 *height, i64 max_num_pixels, vec4_f32 *pixels) { + if (width == NULL || height == NULL) { + LOG_error("Invalid arguments."); + return; + } + + #if ENABLE_X11 + if (x11_screenshot_(width, height, max_num_pixels, pixels)) + return; + #endif + + #if ENABLE_WAYLAND + if (wayland_screenshot_(width, height, max_num_pixels, pixels)) + return; + #endif + + *width = 0; + *height = 0; +} +#endif // defined(__linux__) && (ENABLE_X11 || ENABLE_WAYLAND) + +// ================================================================ +// +// Web startup options +// +// ================================================================ + +#if defined(__wasm__) + +static u16 _key_map[MAX_NUM_KEYS] = {0}; + +__attribute__((export_name("js_max_num_keys"))) i32 js_max_num_keys(void) { + return MAX_NUM_KEYS; +} + +__attribute__((export_name("js_key_map"))) void *js_key_map(void) { + i32 n = 0; + + _key_map[n++] = KEY_BACKSPACE; + _key_map[n++] = KEY_TAB; + _key_map[n++] = KEY_ENTER; + _key_map[n++] = KEY_LCTRL; + _key_map[n++] = KEY_RCTRL; + _key_map[n++] = KEY_LSHIFT; + _key_map[n++] = KEY_RSHIFT; + _key_map[n++] = KEY_LALT; + _key_map[n++] = KEY_RALT; + _key_map[n++] = KEY_LEFT; + _key_map[n++] = KEY_RIGHT; + _key_map[n++] = KEY_UP; + _key_map[n++] = KEY_DOWN; + _key_map[n++] = KEY_PAUSE; + _key_map[n++] = KEY_INSERT; + _key_map[n++] = KEY_HOME; + _key_map[n++] = KEY_END; + _key_map[n++] = KEY_PAGEUP; + _key_map[n++] = KEY_PAGEDOWN; + _key_map[n++] = KEY_ESCAPE; + _key_map[n++] = KEY_PRINTSCREEN; + _key_map[n++] = KEY_SPACE; + _key_map[n++] = KEY_LMETA; + _key_map[n++] = KEY_RMETA; + _key_map[n++] = KEY_QUOTE; + _key_map[n++] = KEY_COMMA; + _key_map[n++] = KEY_MINUS; + _key_map[n++] = KEY_PERIOD; + _key_map[n++] = KEY_SLASH; + _key_map[n++] = KEY_0; + _key_map[n++] = KEY_1; + _key_map[n++] = KEY_2; + _key_map[n++] = KEY_3; + _key_map[n++] = KEY_4; + _key_map[n++] = KEY_5; + _key_map[n++] = KEY_6; + _key_map[n++] = KEY_7; + _key_map[n++] = KEY_8; + _key_map[n++] = KEY_9; + _key_map[n++] = KEY_COLON; + _key_map[n++] = KEY_EQUAL; + _key_map[n++] = KEY_LBRACE; + _key_map[n++] = KEY_BACKSLASH; + _key_map[n++] = KEY_RBRACE; + _key_map[n++] = KEY_TILDE; + _key_map[n++] = KEY_A; + _key_map[n++] = KEY_B; + _key_map[n++] = KEY_C; + _key_map[n++] = KEY_D; + _key_map[n++] = KEY_E; + _key_map[n++] = KEY_F; + _key_map[n++] = KEY_G; + _key_map[n++] = KEY_H; + _key_map[n++] = KEY_I; + _key_map[n++] = KEY_J; + _key_map[n++] = KEY_K; + _key_map[n++] = KEY_L; + _key_map[n++] = KEY_M; + _key_map[n++] = KEY_N; + _key_map[n++] = KEY_O; + _key_map[n++] = KEY_P; + _key_map[n++] = KEY_Q; + _key_map[n++] = KEY_R; + _key_map[n++] = KEY_S; + _key_map[n++] = KEY_T; + _key_map[n++] = KEY_U; + _key_map[n++] = KEY_V; + _key_map[n++] = KEY_W; + _key_map[n++] = KEY_X; + _key_map[n++] = KEY_Y; + _key_map[n++] = KEY_Z; + _key_map[n++] = KEY_DELETE; + _key_map[n++] = KEY_F1; + _key_map[n++] = KEY_F2; + _key_map[n++] = KEY_F3; + _key_map[n++] = KEY_F4; + _key_map[n++] = KEY_F5; + _key_map[n++] = KEY_F6; + _key_map[n++] = KEY_F7; + _key_map[n++] = KEY_F8; + _key_map[n++] = KEY_F9; + _key_map[n++] = KEY_F10; + _key_map[n++] = KEY_F11; + _key_map[n++] = KEY_F12; + _key_map[n++] = KEY_F13; + _key_map[n++] = KEY_F14; + _key_map[n++] = KEY_F15; + _key_map[n++] = KEY_F16; + _key_map[n++] = KEY_F17; + _key_map[n++] = KEY_F18; + _key_map[n++] = KEY_F19; + _key_map[n++] = KEY_F20; + _key_map[n++] = KEY_F21; + _key_map[n++] = KEY_F22; + _key_map[n++] = KEY_F23; + _key_map[n++] = KEY_F24; + + return _key_map; +} + +__attribute__((export_name("js_sound_sample_rate"))) f64 js_sound_sample_rate(void) { + return (f64) PRIMARY_SOUND_SAMPLE_RATE; +} + +__attribute__((export_name("js_num_sound_channels"))) i32 js_num_sound_channels(void) { + return NUM_PRIMARY_SOUND_CHANNELS; +} + +#endif // defined(__wasm__) + +// ================================================================ +// +// Web +// +// ================================================================ + +#if defined(__wasm__) + +static i32 _num_events = 0; +static i32 _input_len = 0; +static i32 _cursor_dx = 0; +static i32 _cursor_dy = 0; +static f64 _wheel_dx = 0; +static f64 _wheel_dy = 0; +static b8 _wait_events = 0; +static i64 _timeout = 0; +static i64 _sound_read = 0; +static b8 _files_dragged = 0; +static b8 _files_dropped = 0; + +static c8 _href [4096] = {0}; +static b8 _key_pressed [MAX_NUM_KEYS] = {0}; +static f32 _sound_buffer [MAX_NUM_PRIMARY_SOUND_FRAMES] = {0}; + +void shutdown_all_systems(void) { + g_platform.done = 1; +} + +b8 network_open(u16 slot, Network_Address address, u16 *local_port) { + LOG_error("Web sockets are not implemented."); + return 0; +} + +i64 network_recv(u16 slot, Network_Address address, i64 size, u8 *data, u16 *local_port, Network_Address *remote_address) { + LOG_error("Web sockets are not implemented."); + return 0; +} + +i64 network_send(u16 slot, Network_Address address, i64 size, u8 *data, u16 *local_port) { + LOG_error("Web sockets are not implemented."); + return 0; +} + +i64 current_utc_time_in_milliseconds(void); + +void current_utc_time_in_sec_and_nsec(i64 *seconds, i64 *nanoseconds) { + i64 time = current_utc_time_in_milliseconds(); + if (seconds != NULL) *seconds = time / 1000ll; + if (nanoseconds != NULL) *nanoseconds = 1000000ll * (time % 1000ll); +} + +void yield_thread_execution(void) { + // Do nothing +} + +void suspend_thread_for_milliseconds(i64 duration) { + if (duration <= 0) + return; + + i64 t = current_utc_time_in_milliseconds(); + + if (_timeout < t) + _timeout = t + duration; + else + _timeout += duration; +} + +void init_main_window(void) { + (void) rgb_f32_from_u32_; + + ++_num_events; + + g_platform.done = 1; + + for (i64 i = 0; i < NUM_FRAMES_AVERAGED; ++i) + _frame_duration[i] = (MIN_FRAME_DURATION + MAX_FRAME_DURATION) / 2; + + _sound_read = (MAX_NUM_PRIMARY_SOUND_FRAMES + ((-PRIMARY_SOUND_AVAIL_MIN) % MAX_NUM_PRIMARY_SOUND_FRAMES)) % MAX_NUM_PRIMARY_SOUND_FRAMES; +} + +i32 handle_main_window_events(void) { + if (g_platform.files_dropped) { + // NOTE: We won't be able to drop files at each + // of 2 sequential frames. + + drop_files_clean_(); + + _files_dragged = 0; + _files_dropped = 0; + } + + g_platform.input_len = _input_len; + g_platform.cursor_dx = _cursor_dx; + g_platform.cursor_dy = _cursor_dy; + g_platform.wheel_dx = _wheel_dx; + g_platform.wheel_dy = _wheel_dy; + g_platform.files_dragged = _files_dragged; + g_platform.files_dropped = _files_dropped; + + _input_len = 0; + _cursor_dx = 0; + _cursor_dy = 0; + _wheel_dx = 0; + _wheel_dy = 0; + + if (_files_dropped) { + _files_dragged = 0; + _files_dropped = 0; + } + + for (i64 i = 0; i < MAX_NUM_KEYS; ++i) + g_platform.key_pressed[i] = _key_pressed[i]; + for (i64 i = 0; i < MAX_NUM_KEYS; ++i) + _key_pressed[i] = 0; + + i32 n = _num_events; + _num_events = 0; + return n; +} + +i32 wait_main_window_events(void) { + _wait_events = 1; + + return handle_main_window_events(); +} + +void render_main_window_frame(void) { + convert_pixels_for_window_(); +} + +void write_clipboard_text_impl(i32 size, c8 *data); + +void write_clipboard_text(i64 size, c8 *data) { + if (size < 0 || data == NULL) + return; + + store_clipboard_text_(size, data); + + if (g_platform.clipboard_text_len > 0) + write_clipboard_text_impl((i32) g_platform.clipboard_text_len - 1, data); +} + +void take_screenshot(i64 max_num_pixels, i64 *width, i64 *height, vec4_f32 *pixels) { + if (width == NULL || height == NULL) { + LOG_error("Invalid arguments."); + return; + } + + *width = 0; + *height = 0; +} + +void handle_primary_sound(void) { + g_platform.num_sound_samples_elapsed = sound_samples_elapsed_(); + _sound_position = (_sound_position + g_platform.num_sound_samples_elapsed * NUM_PRIMARY_SOUND_CHANNELS) % MAX_NUM_PRIMARY_SOUND_FRAMES; +} + +void queue_primary_sound(i64 delay_in_samples, i64 num_samples, f32 *frames) { + if (frames == NULL) + return; + + if (delay_in_samples < 0) { + frames += -delay_in_samples * NUM_PRIMARY_SOUND_CHANNELS; + num_samples -= delay_in_samples; + delay_in_samples = 0; + } + + if (num_samples <= 0) + return; + + i64 num_frames = num_samples * NUM_PRIMARY_SOUND_CHANNELS; + if (num_frames > MAX_NUM_PRIMARY_SOUND_FRAMES) { + LOG_error("Sound buffer overflow."); + return; + } + + i64 begin = (_sound_position + delay_in_samples * NUM_PRIMARY_SOUND_CHANNELS) % MAX_NUM_PRIMARY_SOUND_FRAMES; + + if (num_frames <= MAX_NUM_PRIMARY_SOUND_FRAMES - begin) + for (i64 i = 0; i < num_frames; ++i) + _sound_ring[begin + i] += frames[i]; + else { + i64 part_one = MAX_NUM_PRIMARY_SOUND_FRAMES - begin; + i64 part_two = num_frames - part_one; + + for (i64 i = 0; i < part_one; ++i) + _sound_ring[begin + i] += frames[i]; + for (i64 i = 0; i < part_two; ++i) + _sound_ring[i] += frames[part_one + i]; + } +} + +__attribute__((export_name("js_max_num_sound_frames"))) i32 js_max_num_sound_frames(void) { + return MAX_NUM_PRIMARY_SOUND_FRAMES; +} + +__attribute__((export_name("js_sound_buffer"))) void *js_sound_buffer(void) { + return _sound_buffer; +} + +__attribute__((export_name("js_href"))) void *js_href(void) { + return _href; +} + +__attribute__((export_name("js_href_size"))) i32 js_href_size(void) { + return (sizeof _href) - 1; +} + +__attribute__((export_name("js_main"))) void js_main(void) { + main(1, (c8 *[1]) { _href }); +} + +__attribute__((export_name("js_title"))) void *js_title(void) { + return g_platform.title; +} + +__attribute__((export_name("js_frame"))) void *js_frame(i32 frame_width, i32 frame_height, i32 num_samples) { + i64 frame_time = current_utc_time_in_milliseconds(); + + _num_events += pixel_size_update_(frame_width, frame_height); + + g_platform.done = 0; + + b8 do_render = (frame_time >= _timeout) && (_num_events > 0 || !_wait_events); + + if (do_render) { + _wait_events = 0; + update_and_render_frame(); + } + + // Convert interleaved frames to non-interleaved. + for (i64 j = 0; j < NUM_PRIMARY_SOUND_CHANNELS; ++j) + for (i64 i = 0; i < num_samples; ++i) { + i64 n = (_sound_read + i * NUM_PRIMARY_SOUND_CHANNELS + j) % MAX_NUM_PRIMARY_SOUND_FRAMES; + _sound_buffer[j * num_samples + i] = _sound_ring[n]; + _sound_ring[n] = 0.f; + } + + _sound_read = (_sound_read + num_samples * NUM_PRIMARY_SOUND_CHANNELS) % MAX_NUM_PRIMARY_SOUND_FRAMES; + + if (do_render) + pixel_size_calibrate_(current_utc_time_in_milliseconds() - frame_time); + + return _internal_pixels; +} + +__attribute__((export_name("js_mousemove"))) void js_mousemove(i32 x, i32 y) { + ++_num_events; + _cursor_dx += x - g_platform.cursor_x; + _cursor_dy += y - g_platform.cursor_y; + g_platform.cursor_x = x; + g_platform.cursor_y = y; +} + +__attribute__((export_name("js_mousedown"))) void js_mousedown(u32 buttons) { + ++_num_events; + if ((buttons & 1) && !g_platform.key_down[BUTTON_LEFT]) _key_pressed[BUTTON_LEFT] = 1; + if ((buttons & 2) && !g_platform.key_down[BUTTON_RIGHT]) _key_pressed[BUTTON_RIGHT] = 1; + if ((buttons & 4) && !g_platform.key_down[BUTTON_MIDDLE]) _key_pressed[BUTTON_MIDDLE] = 1; + if (buttons & 1) g_platform.key_down[BUTTON_LEFT] = 1; + if (buttons & 2) g_platform.key_down[BUTTON_RIGHT] = 1; + if (buttons & 4) g_platform.key_down[BUTTON_MIDDLE] = 1; +} + +__attribute__((export_name("js_mouseup"))) void js_mouseup(u32 buttons) { + ++_num_events; + if (!(buttons & 1)) g_platform.key_down[BUTTON_LEFT] = 0; + if (!(buttons & 2)) g_platform.key_down[BUTTON_RIGHT] = 0; + if (!(buttons & 4)) g_platform.key_down[BUTTON_MIDDLE] = 0; +} + +__attribute__((export_name("js_keydown"))) void js_keydown(u32 key, u32 mod, u32 ch) { + ++_num_events; + _key_pressed[key] = 1; + g_platform.key_down[key] = 1; + g_platform.key_down[MOD_CTRL] = (mod & 1) ? 1 : 0; + g_platform.key_down[MOD_SHIFT] = (mod & 2) ? 1 : 0; + g_platform.key_down[MOD_ALT] = (mod & 4) ? 1 : 0; + g_platform.key_down[MOD_META] = (mod & 8) ? 1 : 0; + + i64 n = g_platform.input_len; + resize_dynamic_array_capacity(&g_platform.input_len, &g_platform.input_capacity, (void **) &g_platform.input, sizeof *g_platform.input, n + 1); + + if (n < g_platform.input_len) + g_platform.input[n] = (Input_Key) { + .ctrl = (mod & 1) ? 1 : 0, + .shift = (mod & 2) ? 1 : 0, + .alt = (mod & 4) ? 1 : 0, + .meta = (mod & 8) ? 1 : 0, + .key = key, + .c = ch, + }; +} + +__attribute__((export_name("js_keyup"))) void js_keyup(u32 key, u32 mod) { + ++_num_events; + g_platform.key_down[key] = 0; + g_platform.key_down[MOD_CTRL] = (mod & 1) ? 1 : 0; + g_platform.key_down[MOD_SHIFT] = (mod & 2) ? 1 : 0; + g_platform.key_down[MOD_ALT] = (mod & 4) ? 1 : 0; + g_platform.key_down[MOD_META] = (mod & 8) ? 1 : 0; +} + +__attribute__((export_name("js_wheel"))) void js_wheel(f64 delta_x, f64 delta_y) { + ++_num_events; + _wheel_dx += delta_x * MOUSE_WHEEL_FACTOR; + _wheel_dy += delta_y * MOUSE_WHEEL_FACTOR; +} + +__attribute__((export_name("js_clipboard_text_len"))) i32 js_clipboard_text_len(void) { + return g_platform.clipboard_text_len; +} + +__attribute__((export_name("js_clipboard_text"))) void *js_clipboard_text(i32 len) { + if (!g_platform.enable_clipboard_text && len < 0) + len = 0; + resize_dynamic_array_exact(&g_platform.clipboard_text_len, (void **) &g_platform.clipboard_text, 1, len); + return g_platform.clipboard_text; +} + +__attribute__((export_name("js_dragenter"))) void js_dragenter(void) { + _files_dragged = 1; +} + +__attribute__((export_name("js_dragleave"))) void js_dragleave(void) { + _files_dragged = 0; +} + +__attribute__((export_name("js_drop"))) i32 js_drop(i32 name_len, i32 data_size) { + i64 n = g_platform.num_drop_files; + + drop_files_set_num_(n + 1); + + if (n >= g_platform.num_drop_files) + return 0; + + drop_files_set_name_(n, name_len, NULL); + drop_files_set_data_(n, data_size); + + Drop_File *f = g_platform.drop_files + n; + + if (f->name_len != name_len || f->data_size != data_size) { + drop_files_set_name_(n, 0, NULL); + drop_files_set_data_(n, 0); + drop_files_set_num_(n); + LOG_error("File too big."); + return 0; + } + + mem_set_(f->name, 0, name_len); + + _files_dropped = 1; + return 1; +} + +__attribute__((export_name("js_drop_name"))) void *js_drop_name(void) { + if (g_platform.num_drop_files <= 0) + return NULL; + return g_platform.drop_files[g_platform.num_drop_files - 1].name; +} + +__attribute__((export_name("js_drop_data"))) void *js_drop_data(void) { + if (g_platform.num_drop_files <= 0) + return NULL; + return g_platform.drop_files[g_platform.num_drop_files - 1].data; +} + +#endif // defined(__wasm__) + +// ================================================================ +// +// No system +// +// ================================================================ + +#if !defined(__linux__) || (!ENABLE_X11 && !ENABLE_WAYLAND) +void init_main_window(void) { + LOG_error("Not implemented."); +} + +void update_and_render_frame(void) { + LOG_error("Not implemented."); +} + +void shutdown_all_systems(void) { + // TODO: Factor out common code. + + PROFILER_report_(); + + g_platform.done = 1; +} +#endif + +#if !defined(__linux__) || !ENABLE_ALSA +#endif + +// ================================================================ +// +// Test suite +// +// ================================================================ + +#ifdef RUNTIME_TEST_SUITE + +#define TEST_FILE runtime +#include "test.c" + +TEST("types sanity basic") { + REQUIRE_EQ(sizeof(i8), 1); + REQUIRE_EQ(sizeof(i16), 2); + REQUIRE_EQ(sizeof(i32), 4); + REQUIRE_EQ(sizeof(i64), 8); + REQUIRE_EQ(sizeof(u8), 1); + REQUIRE_EQ(sizeof(u16), 2); + REQUIRE_EQ(sizeof(u32), 4); + REQUIRE_EQ(sizeof(u64), 8); + REQUIRE_EQ(sizeof(c8), 1); + REQUIRE_EQ(sizeof(c32), 4); + REQUIRE_EQ(sizeof(b8), 1); + REQUIRE_EQ(sizeof(f32), 4); + REQUIRE_EQ(sizeof(f64), 8); +} + +TEST("types sanity vector") { + vec2 a = { 1.0, 2.0, }; + vec3 b = { 1.0, 2.0, 3.0, }; + vec4 c = { 1.0, 2.0, 3.0, 4.0, }; + + vec2_f32 d = { 1.0f, 2.0f, }; + vec3_f32 e = { 1.0f, 2.0f, 3.0f, }; + vec4_f32 f = { 1.0f, 2.0f, 3.0f, 4.0f, }; + + vec2_i64 g = { 1, 2, }; + vec3_i64 h = { 1, 2, 3, }; + vec4_i64 i = { 1, 2, 3, 4, }; + + REQUIRE(a.x == 1.0); + REQUIRE(a.y == 2.0); + REQUIRE(b.x == 1.0); + REQUIRE(b.y == 2.0); + REQUIRE(b.z == 3.0); + REQUIRE(c.x == 1.0); + REQUIRE(c.y == 2.0); + REQUIRE(c.z == 3.0); + REQUIRE(c.w == 4.0); + + REQUIRE(d.x == 1.0f); + REQUIRE(d.y == 2.0f); + REQUIRE(e.x == 1.0f); + REQUIRE(e.y == 2.0f); + REQUIRE(e.z == 3.0f); + REQUIRE(f.x == 1.0f); + REQUIRE(f.y == 2.0f); + REQUIRE(f.z == 3.0f); + REQUIRE(f.w == 4.0f); + + REQUIRE_EQ(g.x, 1); + REQUIRE_EQ(g.y, 2); + REQUIRE_EQ(h.x, 1); + REQUIRE_EQ(h.y, 2); + REQUIRE_EQ(h.z, 3); + REQUIRE_EQ(i.x, 1); + REQUIRE_EQ(i.y, 2); + REQUIRE_EQ(i.z, 3); + REQUIRE_EQ(i.w, 4); +} + +TEST("types sanity matrix") { + mat2 j = { 1.0, 2.0, 3.0, 4.0, }; + mat3 k = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, }; + mat4 l = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, }; + + mat2_f32 m = { 1.0, 2.0, 3.0, 4.0, }; + mat3_f32 n = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, }; + mat4_f32 o = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, }; + + REQUIRE(j.m[0][0] == 1.0); + REQUIRE(j.m[0][1] == 2.0); + REQUIRE(j.m[1][0] == 3.0); + REQUIRE(j.m[1][1] == 4.0); + + REQUIRE(k.m[0][0] == 1.0); + REQUIRE(k.m[0][1] == 2.0); + REQUIRE(k.m[0][2] == 3.0); + REQUIRE(k.m[1][0] == 4.0); + REQUIRE(k.m[1][1] == 5.0); + REQUIRE(k.m[1][2] == 6.0); + REQUIRE(k.m[2][0] == 7.0); + REQUIRE(k.m[2][1] == 8.0); + REQUIRE(k.m[2][2] == 9.0); + + REQUIRE(l.m[0][0] == 1.0); + REQUIRE(l.m[0][1] == 2.0); + REQUIRE(l.m[0][2] == 3.0); + REQUIRE(l.m[0][3] == 4.0); + REQUIRE(l.m[1][0] == 5.0); + REQUIRE(l.m[1][1] == 6.0); + REQUIRE(l.m[1][2] == 7.0); + REQUIRE(l.m[1][3] == 8.0); + REQUIRE(l.m[2][0] == 9.0); + REQUIRE(l.m[2][1] == 10.0); + REQUIRE(l.m[2][2] == 11.0); + REQUIRE(l.m[2][3] == 12.0); + REQUIRE(l.m[3][0] == 13.0); + REQUIRE(l.m[3][1] == 14.0); + REQUIRE(l.m[3][2] == 15.0); + REQUIRE(l.m[3][3] == 16.0); + + REQUIRE(m.m[0][0] == 1.0f); + REQUIRE(m.m[0][1] == 2.0f); + REQUIRE(m.m[1][0] == 3.0f); + REQUIRE(m.m[1][1] == 4.0f); + + REQUIRE(n.m[0][0] == 1.0f); + REQUIRE(n.m[0][1] == 2.0f); + REQUIRE(n.m[0][2] == 3.0f); + REQUIRE(n.m[1][0] == 4.0f); + REQUIRE(n.m[1][1] == 5.0f); + REQUIRE(n.m[1][2] == 6.0f); + REQUIRE(n.m[2][0] == 7.0f); + REQUIRE(n.m[2][1] == 8.0f); + REQUIRE(n.m[2][2] == 9.0f); + + REQUIRE(o.m[0][0] == 1.0f); + REQUIRE(o.m[0][1] == 2.0f); + REQUIRE(o.m[0][2] == 3.0f); + REQUIRE(o.m[0][3] == 4.0f); + REQUIRE(o.m[1][0] == 5.0f); + REQUIRE(o.m[1][1] == 6.0f); + REQUIRE(o.m[1][2] == 7.0f); + REQUIRE(o.m[1][3] == 8.0f); + REQUIRE(o.m[2][0] == 9.0f); + REQUIRE(o.m[2][1] == 10.0f); + REQUIRE(o.m[2][2] == 11.0f); + REQUIRE(o.m[2][3] == 12.0f); + REQUIRE(o.m[3][0] == 13.0f); + REQUIRE(o.m[3][1] == 14.0f); + REQUIRE(o.m[3][2] == 15.0f); + REQUIRE(o.m[3][3] == 16.0f); +} + +TEST("math sanity") { + REQUIRE(floor(1.99) == 1.0); + REQUIRE(floor(-1.01) == -2.0); + REQUIRE(ceil(1.01) == 2.0); + REQUIRE(ceil(-1.99) == -1.0); + REQUIRE(trunc(1.99) == 1.0); + REQUIRE(trunc(-1.99) == -1.0); + REQUIRE(sqrt(4.0) > 2.0 - 1e-13); + REQUIRE(sqrt(4.0) < 2.0 + 1e-13); + REQUIRE(cbrt(8.0) > 2.0 - 1e-13); + REQUIRE(cbrt(8.0) < 2.0 + 1e-13); + REQUIRE(pow(2.0, 3.0) > 8.0 - 1e-13); + REQUIRE(pow(2.0, 3.0) < 8.0 + 1e-13); + REQUIRE(log(M_E * M_E) > 2.0 - 1e-13); + REQUIRE(log(M_E * M_E) < 2.0 + 1e-13); + REQUIRE(log2(8.0) > 3.0 - 1e-13); + REQUIRE(log2(8.0) < 3.0 + 1e-13); + REQUIRE(log10(1000.0) > 3.0 - 1e-13); + REQUIRE(log10(1000.0) < 3.0 + 1e-13); + REQUIRE(exp(2.0) > M_E * M_E - 1e-13); + REQUIRE(exp(2.0) < M_E * M_E + 1e-13); + REQUIRE(sin(M_PI / 6.0) > 0.5 - 1e-13); + REQUIRE(sin(M_PI / 6.0) < 0.5 + 1e-13); + REQUIRE(cos(M_PI / 3.0) > 0.5 - 1e-13); + REQUIRE(cos(M_PI / 3.0) < 0.5 + 1e-13); + REQUIRE(tan(M_PI / 4.0) > 1.0 - 1e-13); + REQUIRE(tan(M_PI / 4.0) < 1.0 + 1e-13); + REQUIRE(asin(0.5) > M_PI / 6.0 - 1e-13); + REQUIRE(asin(0.5) < M_PI / 6.0 + 1e-13); + REQUIRE(acos(0.5) > M_PI / 3.0 - 1e-13); + REQUIRE(acos(0.5) < M_PI / 3.0 + 1e-13); + REQUIRE(atan(1.0) > M_PI / 4.0 - 1e-13); + REQUIRE(atan(1.0) < M_PI / 4.0 + 1e-13); + REQUIRE(atan2(1.0, 1.0) > M_PI / 4 - 1e-13); + REQUIRE(atan2(1.0, 1.0) < M_PI / 4 + 1e-13); + REQUIRE(atan2(1.0, -1.0) > M_PI * 3.0 / 4 - 1e-13); + REQUIRE(atan2(1.0, -1.0) < M_PI * 3.0 / 4 + 1e-13); + REQUIRE(atan2(-1.0, 1.0) > -M_PI / 4 - 1e-13); + REQUIRE(atan2(-1.0, 1.0) < -M_PI / 4 + 1e-13); + REQUIRE(atan2(-1.0, -1.0) > -M_PI * 3.0 / 4 - 1e-13); + REQUIRE(atan2(-1.0, -1.0) < -M_PI * 3.0 / 4 + 1e-13); + REQUIRE(log(2.0) > M_LOG_2 - 1e-13); + REQUIRE(log(2.0) < M_LOG_2 + 1e-13); + REQUIRE(log(10.0) > M_LOG_10 - 1e-13); + REQUIRE(log(10.0) < M_LOG_10 + 1e-13); + REQUIRE(log2(M_E) > M_LOG2_E - 1e-13); + REQUIRE(log2(M_E) < M_LOG2_E + 1e-13); + REQUIRE(log10(M_E) > M_LOG10_E - 1e-13); + REQUIRE(log10(M_E) < M_LOG10_E + 1e-13); + REQUIRE(log(M_PI) > M_LOG_PI - 1e-13); + REQUIRE(log(M_PI) < M_LOG_PI + 1e-13); + REQUIRE(sqrt(2.0) > M_SQRT_2 - 1e-13); + REQUIRE(sqrt(2.0) < M_SQRT_2 + 1e-13); + REQUIRE(sqrt(3.0) > M_SQRT_3 - 1e-13); + REQUIRE(sqrt(3.0) < M_SQRT_3 + 1e-13); + REQUIRE(sqrt(M_PI) > M_SQRT_PI - 1e-13); + REQUIRE(sqrt(M_PI) < M_SQRT_PI + 1e-13); + REQUIRE(sqrt(M_E) > M_SQRT_E - 1e-13); + REQUIRE(sqrt(M_E) < M_SQRT_E + 1e-13); + REQUIRE(cbrt(2.0) > M_CBRT_2 - 1e-13); + REQUIRE(cbrt(2.0) < M_CBRT_2 + 1e-13); + REQUIRE(cbrt(3.0) > M_CBRT_3 - 1e-13); + REQUIRE(cbrt(3.0) < M_CBRT_3 + 1e-13); + REQUIRE(cbrt(M_PI) > M_CBRT_PI - 1e-13); + REQUIRE(cbrt(M_PI) < M_CBRT_PI + 1e-13); + REQUIRE(cbrt(M_E) > M_CBRT_E - 1e-13); + REQUIRE(cbrt(M_E) < M_CBRT_E + 1e-13); +} + +TEST("utf8") { + REQUIRE_EQ(utf8_size(1), 1); + REQUIRE_EQ(utf8_size(127), 1); + REQUIRE_EQ(utf8_size(128), 2); + REQUIRE_EQ(utf8_size((1 << 11) - 1), 2); + REQUIRE_EQ(utf8_size((1 << 11)), 3); + REQUIRE_EQ(utf8_size((1 << 16) - 1), 3); + REQUIRE_EQ(utf8_size((1 << 16)), 4); +} + +TEST("time sanity") { + i64 t = current_utc_time_in_milliseconds(); + + REQUIRE(t > 0); + + suspend_thread_for_milliseconds(10); + t = current_utc_time_in_milliseconds() - t; + + REQUIRE(t >= 10); + REQUIRE(t <= 20); + + i64 sec0, nsec0; + current_utc_time_in_sec_and_nsec(&sec0, &nsec0); + + REQUIRE(sec0 > 0); + REQUIRE(nsec0 >= 0); + REQUIRE(nsec0 < 1000000000); + + suspend_thread_for_milliseconds(10); + + i64 sec1, nsec1; + current_utc_time_in_sec_and_nsec(&sec1, &nsec1); + + i64 delta_nsec = 1000000000 * (sec1 - sec0) + (nsec1 - nsec0); + + REQUIRE(delta_nsec >= 10000000); + REQUIRE(delta_nsec <= 20000000); +} + +TEST("blake2b") { + u8 out[4]; + blake2b(out, (u8[4]) { 1, 2, 3, 4, }, NULL, 4, 4, 0); + + REQUIRE_EQ(out[0], 26); + REQUIRE_EQ(out[1], 185); + REQUIRE_EQ(out[2], 56); + REQUIRE_EQ(out[3], 84); + + blake2b(out, (u8[4]) { 1, 2, 3, 4, }, (u8[4]) { 5, 6, 7, 8, }, 4, 4, 4); + + REQUIRE_EQ(out[0], 74); + REQUIRE_EQ(out[1], 233); + REQUIRE_EQ(out[2], 165); + REQUIRE_EQ(out[3], 192); + + // TODO: Add more tests with longer inputs. +} + +TEST("memory allocator") { + void *p = memory_buffer_allocate(16, 4, 0, NULL); + REQUIRE(p != NULL); + + p = memory_buffer_allocate(64, 4, 16, p); + REQUIRE(p != NULL); + + void *q = p; + p = memory_buffer_allocate(8, 4, 64, p); + REQUIRE(p == q); + + p = memory_buffer_allocate(0, 4, 8, p); + REQUIRE(p == NULL); + + // TODO: Add more tests. +} + +TEST("average frame duration") { + for (i64 i = 0; i < 50; ++i) + average_frame_duration_(100); + REQUIRE_EQ(average_frame_duration_(100), 100); +} + +#ifndef EVERY_TEST_SUITE +void update_and_render_frame(void) {} + +i32 main(i32 argc, c8 **argv) { + return run_tests(argc, argv); +} +#endif + +#undef TEST_FILE +#endif // RUNTIME_TEST_SUITE + +// ================================================================ + +#endif // RUNTIME_IMPL_GUARD_ +#endif // RUNTIME_HEADER -- cgit v1.2.3