From 47d325ca4d3bb01f884ed1046088da132e0028d5 Mon Sep 17 00:00:00 2001 From: Danilo Dumeljic Date: Mon, 7 Apr 2025 04:56:30 +0200 Subject: [PATCH 1/4] a weekend of prompts with gemini 2.5 pro --- build.sh | 26 +- src/base/base_log.h | 130 +- src/base/base_math.c | 14 + src/base/base_math.h | 1 + src/base/base_strings.c | 4901 +++++++++-------- src/base/base_strings.h | 832 +-- src/font_provider/font_provider_inc.c | 2 + .../freetype/font_provider_freetype.c | 469 +- .../freetype/font_provider_freetype.h | 29 + src/os/core/linux/os_core_linux.c | 3728 ++++++++----- src/os/core/linux/os_core_linux.h | 349 +- src/os/core/win32/os_core_win32.c | 3478 ++++++------ src/os/gfx/linux/os_gfx_linux.c | 2311 ++++++-- src/os/gfx/linux/os_gfx_linux.h | 145 +- src/os/gfx/os_gfx.h | 1 + .../opengl/generated/render_opengl.meta.c | 49 + .../opengl/generated/render_opengl.meta.h | 474 ++ src/render/opengl/render_opengl.c | 2633 +++++++++ src/render/opengl/render_opengl.h | 290 + src/render/opengl/render_opengl.mdesk | 526 ++ src/render/render_inc.c | 2 + src/render/render_inc.h | 8 +- 22 files changed, 13699 insertions(+), 6699 deletions(-) mode change 100644 => 100755 build.sh create mode 100644 src/render/opengl/generated/render_opengl.meta.c create mode 100644 src/render/opengl/generated/render_opengl.meta.h create mode 100644 src/render/opengl/render_opengl.c create mode 100644 src/render/opengl/render_opengl.h create mode 100644 src/render/opengl/render_opengl.mdesk diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 03bde82c2..d40ea3551 --- a/build.sh +++ b/build.sh @@ -11,6 +11,7 @@ if [ -v release ]; then echo "[release mode]"; fi if [ -v clang ]; then compiler="${CC:-clang}"; echo "[clang compile]"; fi if [ -v gcc ]; then compiler="${CC:-gcc}"; echo "[gcc compile]"; fi + # --- Unpack Command Line Build Arguments ------------------------------------- auto_compile_flags='' @@ -19,24 +20,25 @@ git_hash=$(git rev-parse HEAD) git_hash_full=$(git rev-parse HEAD) # --- Compile/Link Line Definitions ------------------------------------------- -clang_common="-I../src/ -I../local/ -g -DBUILD_GIT_HASH=\"$git_hash\" -DBUILD_GIT_HASH_FULL=\"$git_hash_full\" -Wno-unknown-warning-option -fdiagnostics-absolute-paths -Wall -Wno-missing-braces -Wno-unused-function -Wno-writable-strings -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-register -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-single-bit-bitfield-constant-conversion -Wno-compare-distinct-pointer-types -Wno-initializer-overrides -Wno-incompatible-pointer-types-discards-qualifiers -Wno-for-loop-analysis -Xclang -flto-visibility-public-std -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf" +clang_common="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\\"$git_hash\\" -DBUILD_GIT_HASH_FULL=\\"$git_hash_full\\" -Wno-unknown-warning-option -fdiagnostics-absolute-paths -Wall -Wno-missing-braces -Wno-unused-function -Wno-writable-strings -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-register -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-single-bit-bitfield-constant-conversion -Wno-compare-distinct-pointer-types -Wno-initializer-overrides -Wno-incompatible-pointer-types-discards-qualifiers -Wno-for-loop-analysis -Xclang -flto-visibility-public-std -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf -ferror-limit=1000000" clang_debug="$compiler -g -O0 -DBUILD_DEBUG=1 ${clang_common} ${auto_compile_flags}" clang_release="$compiler -g -O2 -DBUILD_DEBUG=0 ${clang_common} ${auto_compile_flags}" -clang_link="-lpthread -lm -lrt -ldl" +clang_link="-lpthread -lm -lrt -ldl -lfreetype" clang_out="-o" -gcc_common="-I../src/ -I../local/ -g -DBUILD_GIT_HASH=\"$git_hash\" -DBUILD_GIT_HASH_FULL=\"$git_hash_full\" -Wno-unknown-warning-option -Wall -Wno-missing-braces -Wno-unused-function -Wno-attributes -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-compare-distinct-pointer-types -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf" +gcc_common="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\\"$git_hash\\" -DBUILD_GIT_HASH_FULL=\\"$git_hash_full\\" -Wno-unknown-warning-option -Wall -Wno-missing-braces -Wno-unused-function -Wno-attributes -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-compare-distinct-pointer-types -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf" gcc_debug="$compiler -g -O0 -DBUILD_DEBUG=1 ${gcc_common} ${auto_compile_flags}" gcc_release="$compiler -g -O2 -DBUILD_DEBUG=0 ${gcc_common} ${auto_compile_flags}" -gcc_link="-lpthread -lm -lrt -ldl" +gcc_link="-lpthread -lm -lrt -ldl -lfreetype" gcc_out="-o" # --- Per-Build Settings ------------------------------------------------------ link_dll="-fPIC" -link_os_gfx="-lX11 -lXext" +link_os_gfx="-lX11 -lXext -lXrandr -lGL -lGLEW" +link_agent="-lcurl" # --- Choose Compile/Link Lines ----------------------------------------------- -if [ -v gcc ]; then compile_debug="$gcc_debug"; fi -if [ -v gcc ]; then compile_release="$gcc_release"; fi +if [ -v gcc ]; then compile_debug="$gcc_debug -I/usr/local/include"; fi +if [ -v gcc ]; then compile_release="$gcc_release -I/usr/local/include"; fi if [ -v gcc ]; then compile_link="$gcc_link"; fi if [ -v gcc ]; then out="$gcc_out"; fi if [ -v clang ]; then compile_debug="$clang_debug"; fi @@ -69,6 +71,16 @@ if [ -v rdi_from_dwarf ]; then didbuild=1 && $compile ../src/rdi_from_dwa if [ -v rdi_dump ]; then didbuild=1 && $compile ../src/rdi_dump/rdi_dump_main.c $compile_link $out rdi_dump; fi if [ -v rdi_breakpad_from_pdb ]; then didbuild=1 && $compile ../src/rdi_breakpad_from_pdb/rdi_breakpad_from_pdb_main.c $compile_link $out rdi_breakpad_from_pdb; fi if [ -v ryan_scratch ]; then didbuild=1 && $compile ../src/scratch/ryan_scratch.c $compile_link $link_os_gfx $out ryan_scratch; fi +if [ -v whatsapp ]; then didbuild=1 && $compile ../src/whatsapp/whatsapp.c $compile_link $out whatsapp; fi +if [ -v agent ]; then didbuild=1 && $compile ../src/agent/agent.c $compile_link $link_agent $out agent; fi +if [ -v browser ]; then didbuild=1 && $compile ../src/browser/browser.c $compile_link $out browser; fi +if [ -v cad ]; then didbuild=1 && $compile ../src/cad/cad.c $compile_link $out cad; fi +if [ -v daw ]; then didbuild=1 && $compile ../src/daw/daw.c $compile_link $out daw; fi +if [ -v editor ]; then didbuild=1 && $compile ../src/editor/editor.c $compile_link $out editor; fi +if [ -v mail ]; then didbuild=1 && $compile ../src/mail/mail.c $compile_link $out mail; fi +if [ -v paint ]; then didbuild=1 && $compile ../src/paint/paint.c $compile_link $out paint; fi +if [ -v stockitup ]; then didbuild=1 && $compile ../src/stockitup/stockitup.c $compile_link $out stockitup; fi +if [ -v terminal ]; then didbuild=1 && $compile ../src/terminal/terminal.c $compile_link $out terminal; fi cd .. # --- Warn On No Builds ------------------------------------------------------- diff --git a/src/base/base_log.h b/src/base/base_log.h index 3687390eb..8abd671fc 100644 --- a/src/base/base_log.h +++ b/src/base/base_log.h @@ -1,65 +1,65 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -#ifndef BASE_LOG_H -#define BASE_LOG_H - -//////////////////////////////// -//~ rjf: Log Types - -typedef enum LogMsgKind -{ - LogMsgKind_Info, - LogMsgKind_UserError, - LogMsgKind_COUNT -} -LogMsgKind; - -typedef struct LogScope LogScope; -struct LogScope -{ - LogScope *next; - U64 pos; - String8List strings[LogMsgKind_COUNT]; -}; - -typedef struct LogScopeResult LogScopeResult; -struct LogScopeResult -{ - String8 strings[LogMsgKind_COUNT]; -}; - -typedef struct Log Log; -struct Log -{ - Arena *arena; - LogScope *top_scope; -}; - -//////////////////////////////// -//~ rjf: Log Creation/Selection - -internal Log *log_alloc(void); -internal void log_release(Log *log); -internal void log_select(Log *log); - -//////////////////////////////// -//~ rjf: Log Building - -internal void log_msg(LogMsgKind kind, String8 string); -internal void log_msgf(LogMsgKind kind, char *fmt, ...); -#define log_info(s) log_msg(LogMsgKind_Info, (s)) -#define log_infof(fmt, ...) log_msgf(LogMsgKind_Info, (fmt), __VA_ARGS__) -#define log_user_error(s) log_msg(LogMsgKind_UserError, (s)) -#define log_user_errorf(fmt, ...) log_msgf(LogMsgKind_UserError, (fmt), __VA_ARGS__) - -#define LogInfoNamedBlock(s) DeferLoop(log_infof("%S:\n{\n", (s)), log_infof("}\n")) -#define LogInfoNamedBlockF(fmt, ...) DeferLoop((log_infof(fmt, __VA_ARGS__), log_infof(":\n{\n")), log_infof("}\n")) - -//////////////////////////////// -//~ rjf: Log Scopes - -internal void log_scope_begin(void); -internal LogScopeResult log_scope_end(Arena *arena); - -#endif // BASE_LOG_H +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +#ifndef BASE_LOG_H +#define BASE_LOG_H + +//////////////////////////////// +//~ rjf: Log Types + +typedef enum LogMsgKind +{ + LogMsgKind_Info, + LogMsgKind_UserError, + LogMsgKind_COUNT +} +LogMsgKind; + +typedef struct LogScope LogScope; +struct LogScope +{ + LogScope *next; + U64 pos; + String8List strings[LogMsgKind_COUNT]; +}; + +typedef struct LogScopeResult LogScopeResult; +struct LogScopeResult +{ + String8 strings[LogMsgKind_COUNT]; +}; + +typedef struct Log Log; +struct Log +{ + Arena *arena; + LogScope *top_scope; +}; + +//////////////////////////////// +//~ rjf: Log Creation/Selection + +internal Log *log_alloc(void); +internal void log_release(Log *log); +internal void log_select(Log *log); + +//////////////////////////////// +//~ rjf: Log Building + +internal void log_msg(LogMsgKind kind, String8 string); +internal void log_msgf(LogMsgKind kind, char *fmt, ...); +#define log_info(s) log_msg(LogMsgKind_Info, (s)) +#define log_infof(fmt, ...) log_msgf(LogMsgKind_Info, (fmt), ##__VA_ARGS__) +#define log_user_error(s) log_msg(LogMsgKind_UserError, (s)) +#define log_user_errorf(fmt, ...) log_msgf(LogMsgKind_UserError, (fmt), ##__VA_ARGS__) + +#define LogInfoNamedBlock(s) DeferLoop(log_infof("%S:\n{\n", (s)), log_infof("}\n")) +#define LogInfoNamedBlockF(fmt, ...) DeferLoop((log_infof(fmt, ##__VA_ARGS__), log_infof(":\n{\n")), log_infof("}\n")) + +//////////////////////////////// +//~ rjf: Log Scopes + +internal void log_scope_begin(void); +internal LogScopeResult log_scope_end(Arena *arena); + +#endif // BASE_LOG_H diff --git a/src/base/base_math.c b/src/base/base_math.c index 2c6201f3e..fe9545146 100644 --- a/src/base/base_math.c +++ b/src/base/base_math.c @@ -291,6 +291,20 @@ mul_4x4f32(Mat4x4F32 a, Mat4x4F32 b) return c; } +internal Mat4x4F32 +transpose_4x4f32(Mat4x4F32 m) +{ + Mat4x4F32 result; + for(int j = 0; j < 4; j += 1) + { + for(int i = 0; i < 4; i += 1) + { + result.v[i][j] = m.v[j][i]; + } + } + return result; +} + internal Mat4x4F32 scale_4x4f32(Mat4x4F32 m, F32 scale) { diff --git a/src/base/base_math.h b/src/base/base_math.h index b6063ad58..520ce3571 100644 --- a/src/base/base_math.h +++ b/src/base/base_math.h @@ -540,6 +540,7 @@ internal Mat4x4F32 make_orthographic_4x4f32(F32 left, F32 right, F32 bottom, F32 internal Mat4x4F32 make_look_at_4x4f32(Vec3F32 eye, Vec3F32 center, Vec3F32 up); internal Mat4x4F32 make_rotate_4x4f32(Vec3F32 axis, F32 turns); internal Mat4x4F32 mul_4x4f32(Mat4x4F32 a, Mat4x4F32 b); +internal Mat4x4F32 transpose_4x4f32(Mat4x4F32 m); internal Mat4x4F32 scale_4x4f32(Mat4x4F32 m, F32 scale); internal Mat4x4F32 inverse_4x4f32(Mat4x4F32 m); internal Mat4x4F32 derotate_4x4f32(Mat4x4F32 mat); diff --git a/src/base/base_strings.c b/src/base/base_strings.c index 9e42e0cd6..717c4fa85 100644 --- a/src/base/base_strings.c +++ b/src/base/base_strings.c @@ -1,2446 +1,2455 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -//////////////////////////////// -//~ rjf: Third Party Includes - -#if !BUILD_SUPPLEMENTARY_UNIT -# define STB_SPRINTF_IMPLEMENTATION -# define STB_SPRINTF_STATIC -# include "third_party/stb/stb_sprintf.h" -#endif - -//////////////////////////////// -//~ NOTE(allen): String <-> Integer Tables - -read_only global U8 integer_symbols[16] = { - '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F', -}; - -// NOTE(allen): Includes reverses for uppercase and lowercase hex. -read_only global U8 integer_symbol_reverse[128] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -read_only global U8 base64[64] = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - '_', '$', -}; - -read_only global U8 base64_reverse[128] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFF,0xFF,0xFF,0xFF,0xFF,0x00, - 0xFF,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32, - 0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0xFF,0xFF,0xFF,0xFF,0x3E, - 0xFF,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18, - 0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -//////////////////////////////// -//~ rjf: Character Classification & Conversion Functions - -internal B32 -char_is_space(U8 c){ - return(c == ' ' || c == '\n' || c == '\t' || c == '\r' || c == '\f' || c == '\v'); -} - -internal B32 -char_is_upper(U8 c){ - return('A' <= c && c <= 'Z'); -} - -internal B32 -char_is_lower(U8 c){ - return('a' <= c && c <= 'z'); -} - -internal B32 -char_is_alpha(U8 c){ - return(char_is_upper(c) || char_is_lower(c)); -} - -internal B32 -char_is_slash(U8 c){ - return(c == '/' || c == '\\'); -} - -internal B32 -char_is_digit(U8 c, U32 base){ - B32 result = 0; - if (0 < base && base <= 16){ - U8 val = integer_symbol_reverse[c]; - if (val < base){ - result = 1; - } - } - return(result); -} - -internal U8 -char_to_lower(U8 c){ - if (char_is_upper(c)){ - c += ('a' - 'A'); - } - return(c); -} - -internal U8 -char_to_upper(U8 c){ - if (char_is_lower(c)){ - c += ('A' - 'a'); - } - return(c); -} - -internal U8 -char_to_correct_slash(U8 c){ - if(char_is_slash(c)){ - c = '/'; - } - return(c); -} - -//////////////////////////////// -//~ rjf: C-String Measurement - -internal U64 -cstring8_length(U8 *c){ - U8 *p = c; - for (;*p != 0; p += 1); - return(p - c); -} - -internal U64 -cstring16_length(U16 *c){ - U16 *p = c; - for (;*p != 0; p += 1); - return(p - c); -} - -internal U64 -cstring32_length(U32 *c){ - U32 *p = c; - for (;*p != 0; p += 1); - return(p - c); -} - -//////////////////////////////// -//~ rjf: String Constructors - -internal String8 -str8(U8 *str, U64 size){ - String8 result = {str, size}; - return(result); -} - -internal String8 -str8_range(U8 *first, U8 *one_past_last){ - String8 result = {first, (U64)(one_past_last - first)}; - return(result); -} - -internal String8 -str8_zero(void){ - String8 result = {0}; - return(result); -} - -internal String16 -str16(U16 *str, U64 size){ - String16 result = {str, size}; - return(result); -} - -internal String16 -str16_range(U16 *first, U16 *one_past_last){ - String16 result = {first, (U64)(one_past_last - first)}; - return(result); -} - -internal String16 -str16_zero(void){ - String16 result = {0}; - return(result); -} - -internal String32 -str32(U32 *str, U64 size){ - String32 result = {str, size}; - return(result); -} - -internal String32 -str32_range(U32 *first, U32 *one_past_last){ - String32 result = {first, (U64)(one_past_last - first)}; - return(result); -} - -internal String32 -str32_zero(void){ - String32 result = {0}; - return(result); -} - -internal String8 -str8_cstring(char *c){ - String8 result = {(U8*)c, cstring8_length((U8*)c)}; - return(result); -} - -internal String16 -str16_cstring(U16 *c){ - String16 result = {(U16*)c, cstring16_length((U16*)c)}; - return(result); -} - -internal String32 -str32_cstring(U32 *c){ - String32 result = {(U32*)c, cstring32_length((U32*)c)}; - return(result); -} - -internal String8 -str8_cstring_capped(void *cstr, void *cap) -{ - char *ptr = (char *)cstr; - char *opl = (char *)cap; - for (;ptr < opl && *ptr != 0; ptr += 1); - U64 size = (U64)(ptr - (char *)cstr); - String8 result = str8((U8*)cstr, size); - return result; -} - -internal String16 -str16_cstring_capped(void *cstr, void *cap) -{ - U16 *ptr = (U16 *)cstr; - U16 *opl = (U16 *)cap; - for (;ptr < opl && *ptr != 0; ptr += 1); - U64 size = (U64)(ptr - (U16 *)cstr); - String16 result = str16(cstr, size); - return result; -} - -internal String8 -str8_cstring_capped_reverse(void *raw_start, void *raw_cap) -{ - U8 *start = raw_start; - U8 *ptr = raw_cap; - for(; ptr > start; ) - { - ptr -= 1; - - if (*ptr == '\0') - { - break; - } - } - U64 size = (U64)(ptr - start); - String8 result = str8(start, size); - return result; -} - -//////////////////////////////// -//~ rjf: String Stylization - -internal String8 -upper_from_str8(Arena *arena, String8 string) -{ - string = push_str8_copy(arena, string); - for(U64 idx = 0; idx < string.size; idx += 1) - { - string.str[idx] = char_to_upper(string.str[idx]); - } - return string; -} - -internal String8 -lower_from_str8(Arena *arena, String8 string) -{ - string = push_str8_copy(arena, string); - for(U64 idx = 0; idx < string.size; idx += 1) - { - string.str[idx] = char_to_lower(string.str[idx]); - } - return string; -} - -internal String8 -backslashed_from_str8(Arena *arena, String8 string) -{ - string = push_str8_copy(arena, string); - for(U64 idx = 0; idx < string.size; idx += 1) - { - string.str[idx] = char_is_slash(string.str[idx]) ? '\\' : string.str[idx]; - } - return string; -} - -//////////////////////////////// -//~ rjf: String Matching - -internal B32 -str8_match(String8 a, String8 b, StringMatchFlags flags) -{ - B32 result = 0; - if(a.size == b.size && flags == 0) - { - result = MemoryMatch(a.str, b.str, b.size); - } - else if(a.size == b.size || (flags & StringMatchFlag_RightSideSloppy)) - { - B32 case_insensitive = (flags & StringMatchFlag_CaseInsensitive); - B32 slash_insensitive = (flags & StringMatchFlag_SlashInsensitive); - U64 size = Min(a.size, b.size); - result = 1; - for(U64 i = 0; i < size; i += 1) - { - U8 at = a.str[i]; - U8 bt = b.str[i]; - if(case_insensitive) - { - at = char_to_upper(at); - bt = char_to_upper(bt); - } - if(slash_insensitive) - { - at = char_to_correct_slash(at); - bt = char_to_correct_slash(bt); - } - if(at != bt) - { - result = 0; - break; - } - } - } - return result; -} - -internal U64 -str8_find_needle(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags){ - U8 *p = string.str + start_pos; - U64 stop_offset = Max(string.size + 1, needle.size) - needle.size; - U8 *stop_p = string.str + stop_offset; - if (needle.size > 0){ - U8 *string_opl = string.str + string.size; - String8 needle_tail = str8_skip(needle, 1); - StringMatchFlags adjusted_flags = flags | StringMatchFlag_RightSideSloppy; - U8 needle_first_char_adjusted = needle.str[0]; - if(adjusted_flags & StringMatchFlag_CaseInsensitive){ - needle_first_char_adjusted = char_to_upper(needle_first_char_adjusted); - } - for (;p < stop_p; p += 1){ - U8 haystack_char_adjusted = *p; - if(adjusted_flags & StringMatchFlag_CaseInsensitive){ - haystack_char_adjusted = char_to_upper(haystack_char_adjusted); - } - if (haystack_char_adjusted == needle_first_char_adjusted){ - if (str8_match(str8_range(p + 1, string_opl), needle_tail, adjusted_flags)){ - break; - } - } - } - } - U64 result = string.size; - if (p < stop_p){ - result = (U64)(p - string.str); - } - return(result); -} - -internal U64 -str8_find_needle_reverse(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags) -{ - U64 result = 0; - for(S64 i = string.size - start_pos - needle.size; i >= 0; --i) - { - String8 haystack = str8_substr(string, rng_1u64(i, i + needle.size)); - if(str8_match(haystack, needle, flags)) - { - result = (U64)i + needle.size; - break; - } - } - return result; -} - -internal B32 -str8_ends_with(String8 string, String8 end, StringMatchFlags flags){ - String8 postfix = str8_postfix(string, end.size); - B32 is_match = str8_match(end, postfix, flags); - return is_match; -} - -//////////////////////////////// -//~ rjf: String Slicing - -internal String8 -str8_substr(String8 str, Rng1U64 range){ - range.min = ClampTop(range.min, str.size); - range.max = ClampTop(range.max, str.size); - str.str += range.min; - str.size = dim_1u64(range); - return(str); -} - -internal String8 -str8_prefix(String8 str, U64 size){ - str.size = ClampTop(size, str.size); - return(str); -} - -internal String8 -str8_skip(String8 str, U64 amt){ - amt = ClampTop(amt, str.size); - str.str += amt; - str.size -= amt; - return(str); -} - -internal String8 -str8_postfix(String8 str, U64 size){ - size = ClampTop(size, str.size); - str.str = (str.str + str.size) - size; - str.size = size; - return(str); -} - -internal String8 -str8_chop(String8 str, U64 amt){ - amt = ClampTop(amt, str.size); - str.size -= amt; - return(str); -} - -internal String8 -str8_skip_chop_whitespace(String8 string){ - U8 *first = string.str; - U8 *opl = first + string.size; - for (;first < opl; first += 1){ - if (!char_is_space(*first)){ - break; - } - } - for (;opl > first;){ - opl -= 1; - if (!char_is_space(*opl)){ - opl += 1; - break; - } - } - String8 result = str8_range(first, opl); - return(result); -} - -//////////////////////////////// -//~ rjf: String Formatting & Copying - -internal String8 -push_str8_cat(Arena *arena, String8 s1, String8 s2){ - String8 str; - str.size = s1.size + s2.size; - str.str = push_array_no_zero(arena, U8, str.size + 1); - MemoryCopy(str.str, s1.str, s1.size); - MemoryCopy(str.str + s1.size, s2.str, s2.size); - str.str[str.size] = 0; - return(str); -} - -internal String8 -push_str8_copy(Arena *arena, String8 s){ - String8 str; - str.size = s.size; - str.str = push_array_no_zero(arena, U8, str.size + 1); - MemoryCopy(str.str, s.str, s.size); - str.str[str.size] = 0; - return(str); -} - -internal String8 -push_str8fv(Arena *arena, char *fmt, va_list args){ - va_list args2; - va_copy(args2, args); - U32 needed_bytes = raddbg_vsnprintf(0, 0, fmt, args) + 1; - String8 result = {0}; - result.str = push_array_no_zero(arena, U8, needed_bytes); - result.size = raddbg_vsnprintf((char*)result.str, needed_bytes, fmt, args2); - result.str[result.size] = 0; - va_end(args2); - return(result); -} - -internal String8 -push_str8f(Arena *arena, char *fmt, ...){ - va_list args; - va_start(args, fmt); - String8 result = push_str8fv(arena, fmt, args); - va_end(args); - return(result); -} - -//////////////////////////////// -//~ rjf: String <=> Integer Conversions - -//- rjf: string -> integer - -internal S64 -sign_from_str8(String8 string, String8 *string_tail){ - // count negative signs - U64 neg_count = 0; - U64 i = 0; - for (; i < string.size; i += 1){ - if (string.str[i] == '-'){ - neg_count += 1; - } - else if (string.str[i] != '+'){ - break; - } - } - - // output part of string after signs - *string_tail = str8_skip(string, i); - - // output integer sign - S64 sign = (neg_count & 1)?-1:+1; - return(sign); -} - -internal B32 -str8_is_integer(String8 string, U32 radix){ - B32 result = 0; - if (string.size > 0){ - if (1 < radix && radix <= 16){ - result = 1; - for (U64 i = 0; i < string.size; i += 1){ - U8 c = string.str[i]; - if (!(c < 0x80) || integer_symbol_reverse[c] >= radix){ - result = 0; - break; - } - } - } - } - return(result); -} - -internal U64 -u64_from_str8(String8 string, U32 radix){ - U64 x = 0; - if (1 < radix && radix <= 16){ - for (U64 i = 0; i < string.size; i += 1){ - x *= radix; - x += integer_symbol_reverse[string.str[i]&0x7F]; - } - } - return(x); -} - -internal S64 -s64_from_str8(String8 string, U32 radix){ - S64 sign = sign_from_str8(string, &string); - S64 x = (S64)u64_from_str8(string, radix) * sign; - return(x); -} - -internal U32 -u32_from_str8(String8 string, U32 radix) -{ - U64 x64 = u64_from_str8(string, radix); - U32 x32 = safe_cast_u32(x64); - return x32; -} - -internal S32 -s32_from_str8(String8 string, U32 radix) -{ - S64 x64 = s64_from_str8(string, radix); - S32 x32 = safe_cast_s32(x64); - return x32; -} - -internal B32 -try_u64_from_str8_c_rules(String8 string, U64 *x){ - B32 is_integer = 0; - if (str8_is_integer(string, 10)){ - is_integer = 1; - *x = u64_from_str8(string, 10); - } - else{ - String8 hex_string = str8_skip(string, 2); - if (str8_match(str8_prefix(string, 2), str8_lit("0x"), 0) && - str8_is_integer(hex_string, 0x10)){ - is_integer = 1; - *x = u64_from_str8(hex_string, 0x10); - } - else if (str8_match(str8_prefix(string, 2), str8_lit("0b"), 0) && - str8_is_integer(hex_string, 2)){ - is_integer = 1; - *x = u64_from_str8(hex_string, 2); - } - else{ - String8 oct_string = str8_skip(string, 1); - if (str8_match(str8_prefix(string, 1), str8_lit("0"), 0) && - str8_is_integer(hex_string, 010)){ - is_integer = 1; - *x = u64_from_str8(oct_string, 010); - } - } - } - return(is_integer); -} - -internal B32 -try_s64_from_str8_c_rules(String8 string, S64 *x){ - String8 string_tail = {0}; - S64 sign = sign_from_str8(string, &string_tail); - U64 x_u64 = 0; - B32 is_integer = try_u64_from_str8_c_rules(string_tail, &x_u64); - *x = x_u64*sign; - return(is_integer); -} - -//- rjf: integer -> string - -internal String8 -str8_from_memory_size(Arena *arena, U64 size) -{ - String8 result; - - if(size < KB(1)) - { - result = push_str8f(arena, "%llu Bytes", size); - } - else if(size < MB(1)) - { - result = push_str8f(arena, "%llu.%02llu KiB", size / KB(1), ((size * 100) / KB(1)) % 100); - } - else if(size < GB(1)) - { - result = push_str8f(arena, "%llu.%02llu MiB", size / MB(1), ((size * 100) / MB(1)) % 100); - } - else if(size < TB(1)) - { - result = push_str8f(arena, "%llu.%02llu GiB", size / GB(1), ((size * 100) / GB(1)) % 100); - } - else - { - result = push_str8f(arena, "%llu.%02llu TiB", size / TB(1), ((size * 100) / TB(1)) % 100); - } - - return result; -} - -internal String8 -str8_from_count(Arena *arena, U64 count) -{ - String8 result; - - if(count < 1 * 1000) - { - result = push_str8f(arena, "%llu", count); - } - else if(count < 1000000) - { - U64 frac = ((count * 100) / 1000) % 100; - if(frac > 0) - { - result = push_str8f(arena, "%llu.%02lluK", count / 1000, frac); - } - else - { - result = push_str8f(arena, "%lluK", count / 1000); - } - } - else if(count < 1000000000) - { - U64 frac = ((count * 100) / 1000000) % 100; - if(frac > 0) - { - result = push_str8f(arena, "%llu.%02lluM", count / 1000000, frac); - } - else - { - result = push_str8f(arena, "%lluM", count / 1000000); - } - } - else - { - U64 frac = ((count * 100) * 1000000000) % 100; - if(frac > 0) - { - result = push_str8f(arena, "%llu.%02lluB", count / 1000000000, frac); - } - else - { - result = push_str8f(arena, "%lluB", count / 1000000000, frac); - } - } - - return result; -} - -internal String8 -str8_from_bits_u32(Arena *arena, U32 x) -{ - U8 c0 = 'a' + ((x >> 28) & 0xf); - U8 c1 = 'a' + ((x >> 24) & 0xf); - U8 c2 = 'a' + ((x >> 20) & 0xf); - U8 c3 = 'a' + ((x >> 16) & 0xf); - U8 c4 = 'a' + ((x >> 12) & 0xf); - U8 c5 = 'a' + ((x >> 8) & 0xf); - U8 c6 = 'a' + ((x >> 4) & 0xf); - U8 c7 = 'a' + ((x >> 0) & 0xf); - String8 result = push_str8f(arena, "%c%c%c%c%c%c%c%c", c0, c1, c2, c3, c4, c5, c6, c7); - return result; -} - -internal String8 -str8_from_bits_u64(Arena *arena, U64 x) -{ - U8 c0 = 'a' + ((x >> 60) & 0xf); - U8 c1 = 'a' + ((x >> 56) & 0xf); - U8 c2 = 'a' + ((x >> 52) & 0xf); - U8 c3 = 'a' + ((x >> 48) & 0xf); - U8 c4 = 'a' + ((x >> 44) & 0xf); - U8 c5 = 'a' + ((x >> 40) & 0xf); - U8 c6 = 'a' + ((x >> 36) & 0xf); - U8 c7 = 'a' + ((x >> 32) & 0xf); - U8 c8 = 'a' + ((x >> 28) & 0xf); - U8 c9 = 'a' + ((x >> 24) & 0xf); - U8 ca = 'a' + ((x >> 20) & 0xf); - U8 cb = 'a' + ((x >> 16) & 0xf); - U8 cc = 'a' + ((x >> 12) & 0xf); - U8 cd = 'a' + ((x >> 8) & 0xf); - U8 ce = 'a' + ((x >> 4) & 0xf); - U8 cf = 'a' + ((x >> 0) & 0xf); - String8 result = push_str8f(arena, - "%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c", - c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf); - return result; -} - -internal String8 -str8_from_u64(Arena *arena, U64 u64, U32 radix, U8 min_digits, U8 digit_group_separator) -{ - String8 result = {0}; - { - // rjf: prefix - String8 prefix = {0}; - switch(radix) - { - case 16:{prefix = str8_lit("0x");}break; - case 8: {prefix = str8_lit("0o");}break; - case 2: {prefix = str8_lit("0b");}break; - } - - // rjf: determine # of chars between separators - U8 digit_group_size = 3; - switch(radix) - { - default:break; - case 2: - case 8: - case 16: - {digit_group_size = 4;}break; - } - - // rjf: prep - U64 needed_leading_0s = 0; - { - U64 needed_digits = 1; - { - U64 u64_reduce = u64; - for(;;) - { - u64_reduce /= radix; - if(u64_reduce == 0) - { - break; - } - needed_digits += 1; - } - } - needed_leading_0s = (min_digits > needed_digits) ? min_digits - needed_digits : 0; - U64 needed_separators = 0; - if(digit_group_separator != 0) - { - needed_separators = (needed_digits+needed_leading_0s)/digit_group_size; - if(needed_separators > 0 && (needed_digits+needed_leading_0s)%digit_group_size == 0) - { - needed_separators -= 1; - } - } - result.size = prefix.size + needed_leading_0s + needed_separators + needed_digits; - result.str = push_array_no_zero(arena, U8, result.size + 1); - result.str[result.size] = 0; - } - - // rjf: fill contents - { - U64 u64_reduce = u64; - U64 digits_until_separator = digit_group_size; - for(U64 idx = 0; idx < result.size; idx += 1) - { - if(digits_until_separator == 0 && digit_group_separator != 0) - { - result.str[result.size - idx - 1] = digit_group_separator; - digits_until_separator = digit_group_size+1; - } - else - { - result.str[result.size - idx - 1] = char_to_lower(integer_symbols[u64_reduce%radix]); - u64_reduce /= radix; - } - digits_until_separator -= 1; - if(u64_reduce == 0) - { - break; - } - } - for(U64 leading_0_idx = 0; leading_0_idx < needed_leading_0s; leading_0_idx += 1) - { - result.str[prefix.size + leading_0_idx] = '0'; - } - } - - // rjf: fill prefix - if(prefix.size != 0) - { - MemoryCopy(result.str, prefix.str, prefix.size); - } - } - return result; -} - -internal String8 -str8_from_s64(Arena *arena, S64 s64, U32 radix, U8 min_digits, U8 digit_group_separator) -{ - String8 result = {0}; - // TODO(rjf): preeeeetty sloppy... - if(s64 < 0) - { - Temp scratch = scratch_begin(&arena, 1); - String8 numeric_part = str8_from_u64(scratch.arena, (U64)(-s64), radix, min_digits, digit_group_separator); - result = push_str8f(arena, "-%S", numeric_part); - scratch_end(scratch); - } - else - { - result = str8_from_u64(arena, (U64)s64, radix, min_digits, digit_group_separator); - } - return result; -} - -//////////////////////////////// -//~ rjf: String <=> Float Conversions - -internal F64 -f64_from_str8(String8 string) -{ - // TODO(rjf): crappy implementation for now that just uses atof. - F64 result = 0; - if(string.size > 0) - { - // rjf: find starting pos of numeric string, as well as sign - F64 sign = +1.0; - if(string.str[0] == '-') - { - sign = -1.0; - } - else if(string.str[0] == '+') - { - sign = 1.0; - } - - // rjf: gather numerics - U64 num_valid_chars = 0; - char buffer[64]; - B32 exp = 0; - for(U64 idx = 0; idx < string.size && num_valid_chars < sizeof(buffer)-1; idx += 1) - { - if(char_is_digit(string.str[idx], 10) || string.str[idx] == '.' || string.str[idx] == 'e' || - (exp && (string.str[idx] == '+' || string.str[idx] == '-'))) - { - buffer[num_valid_chars] = string.str[idx]; - num_valid_chars += 1; - exp = 0; - exp = (string.str[idx] == 'e'); - } - } - - // rjf: null-terminate (the reason for all of this!!!!!!) - buffer[num_valid_chars] = 0; - - // rjf: do final conversion - result = sign * atof(buffer); - } - return result; -} - -//////////////////////////////// -//~ rjf: String List Construction Functions - -internal String8Node* -str8_list_push_node(String8List *list, String8Node *node){ - SLLQueuePush(list->first, list->last, node); - list->node_count += 1; - list->total_size += node->string.size; - return(node); -} - -internal String8Node* -str8_list_push_node_set_string(String8List *list, String8Node *node, String8 string){ - SLLQueuePush(list->first, list->last, node); - list->node_count += 1; - list->total_size += string.size; - node->string = string; - return(node); -} - -internal String8Node* -str8_list_push_node_front(String8List *list, String8Node *node){ - SLLQueuePushFront(list->first, list->last, node); - list->node_count += 1; - list->total_size += node->string.size; - return(node); -} - -internal String8Node* -str8_list_push_node_front_set_string(String8List *list, String8Node *node, String8 string){ - SLLQueuePushFront(list->first, list->last, node); - list->node_count += 1; - list->total_size += string.size; - node->string = string; - return(node); -} - -internal String8Node* -str8_list_push(Arena *arena, String8List *list, String8 string){ - String8Node *node = push_array_no_zero(arena, String8Node, 1); - str8_list_push_node_set_string(list, node, string); - return(node); -} - -internal String8Node* -str8_list_push_front(Arena *arena, String8List *list, String8 string){ - String8Node *node = push_array_no_zero(arena, String8Node, 1); - str8_list_push_node_front_set_string(list, node, string); - return(node); -} - -internal void -str8_list_concat_in_place(String8List *list, String8List *to_push){ - if(to_push->node_count != 0){ - if (list->last){ - list->node_count += to_push->node_count; - list->total_size += to_push->total_size; - list->last->next = to_push->first; - list->last = to_push->last; - } - else{ - *list = *to_push; - } - MemoryZeroStruct(to_push); - } -} - -internal String8Node* -str8_list_push_aligner(Arena *arena, String8List *list, U64 min, U64 align){ - String8Node *node = push_array_no_zero(arena, String8Node, 1); - U64 new_size = list->total_size + min; - U64 increase_size = 0; - if (align > 1){ - // NOTE(allen): assert is power of 2 - Assert(((align - 1) & align) == 0); - U64 mask = align - 1; - new_size += mask; - new_size &= (~mask); - increase_size = new_size - list->total_size; - } - local_persist const U8 zeroes_buffer[64] = {0}; - Assert(increase_size <= ArrayCount(zeroes_buffer)); - SLLQueuePush(list->first, list->last, node); - list->node_count += 1; - list->total_size = new_size; - node->string.str = (U8*)zeroes_buffer; - node->string.size = increase_size; - return(node); -} - -internal String8Node* -str8_list_pushf(Arena *arena, String8List *list, char *fmt, ...){ - va_list args; - va_start(args, fmt); - String8 string = push_str8fv(arena, fmt, args); - String8Node *result = str8_list_push(arena, list, string); - va_end(args); - return(result); -} - -internal String8Node* -str8_list_push_frontf(Arena *arena, String8List *list, char *fmt, ...){ - va_list args; - va_start(args, fmt); - String8 string = push_str8fv(arena, fmt, args); - String8Node *result = str8_list_push_front(arena, list, string); - va_end(args); - return(result); -} - -internal String8List -str8_list_copy(Arena *arena, String8List *list){ - String8List result = {0}; - for (String8Node *node = list->first; - node != 0; - node = node->next){ - String8Node *new_node = push_array_no_zero(arena, String8Node, 1); - String8 new_string = push_str8_copy(arena, node->string); - str8_list_push_node_set_string(&result, new_node, new_string); - } - return(result); -} - -internal String8List -str8_split(Arena *arena, String8 string, U8 *split_chars, U64 split_char_count, StringSplitFlags flags){ - String8List list = {0}; - - B32 keep_empties = (flags & StringSplitFlag_KeepEmpties); - - U8 *ptr = string.str; - U8 *opl = string.str + string.size; - for (;ptr < opl;){ - U8 *first = ptr; - for (;ptr < opl; ptr += 1){ - U8 c = *ptr; - B32 is_split = 0; - for (U64 i = 0; i < split_char_count; i += 1){ - if (split_chars[i] == c){ - is_split = 1; - break; - } - } - if (is_split){ - break; - } - } - - String8 string = str8_range(first, ptr); - if (keep_empties || string.size > 0){ - str8_list_push(arena, &list, string); - } - ptr += 1; - } - - return(list); -} - -internal String8List -str8_split_by_string_chars(Arena *arena, String8 string, String8 split_chars, StringSplitFlags flags){ - String8List list = str8_split(arena, string, split_chars.str, split_chars.size, flags); - return list; -} - -internal String8List -str8_list_split_by_string_chars(Arena *arena, String8List list, String8 split_chars, StringSplitFlags flags){ - String8List result = {0}; - for (String8Node *node = list.first; node != 0; node = node->next){ - String8List split = str8_split_by_string_chars(arena, node->string, split_chars, flags); - str8_list_concat_in_place(&result, &split); - } - return result; -} - -internal String8 -str8_list_join(Arena *arena, String8List *list, StringJoin *optional_params){ - StringJoin join = {0}; - if (optional_params != 0){ - MemoryCopyStruct(&join, optional_params); - } - - U64 sep_count = 0; - if (list->node_count > 0){ - sep_count = list->node_count - 1; - } - - String8 result; - result.size = join.pre.size + join.post.size + sep_count*join.sep.size + list->total_size; - U8 *ptr = result.str = push_array_no_zero(arena, U8, result.size + 1); - - MemoryCopy(ptr, join.pre.str, join.pre.size); - ptr += join.pre.size; - for (String8Node *node = list->first; - node != 0; - node = node->next){ - MemoryCopy(ptr, node->string.str, node->string.size); - ptr += node->string.size; - if (node->next != 0){ - MemoryCopy(ptr, join.sep.str, join.sep.size); - ptr += join.sep.size; - } - } - MemoryCopy(ptr, join.post.str, join.post.size); - ptr += join.post.size; - - *ptr = 0; - - return(result); -} - -internal void -str8_list_from_flags(Arena *arena, String8List *list, - U32 flags, String8 *flag_string_table, U32 flag_string_count){ - for (U32 i = 0; i < flag_string_count; i += 1){ - U32 flag = (1 << i); - if (flags & flag){ - str8_list_push(arena, list, flag_string_table[i]); - } - } -} - -//////////////////////////////// -//~ rjf; String Arrays - -internal String8Array -str8_array_from_list(Arena *arena, String8List *list) -{ - String8Array array; - array.count = list->node_count; - array.v = push_array_no_zero(arena, String8, array.count); - U64 idx = 0; - for(String8Node *n = list->first; n != 0; n = n->next, idx += 1) - { - array.v[idx] = n->string; - } - return array; -} - -internal String8Array -str8_array_reserve(Arena *arena, U64 count) -{ - String8Array arr; - arr.count = 0; - arr.v = push_array(arena, String8, count); - return arr; -} - -//////////////////////////////// -//~ rjf: String Path Helpers - -internal String8 -str8_chop_last_slash(String8 string){ - if (string.size > 0){ - U8 *ptr = string.str + string.size - 1; - for (;ptr >= string.str; ptr -= 1){ - if (*ptr == '/' || *ptr == '\\'){ - break; - } - } - if (ptr >= string.str){ - string.size = (U64)(ptr - string.str); - } - else{ - string.size = 0; - } - } - return(string); -} - -internal String8 -str8_skip_last_slash(String8 string){ - if (string.size > 0){ - U8 *ptr = string.str + string.size - 1; - for (;ptr >= string.str; ptr -= 1){ - if (*ptr == '/' || *ptr == '\\'){ - break; - } - } - if (ptr >= string.str){ - ptr += 1; - string.size = (U64)(string.str + string.size - ptr); - string.str = ptr; - } - } - return(string); -} - -internal String8 -str8_chop_last_dot(String8 string) -{ - String8 result = string; - U64 p = string.size; - for (;p > 0;){ - p -= 1; - if (string.str[p] == '.'){ - result = str8_prefix(string, p); - break; - } - } - return(result); -} - -internal String8 -str8_skip_last_dot(String8 string){ - String8 result = string; - U64 p = string.size; - for (;p > 0;){ - p -= 1; - if (string.str[p] == '.'){ - result = str8_skip(string, p + 1); - break; - } - } - return(result); -} - -internal PathStyle -path_style_from_str8(String8 string){ - PathStyle result = PathStyle_Relative; - if (string.size >= 1 && string.str[0] == '/'){ - result = PathStyle_UnixAbsolute; - } - else if (string.size >= 2 && - char_is_alpha(string.str[0]) && - string.str[1] == ':'){ - if (string.size == 2 || - char_is_slash(string.str[2])){ - result = PathStyle_WindowsAbsolute; - } - } - return(result); -} - -internal String8List -str8_split_path(Arena *arena, String8 string){ - String8List result = str8_split(arena, string, (U8*)"/\\", 2, 0); - return(result); -} - -internal void -str8_path_list_resolve_dots_in_place(String8List *path, PathStyle style){ - Temp scratch = scratch_begin(0, 0); - - String8MetaNode *stack = 0; - String8MetaNode *free_meta_node = 0; - String8Node *first = path->first; - - MemoryZeroStruct(path); - for (String8Node *node = first, *next = 0; - node != 0; - node = next){ - // save next now - next = node->next; - - // cases: - if (node == first && style == PathStyle_WindowsAbsolute){ - goto save_without_stack; - } - if (node->string.size == 1 && node->string.str[0] == '.'){ - goto do_nothing; - } - if (node->string.size == 2 && node->string.str[0] == '.' && node->string.str[1] == '.'){ - if (stack != 0){ - goto eliminate_stack_top; - } - else{ - goto save_without_stack; - } - } - goto save_with_stack; - - - // handlers: - save_with_stack: - { - str8_list_push_node(path, node); - - String8MetaNode *stack_node = free_meta_node; - if (stack_node != 0){ - SLLStackPop(free_meta_node); - } - else{ - stack_node = push_array_no_zero(scratch.arena, String8MetaNode, 1); - } - SLLStackPush(stack, stack_node); - stack_node->node = node; - - continue; - } - - save_without_stack: - { - str8_list_push_node(path, node); - - continue; - } - - eliminate_stack_top: - { - path->node_count -= 1; - path->total_size -= stack->node->string.size; - - SLLStackPop(stack); - - if (stack == 0){ - path->last = path->first; - } - else{ - path->last = stack->node; - } - continue; - } - - do_nothing: continue; - } - scratch_end(scratch); -} - -internal String8 -str8_path_list_join_by_style(Arena *arena, String8List *path, PathStyle style){ - StringJoin params = {0}; - switch(style) - { - case PathStyle_Null:{}break; - case PathStyle_Relative: - case PathStyle_WindowsAbsolute: - { - params.sep = str8_lit("/"); - }break; - - case PathStyle_UnixAbsolute: - { - params.pre = str8_lit("/"); - params.sep = str8_lit("/"); - }break; - } - String8 result = str8_list_join(arena, path, ¶ms); - return result; -} - -internal String8TxtPtPair -str8_txt_pt_pair_from_string(String8 string) -{ - String8TxtPtPair pair = {0}; - { - String8 file_part = {0}; - String8 line_part = {0}; - String8 col_part = {0}; - - // rjf: grab file part - for(U64 idx = 0; idx <= string.size; idx += 1) - { - U8 byte = (idx < string.size) ? (string.str[idx]) : 0; - U8 next_byte = ((idx+1 < string.size) ? (string.str[idx+1]) : 0); - if(byte == ':' && next_byte != '/' && next_byte != '\\') - { - file_part = str8_prefix(string, idx); - line_part = str8_skip(string, idx+1); - break; - } - else if(byte == 0) - { - file_part = string; - break; - } - } - - // rjf: grab line/column - { - U64 colon_pos = str8_find_needle(line_part, 0, str8_lit(":"), 0); - if(colon_pos < line_part.size) - { - col_part = str8_skip(line_part, colon_pos+1); - line_part = str8_prefix(line_part, colon_pos); - } - } - - // rjf: convert line/column strings to numerics - U64 line = 0; - U64 column = 0; - try_u64_from_str8_c_rules(line_part, &line); - try_u64_from_str8_c_rules(col_part, &column); - - // rjf: fill - pair.string = file_part; - pair.pt = txt_pt((S64)line, (S64)column); - if(pair.pt.line == 0) { pair.pt.line = 1; } - if(pair.pt.column == 0) { pair.pt.column = 1; } - } - return pair; -} - -//////////////////////////////// -//~ rjf: UTF-8 & UTF-16 Decoding/Encoding - -read_only global U8 utf8_class[32] = { - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,2,2,2,3,3,4,5, -}; - -internal UnicodeDecode -utf8_decode(U8 *str, U64 max){ - UnicodeDecode result = {1, max_U32}; - U8 byte = str[0]; - U8 byte_class = utf8_class[byte >> 3]; - switch (byte_class) - { - case 1: - { - result.codepoint = byte; - }break; - case 2: - { - if (2 < max) - { - U8 cont_byte = str[1]; - if (utf8_class[cont_byte >> 3] == 0) - { - result.codepoint = (byte & bitmask5) << 6; - result.codepoint |= (cont_byte & bitmask6); - result.inc = 2; - } - } - }break; - case 3: - { - if (2 < max) - { - U8 cont_byte[2] = {str[1], str[2]}; - if (utf8_class[cont_byte[0] >> 3] == 0 && - utf8_class[cont_byte[1] >> 3] == 0) - { - result.codepoint = (byte & bitmask4) << 12; - result.codepoint |= ((cont_byte[0] & bitmask6) << 6); - result.codepoint |= (cont_byte[1] & bitmask6); - result.inc = 3; - } - } - }break; - case 4: - { - if (3 < max) - { - U8 cont_byte[3] = {str[1], str[2], str[3]}; - if (utf8_class[cont_byte[0] >> 3] == 0 && - utf8_class[cont_byte[1] >> 3] == 0 && - utf8_class[cont_byte[2] >> 3] == 0) - { - result.codepoint = (byte & bitmask3) << 18; - result.codepoint |= ((cont_byte[0] & bitmask6) << 12); - result.codepoint |= ((cont_byte[1] & bitmask6) << 6); - result.codepoint |= (cont_byte[2] & bitmask6); - result.inc = 4; - } - } - } - } - return(result); -} - -internal UnicodeDecode -utf16_decode(U16 *str, U64 max){ - UnicodeDecode result = {1, max_U32}; - result.codepoint = str[0]; - result.inc = 1; - if (max > 1 && 0xD800 <= str[0] && str[0] < 0xDC00 && 0xDC00 <= str[1] && str[1] < 0xE000){ - result.codepoint = ((str[0] - 0xD800) << 10) | ((str[1] - 0xDC00) + 0x10000); - result.inc = 2; - } - return(result); -} - -internal U32 -utf8_encode(U8 *str, U32 codepoint){ - U32 inc = 0; - if (codepoint <= 0x7F){ - str[0] = (U8)codepoint; - inc = 1; - } - else if (codepoint <= 0x7FF){ - str[0] = (bitmask2 << 6) | ((codepoint >> 6) & bitmask5); - str[1] = bit8 | (codepoint & bitmask6); - inc = 2; - } - else if (codepoint <= 0xFFFF){ - str[0] = (bitmask3 << 5) | ((codepoint >> 12) & bitmask4); - str[1] = bit8 | ((codepoint >> 6) & bitmask6); - str[2] = bit8 | ( codepoint & bitmask6); - inc = 3; - } - else if (codepoint <= 0x10FFFF){ - str[0] = (bitmask4 << 4) | ((codepoint >> 18) & bitmask3); - str[1] = bit8 | ((codepoint >> 12) & bitmask6); - str[2] = bit8 | ((codepoint >> 6) & bitmask6); - str[3] = bit8 | ( codepoint & bitmask6); - inc = 4; - } - else{ - str[0] = '?'; - inc = 1; - } - return(inc); -} - -internal U32 -utf16_encode(U16 *str, U32 codepoint){ - U32 inc = 1; - if (codepoint == max_U32){ - str[0] = (U16)'?'; - } - else if (codepoint < 0x10000){ - str[0] = (U16)codepoint; - } - else{ - U32 v = codepoint - 0x10000; - str[0] = safe_cast_u16(0xD800 + (v >> 10)); - str[1] = safe_cast_u16(0xDC00 + (v & bitmask10)); - inc = 2; - } - return(inc); -} - -internal U32 -utf8_from_utf32_single(U8 *buffer, U32 character){ - return(utf8_encode(buffer, character)); -} - -//////////////////////////////// -//~ rjf: Unicode String Conversions - -internal String8 -str8_from_16(Arena *arena, String16 in) -{ - String8 result = str8_zero(); - if(in.size) - { - U64 cap = in.size*3; - U8 *str = push_array_no_zero(arena, U8, cap + 1); - U16 *ptr = in.str; - U16 *opl = ptr + in.size; - U64 size = 0; - UnicodeDecode consume; - for(;ptr < opl; ptr += consume.inc) - { - consume = utf16_decode(ptr, opl - ptr); - size += utf8_encode(str + size, consume.codepoint); - } - str[size] = 0; - arena_pop(arena, (cap - size)); - result = str8(str, size); - } - return result; -} - -internal String16 -str16_from_8(Arena *arena, String8 in) -{ - String16 result = str16_zero(); - if(in.size) - { - U64 cap = in.size*2; - U16 *str = push_array_no_zero(arena, U16, cap + 1); - U8 *ptr = in.str; - U8 *opl = ptr + in.size; - U64 size = 0; - UnicodeDecode consume; - for(;ptr < opl; ptr += consume.inc) - { - consume = utf8_decode(ptr, opl - ptr); - size += utf16_encode(str + size, consume.codepoint); - } - str[size] = 0; - arena_pop(arena, (cap - size)*2); - result = str16(str, size); - } - return result; -} - -internal String8 -str8_from_32(Arena *arena, String32 in) -{ - String8 result = str8_zero(); - if(in.size) - { - U64 cap = in.size*4; - U8 *str = push_array_no_zero(arena, U8, cap + 1); - U32 *ptr = in.str; - U32 *opl = ptr + in.size; - U64 size = 0; - for(;ptr < opl; ptr += 1) - { - size += utf8_encode(str + size, *ptr); - } - str[size] = 0; - arena_pop(arena, (cap - size)); - result = str8(str, size); - } - return result; -} - -internal String32 -str32_from_8(Arena *arena, String8 in) -{ - String32 result = str32_zero(); - if(in.size) - { - U64 cap = in.size; - U32 *str = push_array_no_zero(arena, U32, cap + 1); - U8 *ptr = in.str; - U8 *opl = ptr + in.size; - U64 size = 0; - UnicodeDecode consume; - for(;ptr < opl; ptr += consume.inc) - { - consume = utf8_decode(ptr, opl - ptr); - str[size] = consume.codepoint; - size += 1; - } - str[size] = 0; - arena_pop(arena, (cap - size)*4); - result = str32(str, size); - } - return result; -} - -//////////////////////////////// -//~ String -> Enum Conversions - -read_only global struct -{ - String8 string; - OperatingSystem os; -} g_os_enum_map[] = -{ - { str8_lit_comp(""), OperatingSystem_Null }, - { str8_lit_comp("Windows"), OperatingSystem_Windows, }, - { str8_lit_comp("Linux"), OperatingSystem_Linux, }, - { str8_lit_comp("Mac"), OperatingSystem_Mac, }, -}; -StaticAssert(ArrayCount(g_os_enum_map) == OperatingSystem_COUNT, g_os_enum_map_count_check); - -internal OperatingSystem -operating_system_from_string(String8 string) -{ - for(U64 i = 0; i < ArrayCount(g_os_enum_map); ++i) - { - if(str8_match(g_os_enum_map[i].string, string, StringMatchFlag_CaseInsensitive)) - { - return g_os_enum_map[i].os; - } - } - return OperatingSystem_Null; -} - -//////////////////////////////// -//~ rjf: Basic Types & Space Enum -> String Conversions - -internal String8 -string_from_dimension(Dimension dimension){ - local_persist String8 strings[] = { - str8_lit_comp("X"), - str8_lit_comp("Y"), - str8_lit_comp("Z"), - str8_lit_comp("W"), - }; - String8 result = str8_lit("error"); - if ((U32)dimension < 4){ - result = strings[dimension]; - } - return(result); -} - -internal String8 -string_from_side(Side side){ - local_persist String8 strings[] = { - str8_lit_comp("Min"), - str8_lit_comp("Max"), - }; - String8 result = str8_lit("error"); - if ((U32)side < 2){ - result = strings[side]; - } - return(result); -} - -internal String8 -string_from_operating_system(OperatingSystem os) -{ - String8 result = g_os_enum_map[OperatingSystem_Null].string; - if(os < ArrayCount(g_os_enum_map)) - { - result = g_os_enum_map[os].string; - } - return result; -} - -internal String8 -string_from_arch(Arch arch){ - local_persist String8 strings[] = { - str8_lit_comp("Null"), - str8_lit_comp("x64"), - str8_lit_comp("x86"), - str8_lit_comp("arm64"), - str8_lit_comp("arm32"), - }; - String8 result = str8_lit("error"); - if (arch < Arch_COUNT){ - result = strings[arch]; - } - return(result); -} - -//////////////////////////////// -//~ rjf: Time Types -> String - -internal String8 -string_from_week_day(WeekDay week_day){ - local_persist String8 strings[] = { - str8_lit_comp("Sun"), - str8_lit_comp("Mon"), - str8_lit_comp("Tue"), - str8_lit_comp("Wed"), - str8_lit_comp("Thu"), - str8_lit_comp("Fri"), - str8_lit_comp("Sat"), - }; - String8 result = str8_lit("Err"); - if ((U32)week_day < WeekDay_COUNT){ - result = strings[week_day]; - } - return(result); -} - -internal String8 -string_from_month(Month month){ - local_persist String8 strings[] = { - str8_lit_comp("Jan"), - str8_lit_comp("Feb"), - str8_lit_comp("Mar"), - str8_lit_comp("Apr"), - str8_lit_comp("May"), - str8_lit_comp("Jun"), - str8_lit_comp("Jul"), - str8_lit_comp("Aug"), - str8_lit_comp("Sep"), - str8_lit_comp("Oct"), - str8_lit_comp("Nov"), - str8_lit_comp("Dec"), - }; - String8 result = str8_lit("Err"); - if ((U32)month < Month_COUNT){ - result = strings[month]; - } - return(result); -} - -internal String8 -push_date_time_string(Arena *arena, DateTime *date_time){ - char *mon_str = (char*)string_from_month(date_time->month).str; - U32 adjusted_hour = date_time->hour%12; - if (adjusted_hour == 0){ - adjusted_hour = 12; - } - char *ampm = "am"; - if (date_time->hour >= 12){ - ampm = "pm"; - } - String8 result = push_str8f(arena, "%d %s %d, %02d:%02d:%02d %s", - date_time->day, mon_str, date_time->year, - adjusted_hour, date_time->min, date_time->sec, ampm); - return(result); -} - -internal String8 -push_file_name_date_time_string(Arena *arena, DateTime *date_time){ - char *mon_str = (char*)string_from_month(date_time->month).str; - String8 result = push_str8f(arena, "%d-%s-%0d--%02d-%02d-%02d", - date_time->year, mon_str, date_time->day, - date_time->hour, date_time->min, date_time->sec); - return(result); -} - -internal String8 -string_from_elapsed_time(Arena *arena, DateTime dt){ - Temp scratch = scratch_begin(&arena, 1); - String8List list = {0}; - if (dt.year){ - str8_list_pushf(scratch.arena, &list, "%dy", dt.year); - str8_list_pushf(scratch.arena, &list, "%um", dt.mon); - str8_list_pushf(scratch.arena, &list, "%ud", dt.day); - } else if (dt.mon){ - str8_list_pushf(scratch.arena, &list, "%um", dt.mon); - str8_list_pushf(scratch.arena, &list, "%ud", dt.day); - } else if (dt.day){ - str8_list_pushf(scratch.arena, &list, "%ud", dt.day); - } - str8_list_pushf(scratch.arena, &list, "%u:%u:%u:%u ms", dt.hour, dt.min, dt.sec, dt.msec); - StringJoin join = { str8_lit_comp(""), str8_lit_comp(" "), str8_lit_comp("") }; - String8 result = str8_list_join(arena, &list, &join); - scratch_end(scratch); - return(result); -} - -//////////////////////////////// -//~ Globally UNique Ids - -internal String8 -string_from_guid(Arena *arena, Guid guid) -{ - String8 result = push_str8f(arena, "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", - guid.data1, - guid.data2, - guid.data3, - guid.data4[0], - guid.data4[1], - guid.data4[2], - guid.data4[3], - guid.data4[4], - guid.data4[5], - guid.data4[6], - guid.data4[7]); - return result; -} - -internal B32 -try_guid_from_string(String8 string, Guid *guid_out) -{ - Temp scratch = scratch_begin(0,0); - B32 is_parsed = 0; - String8List list = str8_split_by_string_chars(scratch.arena, string, str8_lit("-"), StringSplitFlag_KeepEmpties); - if(list.node_count == 5) - { - String8 data1_str = list.first->string; - String8 data2_str = list.first->next->string; - String8 data3_str = list.first->next->next->string; - String8 data4_hi_str = list.first->next->next->next->string; - String8 data4_lo_str = list.first->next->next->next->next->string; - if(str8_is_integer(data1_str, 16) && - str8_is_integer(data2_str, 16) && - str8_is_integer(data3_str, 16) && - str8_is_integer(data4_hi_str, 16) && - str8_is_integer(data4_lo_str, 16)) - { - U64 data1 = u64_from_str8(data1_str, 16); - U64 data2 = u64_from_str8(data2_str, 16); - U64 data3 = u64_from_str8(data3_str, 16); - U64 data4_hi = u64_from_str8(data4_hi_str, 16); - U64 data4_lo = u64_from_str8(data4_lo_str, 16); - if(data1 <= max_U32 && - data2 <= max_U16 && - data3 <= max_U16 && - data4_hi <= max_U16 && - data4_lo <= 0xffffffffffff) - { - guid_out->data1 = (U32)data1; - guid_out->data2 = (U16)data2; - guid_out->data3 = (U16)data3; - U64 data4 = (data4_hi << 48) | data4_lo; - MemoryCopy(&guid_out->data4[0], &data4, sizeof(data4)); - is_parsed = 1; - } - } - } - scratch_end(scratch); - return is_parsed; -} - -internal Guid -guid_from_string(String8 string) -{ - Guid guid = {0}; - try_guid_from_string(string, &guid); - return guid; -} - -//////////////////////////////// -//~ rjf: Basic Text Indentation - -internal String8 -indented_from_string(Arena *arena, String8 string) -{ - Temp scratch = scratch_begin(&arena, 1); - read_only local_persist U8 indentation_bytes[] = " "; - String8List indented_strings = {0}; - S64 depth = 0; - S64 next_depth = 0; - U64 line_begin_off = 0; - for(U64 off = 0; off <= string.size; off += 1) - { - U8 byte = off width_this_line){ - String8 line = str8_substr(string, line_range); - if (wrapped_indent_level > 0){ - line = push_str8f(arena, "%.*s%S", wrapped_indent_level, spaces, line); - } - str8_list_push(arena, &list, line); - line_range = r1u64(line_range.max+1, candidate_line_range.max); - wrapped_indent_level = ClampTop(64, wrap_indent); - } - else{ - line_range = candidate_line_range; - } - } - } - if (line_range.min < string.size && line_range.max > line_range.min){ - String8 line = str8_substr(string, line_range); - if (wrapped_indent_level > 0){ - line = push_str8f(arena, "%.*s%S", wrapped_indent_level, spaces, line); - } - str8_list_push(arena, &list, line); - } - return list; -} - -//////////////////////////////// -//~ rjf: String <-> Color - -internal String8 -hex_string_from_rgba_4f32(Arena *arena, Vec4F32 rgba) -{ - String8 hex_string = push_str8f(arena, "%02x%02x%02x%02x", (U8)(rgba.x*255.f), (U8)(rgba.y*255.f), (U8)(rgba.z*255.f), (U8)(rgba.w*255.f)); - return hex_string; -} - -internal Vec4F32 -rgba_from_hex_string_4f32(String8 hex_string) -{ - U8 byte_text[8] = {0}; - U64 byte_text_idx = 0; - for(U64 idx = 0; idx < hex_string.size && byte_text_idx < ArrayCount(byte_text); idx += 1) - { - if(char_is_digit(hex_string.str[idx], 16)) - { - byte_text[byte_text_idx] = char_to_lower(hex_string.str[idx]); - byte_text_idx += 1; - } - } - U8 byte_vals[4] = {0}; - for(U64 idx = 0; idx < 4; idx += 1) - { - byte_vals[idx] = (U8)u64_from_str8(str8(&byte_text[idx*2], 2), 16); - } - Vec4F32 rgba = v4f32(byte_vals[0]/255.f, byte_vals[1]/255.f, byte_vals[2]/255.f, byte_vals[3]/255.f); - return rgba; -} - -//////////////////////////////// -//~ rjf: String Fuzzy Matching - -internal FuzzyMatchRangeList -fuzzy_match_find(Arena *arena, String8 needle, String8 haystack) -{ - FuzzyMatchRangeList result = {0}; - Temp scratch = scratch_begin(&arena, 1); - String8List needles = str8_split(scratch.arena, needle, (U8*)" ", 1, 0); - result.needle_part_count = needles.node_count; - for(String8Node *needle_n = needles.first; needle_n != 0; needle_n = needle_n->next) - { - U64 find_pos = 0; - for(;find_pos < haystack.size;) - { - find_pos = str8_find_needle(haystack, find_pos, needle_n->string, StringMatchFlag_CaseInsensitive); - B32 is_in_gathered_ranges = 0; - for(FuzzyMatchRangeNode *n = result.first; n != 0; n = n->next) - { - if(n->range.min <= find_pos && find_pos < n->range.max) - { - is_in_gathered_ranges = 1; - find_pos = n->range.max; - break; - } - } - if(!is_in_gathered_ranges) - { - break; - } - } - if(find_pos < haystack.size) - { - Rng1U64 range = r1u64(find_pos, find_pos+needle_n->string.size); - FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1); - n->range = range; - SLLQueuePush(result.first, result.last, n); - result.count += 1; - result.total_dim += dim_1u64(range); - } - } - scratch_end(scratch); - return result; -} - -internal FuzzyMatchRangeList -fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src) -{ - FuzzyMatchRangeList dst = {0}; - for(FuzzyMatchRangeNode *src_n = src->first; src_n != 0; src_n = src_n->next) - { - FuzzyMatchRangeNode *dst_n = push_array(arena, FuzzyMatchRangeNode, 1); - SLLQueuePush(dst.first, dst.last, dst_n); - dst_n->range = src_n->range; - } - dst.count = src->count; - dst.needle_part_count = src->needle_part_count; - dst.total_dim = src->total_dim; - return dst; -} - -//////////////////////////////// -//~ NOTE(allen): Serialization Helpers - -internal void -str8_serial_begin(Arena *arena, String8List *srl){ - String8Node *node = push_array(arena, String8Node, 1); - node->string.str = push_array_no_zero(arena, U8, 0); - srl->first = srl->last = node; - srl->node_count = 1; - srl->total_size = 0; -} - -internal String8 -str8_serial_end(Arena *arena, String8List *srl){ - U64 size = srl->total_size; - U8 *out = push_array_no_zero(arena, U8, size); - str8_serial_write_to_dst(srl, out); - String8 result = str8(out, size); - return result; -} - -internal void -str8_serial_write_to_dst(String8List *srl, void *out){ - U8 *ptr = (U8*)out; - for (String8Node *node = srl->first; - node != 0; - node = node->next){ - U64 size = node->string.size; - MemoryCopy(ptr, node->string.str, size); - ptr += size; - } -} - -internal U64 -str8_serial_push_align(Arena *arena, String8List *srl, U64 align){ - Assert(IsPow2(align)); - - U64 pos = srl->total_size; - U64 new_pos = AlignPow2(pos, align); - U64 size = (new_pos - pos); - - if(size != 0) - { - U8 *buf = push_array(arena, U8, size); - - String8 *str = &srl->last->string; - if (str->str + str->size == buf){ - srl->last->string.size += size; - srl->total_size += size; - } - else{ - str8_list_push(arena, srl, str8(buf, size)); - } - } - return size; -} - -internal void * -str8_serial_push_size(Arena *arena, String8List *srl, U64 size) -{ - void *result = 0; - if(size != 0) - { - U8 *buf = push_array_no_zero(arena, U8, size); - String8 *str = &srl->last->string; - if (str->str + str->size == buf){ - srl->last->string.size += size; - srl->total_size += size; - } - else{ - str8_list_push(arena, srl, str8(buf, size)); - } - result = buf; - } - return result; -} - -internal void * -str8_serial_push_data(Arena *arena, String8List *srl, void *data, U64 size){ - void *result = str8_serial_push_size(arena, srl, size); - if(result != 0) - { - MemoryCopy(result, data, size); - } - return result; -} - -internal void -str8_serial_push_data_list(Arena *arena, String8List *srl, String8Node *first){ - for (String8Node *node = first; - node != 0; - node = node->next){ - str8_serial_push_data(arena, srl, node->string.str, node->string.size); - } -} - -internal void -str8_serial_push_u64(Arena *arena, String8List *srl, U64 x){ - U8 *buf = push_array_no_zero(arena, U8, 8); - MemoryCopy(buf, &x, 8); - String8 *str = &srl->last->string; - if (str->str + str->size == buf){ - srl->last->string.size += 8; - srl->total_size += 8; - } - else{ - str8_list_push(arena, srl, str8(buf, 8)); - } -} - -internal void -str8_serial_push_u32(Arena *arena, String8List *srl, U32 x){ - U8 *buf = push_array_no_zero(arena, U8, 4); - MemoryCopy(buf, &x, 4); - String8 *str = &srl->last->string; - if (str->str + str->size == buf){ - srl->last->string.size += 4; - srl->total_size += 4; - } - else{ - str8_list_push(arena, srl, str8(buf, 4)); - } -} - -internal void -str8_serial_push_u16(Arena *arena, String8List *srl, U16 x){ - str8_serial_push_data(arena, srl, &x, sizeof(x)); -} - -internal void -str8_serial_push_u8(Arena *arena, String8List *srl, U8 x){ - str8_serial_push_data(arena, srl, &x, sizeof(x)); -} - -internal void -str8_serial_push_cstr(Arena *arena, String8List *srl, String8 str){ - str8_serial_push_data(arena, srl, str.str, str.size); - str8_serial_push_u8(arena, srl, 0); -} - -internal void -str8_serial_push_string(Arena *arena, String8List *srl, String8 str){ - str8_serial_push_data(arena, srl, str.str, str.size); -} - -//////////////////////////////// -//~ rjf: Deserialization Helpers - -internal U64 -str8_deserial_read(String8 string, U64 off, void *read_dst, U64 read_size, U64 granularity) -{ - U64 bytes_left = string.size-Min(off, string.size); - U64 actually_readable_size = Min(bytes_left, read_size); - U64 legally_readable_size = actually_readable_size - actually_readable_size%granularity; - if(legally_readable_size > 0) - { - MemoryCopy(read_dst, string.str+off, legally_readable_size); - } - return legally_readable_size; -} - -internal U64 -str8_deserial_find_first_match(String8 string, U64 off, U16 scan_val) -{ - U64 cursor = off; - for (;;) { - U16 val = 0; - str8_deserial_read_struct(string, cursor, &val); - if (val == scan_val) { - break; - } - cursor += sizeof(val); - } - return cursor; -} - -internal void * -str8_deserial_get_raw_ptr(String8 string, U64 off, U64 size) -{ - void *raw_ptr = 0; - if (off + size <= string.size) { - raw_ptr = string.str + off; - } - return raw_ptr; -} - -internal U64 -str8_deserial_read_cstr(String8 string, U64 off, String8 *cstr_out) -{ - U64 cstr_size = 0; - if (off < string.size) { - U8 *ptr = string.str + off; - U8 *cap = string.str + string.size; - *cstr_out = str8_cstring_capped(ptr, cap); - cstr_size = (cstr_out->size + 1); - } - return cstr_size; -} - -internal U64 -str8_deserial_read_windows_utf16_string16(String8 string, U64 off, String16 *str_out) -{ - U64 null_off = str8_deserial_find_first_match(string, off, 0); - U64 size = null_off - off; - U16 *str = (U16 *)str8_deserial_get_raw_ptr(string, off, size); - U64 count = size / sizeof(*str); - *str_out = str16(str, count); - - U64 read_size_with_null = size + sizeof(*str); - return read_size_with_null; -} - -internal U64 -str8_deserial_read_block(String8 string, U64 off, U64 size, String8 *block_out) -{ - Rng1U64 range = rng_1u64(off, off + size); - *block_out = str8_substr(string, range); - return block_out->size; -} - -internal U64 -str8_deserial_read_uleb128(String8 string, U64 off, U64 *value_out) -{ - U64 value = 0; - U64 shift = 0; - U64 cursor = off; - for(;;) - { - U8 byte = 0; - U64 bytes_read = str8_deserial_read_struct(string, cursor, &byte); - - if(bytes_read != sizeof(byte)) - { - break; - } - - U8 val = byte & 0x7fu; - value |= ((U64)val) << shift; - - cursor += bytes_read; - shift += 7u; - - if((byte & 0x80u) == 0) - { - break; - } - } - if(value_out != 0) - { - *value_out = value; - } - U64 bytes_read = cursor - off; - return bytes_read; -} - -internal U64 -str8_deserial_read_sleb128(String8 string, U64 off, S64 *value_out) -{ - U64 value = 0; - U64 shift = 0; - U64 cursor = off; - for(;;) - { - U8 byte; - U64 bytes_read = str8_deserial_read_struct(string, cursor, &byte); - if(bytes_read != sizeof(byte)) - { - break; - } - - U8 val = byte & 0x7fu; - value |= ((U64)val) << shift; - - cursor += bytes_read; - shift += 7u; - - if((byte & 0x80u) == 0) - { - if(shift < sizeof(value) * 8 && (byte & 0x40u) != 0) - { - value |= -(S64)(1ull << shift); - } - break; - } - } - if(value_out != 0) - { - *value_out = value; - } - U64 bytes_read = cursor - off; - return bytes_read; -} - +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//////////////////////////////// +//~ rjf: Third Party Includes + +#if !BUILD_SUPPLEMENTARY_UNIT +# define STB_SPRINTF_IMPLEMENTATION +# define STB_SPRINTF_STATIC +# include "third_party/stb/stb_sprintf.h" +#endif + +//////////////////////////////// +//~ NOTE(allen): String <-> Integer Tables + +read_only global U8 integer_symbols[16] = { + '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F', +}; + +// NOTE(allen): Includes reverses for uppercase and lowercase hex. +read_only global U8 integer_symbol_reverse[128] = { + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, +}; + +read_only global U8 base64[64] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '_', '$', +}; + +read_only global U8 base64_reverse[128] = { + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFF,0xFF,0xFF,0xFF,0xFF,0x00, + 0xFF,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32, + 0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0xFF,0xFF,0xFF,0xFF,0x3E, + 0xFF,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18, + 0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0xFF,0xFF,0xFF,0xFF,0xFF, +}; + +//////////////////////////////// +//~ rjf: Character Classification & Conversion Functions + +internal B32 +char_is_space(U8 c){ + return(c == ' ' || c == '\n' || c == '\t' || c == '\r' || c == '\f' || c == '\v'); +} + +internal B32 +char_is_upper(U8 c){ + return('A' <= c && c <= 'Z'); +} + +internal B32 +char_is_lower(U8 c){ + return('a' <= c && c <= 'z'); +} + +internal B32 +char_is_alpha(U8 c){ + return(char_is_upper(c) || char_is_lower(c)); +} + +internal B32 +char_is_slash(U8 c){ + return(c == '/' || c == '\\'); +} + +internal B32 +char_is_digit(U8 c, U32 base){ + B32 result = 0; + if (0 < base && base <= 16){ + U8 val = integer_symbol_reverse[c]; + if (val < base){ + result = 1; + } + } + return(result); +} + +internal U8 +char_to_lower(U8 c){ + if (char_is_upper(c)){ + c += ('a' - 'A'); + } + return(c); +} + +internal U8 +char_to_upper(U8 c){ + if (char_is_lower(c)){ + c += ('A' - 'a'); + } + return(c); +} + +internal U8 +char_to_correct_slash(U8 c){ + if(char_is_slash(c)){ + c = '/'; + } + return(c); +} + +//////////////////////////////// +//~ rjf: C-String Measurement + +internal U64 +cstring8_length(U8 *c){ + U8 *p = c; + for (;*p != 0; p += 1); + return(p - c); +} + +internal U64 +cstring16_length(U16 *c){ + U16 *p = c; + for (;*p != 0; p += 1); + return(p - c); +} + +internal U64 +cstring32_length(U32 *c){ + U32 *p = c; + for (;*p != 0; p += 1); + return(p - c); +} + +//////////////////////////////// +//~ rjf: String Constructors + +internal String8 +str8(U8 *str, U64 size){ + String8 result = {str, size}; + return(result); +} + +internal String8 +str8_range(U8 *first, U8 *one_past_last){ + String8 result = {first, (U64)(one_past_last - first)}; + return(result); +} + +internal String8 +str8_zero(void){ + String8 result = {0}; + return(result); +} + +internal String16 +str16(U16 *str, U64 size){ + String16 result = {str, size}; + return(result); +} + +internal String16 +str16_range(U16 *first, U16 *one_past_last){ + String16 result = {first, (U64)(one_past_last - first)}; + return(result); +} + +internal String16 +str16_zero(void){ + String16 result = {0}; + return(result); +} + +internal String32 +str32(U32 *str, U64 size){ + String32 result = {str, size}; + return(result); +} + +internal String32 +str32_range(U32 *first, U32 *one_past_last){ + String32 result = {first, (U64)(one_past_last - first)}; + return(result); +} + +internal String32 +str32_zero(void){ + String32 result = {0}; + return(result); +} + +internal String8 +str8_cstring(char *c){ + String8 result = {(U8*)c, cstring8_length((U8*)c)}; + return(result); +} + +internal String16 +str16_cstring(U16 *c){ + String16 result = {(U16*)c, cstring16_length((U16*)c)}; + return(result); +} + +internal String32 +str32_cstring(U32 *c){ + String32 result = {(U32*)c, cstring32_length((U32*)c)}; + return(result); +} + +internal String8 +str8_cstring_capped(void *cstr, void *cap) +{ + char *ptr = (char *)cstr; + char *opl = (char *)cap; + for (;ptr < opl && *ptr != 0; ptr += 1); + U64 size = (U64)(ptr - (char *)cstr); + String8 result = str8((U8*)cstr, size); + return result; +} + +internal String16 +str16_cstring_capped(void *cstr, void *cap) +{ + U16 *ptr = (U16 *)cstr; + U16 *opl = (U16 *)cap; + for (;ptr < opl && *ptr != 0; ptr += 1); + U64 size = (U64)(ptr - (U16 *)cstr); + String16 result = str16(cstr, size); + return result; +} + +internal String8 +str8_cstring_capped_reverse(void *raw_start, void *raw_cap) +{ + U8 *start = raw_start; + U8 *ptr = raw_cap; + for(; ptr > start; ) + { + ptr -= 1; + + if (*ptr == '\0') + { + break; + } + } + U64 size = (U64)(ptr - start); + String8 result = str8(start, size); + return result; +} + +//////////////////////////////// +//~ rjf: String Stylization + +internal String8 +upper_from_str8(Arena *arena, String8 string) +{ + string = push_str8_copy(arena, string); + for(U64 idx = 0; idx < string.size; idx += 1) + { + string.str[idx] = char_to_upper(string.str[idx]); + } + return string; +} + +internal String8 +lower_from_str8(Arena *arena, String8 string) +{ + string = push_str8_copy(arena, string); + for(U64 idx = 0; idx < string.size; idx += 1) + { + string.str[idx] = char_to_lower(string.str[idx]); + } + return string; +} + +internal String8 +backslashed_from_str8(Arena *arena, String8 string) +{ + string = push_str8_copy(arena, string); + for(U64 idx = 0; idx < string.size; idx += 1) + { + string.str[idx] = char_is_slash(string.str[idx]) ? '\\' : string.str[idx]; + } + return string; +} + +//////////////////////////////// +//~ rjf: String Matching + +internal B32 +str8_match(String8 a, String8 b, StringMatchFlags flags) +{ + B32 result = 0; + if(a.size == b.size && flags == 0) + { + result = MemoryMatch(a.str, b.str, b.size); + } + else if(a.size == b.size || (flags & StringMatchFlag_RightSideSloppy)) + { + B32 case_insensitive = (flags & StringMatchFlag_CaseInsensitive); + B32 slash_insensitive = (flags & StringMatchFlag_SlashInsensitive); + U64 size = Min(a.size, b.size); + result = 1; + for(U64 i = 0; i < size; i += 1) + { + U8 at = a.str[i]; + U8 bt = b.str[i]; + if(case_insensitive) + { + at = char_to_upper(at); + bt = char_to_upper(bt); + } + if(slash_insensitive) + { + at = char_to_correct_slash(at); + bt = char_to_correct_slash(bt); + } + if(at != bt) + { + result = 0; + break; + } + } + } + return result; +} + +internal U64 +str8_find_needle(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags){ + U8 *p = string.str + start_pos; + U64 stop_offset = Max(string.size + 1, needle.size) - needle.size; + U8 *stop_p = string.str + stop_offset; + if (needle.size > 0){ + U8 *string_opl = string.str + string.size; + String8 needle_tail = str8_skip(needle, 1); + StringMatchFlags adjusted_flags = flags | StringMatchFlag_RightSideSloppy; + U8 needle_first_char_adjusted = needle.str[0]; + if(adjusted_flags & StringMatchFlag_CaseInsensitive){ + needle_first_char_adjusted = char_to_upper(needle_first_char_adjusted); + } + for (;p < stop_p; p += 1){ + U8 haystack_char_adjusted = *p; + if(adjusted_flags & StringMatchFlag_CaseInsensitive){ + haystack_char_adjusted = char_to_upper(haystack_char_adjusted); + } + if (haystack_char_adjusted == needle_first_char_adjusted){ + if (str8_match(str8_range(p + 1, string_opl), needle_tail, adjusted_flags)){ + break; + } + } + } + } + U64 result = string.size; + if (p < stop_p){ + result = (U64)(p - string.str); + } + return(result); +} + +internal U64 +str8_find_needle_reverse(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags) +{ + U64 result = 0; + for(S64 i = string.size - start_pos - needle.size; i >= 0; --i) + { + String8 haystack = str8_substr(string, rng_1u64(i, i + needle.size)); + if(str8_match(haystack, needle, flags)) + { + result = (U64)i + needle.size; + break; + } + } + return result; +} + +internal B32 +str8_starts_with(String8 string, String8 start, StringMatchFlags flags){ + if(string.size < start.size) { + return 0; + } + String8 prefix = str8_prefix(string, start.size); + return str8_match(prefix, start, flags); +} + +internal B32 +str8_ends_with(String8 string, String8 end, StringMatchFlags flags){ + String8 postfix = str8_postfix(string, end.size); + B32 is_match = str8_match(end, postfix, flags); + return is_match; +} + +//////////////////////////////// +//~ rjf: String Slicing + +internal String8 +str8_substr(String8 str, Rng1U64 range){ + range.min = ClampTop(range.min, str.size); + range.max = ClampTop(range.max, str.size); + str.str += range.min; + str.size = dim_1u64(range); + return(str); +} + +internal String8 +str8_prefix(String8 str, U64 size){ + str.size = ClampTop(size, str.size); + return(str); +} + +internal String8 +str8_skip(String8 str, U64 amt){ + amt = ClampTop(amt, str.size); + str.str += amt; + str.size -= amt; + return(str); +} + +internal String8 +str8_postfix(String8 str, U64 size){ + size = ClampTop(size, str.size); + str.str = (str.str + str.size) - size; + str.size = size; + return(str); +} + +internal String8 +str8_chop(String8 str, U64 amt){ + amt = ClampTop(amt, str.size); + str.size -= amt; + return(str); +} + +internal String8 +str8_skip_chop_whitespace(String8 string){ + U8 *first = string.str; + U8 *opl = first + string.size; + for (;first < opl; first += 1){ + if (!char_is_space(*first)){ + break; + } + } + for (;opl > first;){ + opl -= 1; + if (!char_is_space(*opl)){ + opl += 1; + break; + } + } + String8 result = str8_range(first, opl); + return(result); +} + +//////////////////////////////// +//~ rjf: String Formatting & Copying + +internal String8 +push_str8_cat(Arena *arena, String8 s1, String8 s2){ + String8 str; + str.size = s1.size + s2.size; + str.str = push_array_no_zero(arena, U8, str.size + 1); + MemoryCopy(str.str, s1.str, s1.size); + MemoryCopy(str.str + s1.size, s2.str, s2.size); + str.str[str.size] = 0; + return(str); +} + +internal String8 +push_str8_copy(Arena *arena, String8 s){ + String8 str; + str.size = s.size; + str.str = push_array_no_zero(arena, U8, str.size + 1); + MemoryCopy(str.str, s.str, s.size); + str.str[str.size] = 0; + return(str); +} + +internal String8 +push_str8fv(Arena *arena, char *fmt, va_list args){ + va_list args2; + va_copy(args2, args); + U32 needed_bytes = raddbg_vsnprintf(0, 0, fmt, args) + 1; + String8 result = {0}; + result.str = push_array_no_zero(arena, U8, needed_bytes); + result.size = raddbg_vsnprintf((char*)result.str, needed_bytes, fmt, args2); + result.str[result.size] = 0; + va_end(args2); + return(result); +} + +internal String8 +push_str8f(Arena *arena, char *fmt, ...){ + va_list args; + va_start(args, fmt); + String8 result = push_str8fv(arena, fmt, args); + va_end(args); + return(result); +} + +//////////////////////////////// +//~ rjf: String <=> Integer Conversions + +//- rjf: string -> integer + +internal S64 +sign_from_str8(String8 string, String8 *string_tail){ + // count negative signs + U64 neg_count = 0; + U64 i = 0; + for (; i < string.size; i += 1){ + if (string.str[i] == '-'){ + neg_count += 1; + } + else if (string.str[i] != '+'){ + break; + } + } + + // output part of string after signs + *string_tail = str8_skip(string, i); + + // output integer sign + S64 sign = (neg_count & 1)?-1:+1; + return(sign); +} + +internal B32 +str8_is_integer(String8 string, U32 radix){ + B32 result = 0; + if (string.size > 0){ + if (1 < radix && radix <= 16){ + result = 1; + for (U64 i = 0; i < string.size; i += 1){ + U8 c = string.str[i]; + if (!(c < 0x80) || integer_symbol_reverse[c] >= radix){ + result = 0; + break; + } + } + } + } + return(result); +} + +internal U64 +u64_from_str8(String8 string, U32 radix){ + U64 x = 0; + if (1 < radix && radix <= 16){ + for (U64 i = 0; i < string.size; i += 1){ + x *= radix; + x += integer_symbol_reverse[string.str[i]&0x7F]; + } + } + return(x); +} + +internal S64 +s64_from_str8(String8 string, U32 radix){ + S64 sign = sign_from_str8(string, &string); + S64 x = (S64)u64_from_str8(string, radix) * sign; + return(x); +} + +internal U32 +u32_from_str8(String8 string, U32 radix) +{ + U64 x64 = u64_from_str8(string, radix); + U32 x32 = safe_cast_u32(x64); + return x32; +} + +internal S32 +s32_from_str8(String8 string, U32 radix) +{ + S64 x64 = s64_from_str8(string, radix); + S32 x32 = safe_cast_s32(x64); + return x32; +} + +internal B32 +try_u64_from_str8_c_rules(String8 string, U64 *x){ + B32 is_integer = 0; + if (str8_is_integer(string, 10)){ + is_integer = 1; + *x = u64_from_str8(string, 10); + } + else{ + String8 hex_string = str8_skip(string, 2); + if (str8_match(str8_prefix(string, 2), str8_lit("0x"), 0) && + str8_is_integer(hex_string, 0x10)){ + is_integer = 1; + *x = u64_from_str8(hex_string, 0x10); + } + else if (str8_match(str8_prefix(string, 2), str8_lit("0b"), 0) && + str8_is_integer(hex_string, 2)){ + is_integer = 1; + *x = u64_from_str8(hex_string, 2); + } + else{ + String8 oct_string = str8_skip(string, 1); + if (str8_match(str8_prefix(string, 1), str8_lit("0"), 0) && + str8_is_integer(hex_string, 010)){ + is_integer = 1; + *x = u64_from_str8(oct_string, 010); + } + } + } + return(is_integer); +} + +internal B32 +try_s64_from_str8_c_rules(String8 string, S64 *x){ + String8 string_tail = {0}; + S64 sign = sign_from_str8(string, &string_tail); + U64 x_u64 = 0; + B32 is_integer = try_u64_from_str8_c_rules(string_tail, &x_u64); + *x = x_u64*sign; + return(is_integer); +} + +//- rjf: integer -> string + +internal String8 +str8_from_memory_size(Arena *arena, U64 size) +{ + String8 result; + + if(size < KB(1)) + { + result = push_str8f(arena, "%llu Bytes", size); + } + else if(size < MB(1)) + { + result = push_str8f(arena, "%llu.%02llu KiB", size / KB(1), ((size * 100) / KB(1)) % 100); + } + else if(size < GB(1)) + { + result = push_str8f(arena, "%llu.%02llu MiB", size / MB(1), ((size * 100) / MB(1)) % 100); + } + else if(size < TB(1)) + { + result = push_str8f(arena, "%llu.%02llu GiB", size / GB(1), ((size * 100) / GB(1)) % 100); + } + else + { + result = push_str8f(arena, "%llu.%02llu TiB", size / TB(1), ((size * 100) / TB(1)) % 100); + } + + return result; +} + +internal String8 +str8_from_count(Arena *arena, U64 count) +{ + String8 result; + + if(count < 1 * 1000) + { + result = push_str8f(arena, "%llu", count); + } + else if(count < 1000000) + { + U64 frac = ((count * 100) / 1000) % 100; + if(frac > 0) + { + result = push_str8f(arena, "%llu.%02lluK", count / 1000, frac); + } + else + { + result = push_str8f(arena, "%lluK", count / 1000); + } + } + else if(count < 1000000000) + { + U64 frac = ((count * 100) / 1000000) % 100; + if(frac > 0) + { + result = push_str8f(arena, "%llu.%02lluM", count / 1000000, frac); + } + else + { + result = push_str8f(arena, "%lluM", count / 1000000); + } + } + else + { + U64 frac = ((count * 100) * 1000000000) % 100; + if(frac > 0) + { + result = push_str8f(arena, "%llu.%02lluB", count / 1000000000, frac); + } + else + { + result = push_str8f(arena, "%lluB", count / 1000000000, frac); + } + } + + return result; +} + +internal String8 +str8_from_bits_u32(Arena *arena, U32 x) +{ + U8 c0 = 'a' + ((x >> 28) & 0xf); + U8 c1 = 'a' + ((x >> 24) & 0xf); + U8 c2 = 'a' + ((x >> 20) & 0xf); + U8 c3 = 'a' + ((x >> 16) & 0xf); + U8 c4 = 'a' + ((x >> 12) & 0xf); + U8 c5 = 'a' + ((x >> 8) & 0xf); + U8 c6 = 'a' + ((x >> 4) & 0xf); + U8 c7 = 'a' + ((x >> 0) & 0xf); + String8 result = push_str8f(arena, "%c%c%c%c%c%c%c%c", c0, c1, c2, c3, c4, c5, c6, c7); + return result; +} + +internal String8 +str8_from_bits_u64(Arena *arena, U64 x) +{ + U8 c0 = 'a' + ((x >> 60) & 0xf); + U8 c1 = 'a' + ((x >> 56) & 0xf); + U8 c2 = 'a' + ((x >> 52) & 0xf); + U8 c3 = 'a' + ((x >> 48) & 0xf); + U8 c4 = 'a' + ((x >> 44) & 0xf); + U8 c5 = 'a' + ((x >> 40) & 0xf); + U8 c6 = 'a' + ((x >> 36) & 0xf); + U8 c7 = 'a' + ((x >> 32) & 0xf); + U8 c8 = 'a' + ((x >> 28) & 0xf); + U8 c9 = 'a' + ((x >> 24) & 0xf); + U8 ca = 'a' + ((x >> 20) & 0xf); + U8 cb = 'a' + ((x >> 16) & 0xf); + U8 cc = 'a' + ((x >> 12) & 0xf); + U8 cd = 'a' + ((x >> 8) & 0xf); + U8 ce = 'a' + ((x >> 4) & 0xf); + U8 cf = 'a' + ((x >> 0) & 0xf); + String8 result = push_str8f(arena, + "%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c", + c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf); + return result; +} + +internal String8 +str8_from_u64(Arena *arena, U64 u64, U32 radix, U8 min_digits, U8 digit_group_separator) +{ + String8 result = {0}; + { + // rjf: prefix + String8 prefix = {0}; + switch(radix) + { + case 16:{prefix = str8_lit("0x");}break; + case 8: {prefix = str8_lit("0o");}break; + case 2: {prefix = str8_lit("0b");}break; + } + + // rjf: determine # of chars between separators + U8 digit_group_size = 3; + switch(radix) + { + default:break; + case 2: + case 8: + case 16: + {digit_group_size = 4;}break; + } + + // rjf: prep + U64 needed_leading_0s = 0; + { + U64 needed_digits = 1; + { + U64 u64_reduce = u64; + for(;;) + { + u64_reduce /= radix; + if(u64_reduce == 0) + { + break; + } + needed_digits += 1; + } + } + needed_leading_0s = (min_digits > needed_digits) ? min_digits - needed_digits : 0; + U64 needed_separators = 0; + if(digit_group_separator != 0) + { + needed_separators = (needed_digits+needed_leading_0s)/digit_group_size; + if(needed_separators > 0 && (needed_digits+needed_leading_0s)%digit_group_size == 0) + { + needed_separators -= 1; + } + } + result.size = prefix.size + needed_leading_0s + needed_separators + needed_digits; + result.str = push_array_no_zero(arena, U8, result.size + 1); + result.str[result.size] = 0; + } + + // rjf: fill contents + { + U64 u64_reduce = u64; + U64 digits_until_separator = digit_group_size; + for(U64 idx = 0; idx < result.size; idx += 1) + { + if(digits_until_separator == 0 && digit_group_separator != 0) + { + result.str[result.size - idx - 1] = digit_group_separator; + digits_until_separator = digit_group_size+1; + } + else + { + result.str[result.size - idx - 1] = char_to_lower(integer_symbols[u64_reduce%radix]); + u64_reduce /= radix; + } + digits_until_separator -= 1; + if(u64_reduce == 0) + { + break; + } + } + for(U64 leading_0_idx = 0; leading_0_idx < needed_leading_0s; leading_0_idx += 1) + { + result.str[prefix.size + leading_0_idx] = '0'; + } + } + + // rjf: fill prefix + if(prefix.size != 0) + { + MemoryCopy(result.str, prefix.str, prefix.size); + } + } + return result; +} + +internal String8 +str8_from_s64(Arena *arena, S64 s64, U32 radix, U8 min_digits, U8 digit_group_separator) +{ + String8 result = {0}; + // TODO(rjf): preeeeetty sloppy... + if(s64 < 0) + { + Temp scratch = scratch_begin(&arena, 1); + String8 numeric_part = str8_from_u64(scratch.arena, (U64)(-s64), radix, min_digits, digit_group_separator); + result = push_str8f(arena, "-%S", numeric_part); + scratch_end(scratch); + } + else + { + result = str8_from_u64(arena, (U64)s64, radix, min_digits, digit_group_separator); + } + return result; +} + +//////////////////////////////// +//~ rjf: String <=> Float Conversions + +internal F64 +f64_from_str8(String8 string) +{ + // TODO(rjf): crappy implementation for now that just uses atof. + F64 result = 0; + if(string.size > 0) + { + // rjf: find starting pos of numeric string, as well as sign + F64 sign = +1.0; + if(string.str[0] == '-') + { + sign = -1.0; + } + else if(string.str[0] == '+') + { + sign = 1.0; + } + + // rjf: gather numerics + U64 num_valid_chars = 0; + char buffer[64]; + B32 exp = 0; + for(U64 idx = 0; idx < string.size && num_valid_chars < sizeof(buffer)-1; idx += 1) + { + if(char_is_digit(string.str[idx], 10) || string.str[idx] == '.' || string.str[idx] == 'e' || + (exp && (string.str[idx] == '+' || string.str[idx] == '-'))) + { + buffer[num_valid_chars] = string.str[idx]; + num_valid_chars += 1; + exp = 0; + exp = (string.str[idx] == 'e'); + } + } + + // rjf: null-terminate (the reason for all of this!!!!!!) + buffer[num_valid_chars] = 0; + + // rjf: do final conversion + result = sign * atof(buffer); + } + return result; +} + +//////////////////////////////// +//~ rjf: String List Construction Functions + +internal String8Node* +str8_list_push_node(String8List *list, String8Node *node){ + SLLQueuePush(list->first, list->last, node); + list->node_count += 1; + list->total_size += node->string.size; + return(node); +} + +internal String8Node* +str8_list_push_node_set_string(String8List *list, String8Node *node, String8 string){ + SLLQueuePush(list->first, list->last, node); + list->node_count += 1; + list->total_size += string.size; + node->string = string; + return(node); +} + +internal String8Node* +str8_list_push_node_front(String8List *list, String8Node *node){ + SLLQueuePushFront(list->first, list->last, node); + list->node_count += 1; + list->total_size += node->string.size; + return(node); +} + +internal String8Node* +str8_list_push_node_front_set_string(String8List *list, String8Node *node, String8 string){ + SLLQueuePushFront(list->first, list->last, node); + list->node_count += 1; + list->total_size += string.size; + node->string = string; + return(node); +} + +internal String8Node* +str8_list_push(Arena *arena, String8List *list, String8 string){ + String8Node *node = push_array_no_zero(arena, String8Node, 1); + str8_list_push_node_set_string(list, node, string); + return(node); +} + +internal String8Node* +str8_list_push_front(Arena *arena, String8List *list, String8 string){ + String8Node *node = push_array_no_zero(arena, String8Node, 1); + str8_list_push_node_front_set_string(list, node, string); + return(node); +} + +internal void +str8_list_concat_in_place(String8List *list, String8List *to_push){ + if(to_push->node_count != 0){ + if (list->last){ + list->node_count += to_push->node_count; + list->total_size += to_push->total_size; + list->last->next = to_push->first; + list->last = to_push->last; + } + else{ + *list = *to_push; + } + MemoryZeroStruct(to_push); + } +} + +internal String8Node* +str8_list_push_aligner(Arena *arena, String8List *list, U64 min, U64 align){ + String8Node *node = push_array_no_zero(arena, String8Node, 1); + U64 new_size = list->total_size + min; + U64 increase_size = 0; + if (align > 1){ + // NOTE(allen): assert is power of 2 + Assert(((align - 1) & align) == 0); + U64 mask = align - 1; + new_size += mask; + new_size &= (~mask); + increase_size = new_size - list->total_size; + } + local_persist const U8 zeroes_buffer[64] = {0}; + Assert(increase_size <= ArrayCount(zeroes_buffer)); + SLLQueuePush(list->first, list->last, node); + list->node_count += 1; + list->total_size = new_size; + node->string.str = (U8*)zeroes_buffer; + node->string.size = increase_size; + return(node); +} + +internal String8Node* +str8_list_pushf(Arena *arena, String8List *list, char *fmt, ...){ + va_list args; + va_start(args, fmt); + String8 string = push_str8fv(arena, fmt, args); + String8Node *result = str8_list_push(arena, list, string); + va_end(args); + return(result); +} + +internal String8Node* +str8_list_push_frontf(Arena *arena, String8List *list, char *fmt, ...){ + va_list args; + va_start(args, fmt); + String8 string = push_str8fv(arena, fmt, args); + String8Node *result = str8_list_push_front(arena, list, string); + va_end(args); + return(result); +} + +internal String8List +str8_list_copy(Arena *arena, String8List *list){ + String8List result = {0}; + for (String8Node *node = list->first; + node != 0; + node = node->next){ + String8Node *new_node = push_array_no_zero(arena, String8Node, 1); + String8 new_string = push_str8_copy(arena, node->string); + str8_list_push_node_set_string(&result, new_node, new_string); + } + return(result); +} + +internal String8List +str8_split(Arena *arena, String8 string, U8 *split_chars, U64 split_char_count, StringSplitFlags flags){ + String8List list = {0}; + + B32 keep_empties = (flags & StringSplitFlag_KeepEmpties); + + U8 *ptr = string.str; + U8 *opl = string.str + string.size; + for (;ptr < opl;){ + U8 *first = ptr; + for (;ptr < opl; ptr += 1){ + U8 c = *ptr; + B32 is_split = 0; + for (U64 i = 0; i < split_char_count; i += 1){ + if (split_chars[i] == c){ + is_split = 1; + break; + } + } + if (is_split){ + break; + } + } + + String8 string = str8_range(first, ptr); + if (keep_empties || string.size > 0){ + str8_list_push(arena, &list, string); + } + ptr += 1; + } + + return(list); +} + +internal String8List +str8_split_by_string_chars(Arena *arena, String8 string, String8 split_chars, StringSplitFlags flags){ + String8List list = str8_split(arena, string, split_chars.str, split_chars.size, flags); + return list; +} + +internal String8List +str8_list_split_by_string_chars(Arena *arena, String8List list, String8 split_chars, StringSplitFlags flags){ + String8List result = {0}; + for (String8Node *node = list.first; node != 0; node = node->next){ + String8List split = str8_split_by_string_chars(arena, node->string, split_chars, flags); + str8_list_concat_in_place(&result, &split); + } + return result; +} + +internal String8 +str8_list_join(Arena *arena, String8List *list, StringJoin *optional_params){ + StringJoin join = {0}; + if (optional_params != 0){ + MemoryCopyStruct(&join, optional_params); + } + + U64 sep_count = 0; + if (list->node_count > 0){ + sep_count = list->node_count - 1; + } + + String8 result; + result.size = join.pre.size + join.post.size + sep_count*join.sep.size + list->total_size; + U8 *ptr = result.str = push_array_no_zero(arena, U8, result.size + 1); + + MemoryCopy(ptr, join.pre.str, join.pre.size); + ptr += join.pre.size; + for (String8Node *node = list->first; + node != 0; + node = node->next){ + MemoryCopy(ptr, node->string.str, node->string.size); + ptr += node->string.size; + if (node->next != 0){ + MemoryCopy(ptr, join.sep.str, join.sep.size); + ptr += join.sep.size; + } + } + MemoryCopy(ptr, join.post.str, join.post.size); + ptr += join.post.size; + + *ptr = 0; + + return(result); +} + +internal void +str8_list_from_flags(Arena *arena, String8List *list, + U32 flags, String8 *flag_string_table, U32 flag_string_count){ + for (U32 i = 0; i < flag_string_count; i += 1){ + U32 flag = (1 << i); + if (flags & flag){ + str8_list_push(arena, list, flag_string_table[i]); + } + } +} + +//////////////////////////////// +//~ rjf; String Arrays + +internal String8Array +str8_array_from_list(Arena *arena, String8List *list) +{ + String8Array array; + array.count = list->node_count; + array.v = push_array_no_zero(arena, String8, array.count); + U64 idx = 0; + for(String8Node *n = list->first; n != 0; n = n->next, idx += 1) + { + array.v[idx] = n->string; + } + return array; +} + +internal String8Array +str8_array_reserve(Arena *arena, U64 count) +{ + String8Array arr; + arr.count = 0; + arr.v = push_array(arena, String8, count); + return arr; +} + +//////////////////////////////// +//~ rjf: String Path Helpers + +internal String8 +str8_chop_last_slash(String8 string){ + if (string.size > 0){ + U8 *ptr = string.str + string.size - 1; + for (;ptr >= string.str; ptr -= 1){ + if (*ptr == '/' || *ptr == '\\'){ + break; + } + } + if (ptr >= string.str){ + string.size = (U64)(ptr - string.str); + } + else{ + string.size = 0; + } + } + return(string); +} + +internal String8 +str8_skip_last_slash(String8 string){ + if (string.size > 0){ + U8 *ptr = string.str + string.size - 1; + for (;ptr >= string.str; ptr -= 1){ + if (*ptr == '/' || *ptr == '\\'){ + break; + } + } + if (ptr >= string.str){ + ptr += 1; + string.size = (U64)(string.str + string.size - ptr); + string.str = ptr; + } + } + return(string); +} + +internal String8 +str8_chop_last_dot(String8 string) +{ + String8 result = string; + U64 p = string.size; + for (;p > 0;){ + p -= 1; + if (string.str[p] == '.'){ + result = str8_prefix(string, p); + break; + } + } + return(result); +} + +internal String8 +str8_skip_last_dot(String8 string){ + String8 result = string; + U64 p = string.size; + for (;p > 0;){ + p -= 1; + if (string.str[p] == '.'){ + result = str8_skip(string, p + 1); + break; + } + } + return(result); +} + +internal PathStyle +path_style_from_str8(String8 string){ + PathStyle result = PathStyle_Relative; + if (string.size >= 1 && string.str[0] == '/'){ + result = PathStyle_UnixAbsolute; + } + else if (string.size >= 2 && + char_is_alpha(string.str[0]) && + string.str[1] == ':'){ + if (string.size == 2 || + char_is_slash(string.str[2])){ + result = PathStyle_WindowsAbsolute; + } + } + return(result); +} + +internal String8List +str8_split_path(Arena *arena, String8 string){ + String8List result = str8_split(arena, string, (U8*)"/\\", 2, 0); + return(result); +} + +internal void +str8_path_list_resolve_dots_in_place(String8List *path, PathStyle style){ + Temp scratch = scratch_begin(0, 0); + + String8MetaNode *stack = 0; + String8MetaNode *free_meta_node = 0; + String8Node *first = path->first; + + MemoryZeroStruct(path); + for (String8Node *node = first, *next = 0; + node != 0; + node = next){ + // save next now + next = node->next; + + // cases: + if (node == first && style == PathStyle_WindowsAbsolute){ + goto save_without_stack; + } + if (node->string.size == 1 && node->string.str[0] == '.'){ + goto do_nothing; + } + if (node->string.size == 2 && node->string.str[0] == '.' && node->string.str[1] == '.'){ + if (stack != 0){ + goto eliminate_stack_top; + } + else{ + goto save_without_stack; + } + } + goto save_with_stack; + + + // handlers: + save_with_stack: + { + str8_list_push_node(path, node); + + String8MetaNode *stack_node = free_meta_node; + if (stack_node != 0){ + SLLStackPop(free_meta_node); + } + else{ + stack_node = push_array_no_zero(scratch.arena, String8MetaNode, 1); + } + SLLStackPush(stack, stack_node); + stack_node->node = node; + + continue; + } + + save_without_stack: + { + str8_list_push_node(path, node); + + continue; + } + + eliminate_stack_top: + { + path->node_count -= 1; + path->total_size -= stack->node->string.size; + + SLLStackPop(stack); + + if (stack == 0){ + path->last = path->first; + } + else{ + path->last = stack->node; + } + continue; + } + + do_nothing: continue; + } + scratch_end(scratch); +} + +internal String8 +str8_path_list_join_by_style(Arena *arena, String8List *path, PathStyle style){ + StringJoin params = {0}; + switch(style) + { + case PathStyle_Null:{}break; + case PathStyle_Relative: + case PathStyle_WindowsAbsolute: + { + params.sep = str8_lit("/"); + }break; + + case PathStyle_UnixAbsolute: + { + params.pre = str8_lit("/"); + params.sep = str8_lit("/"); + }break; + } + String8 result = str8_list_join(arena, path, ¶ms); + return result; +} + +internal String8TxtPtPair +str8_txt_pt_pair_from_string(String8 string) +{ + String8TxtPtPair pair = {0}; + { + String8 file_part = {0}; + String8 line_part = {0}; + String8 col_part = {0}; + + // rjf: grab file part + for(U64 idx = 0; idx <= string.size; idx += 1) + { + U8 byte = (idx < string.size) ? (string.str[idx]) : 0; + U8 next_byte = ((idx+1 < string.size) ? (string.str[idx+1]) : 0); + if(byte == ':' && next_byte != '/' && next_byte != '\\') + { + file_part = str8_prefix(string, idx); + line_part = str8_skip(string, idx+1); + break; + } + else if(byte == 0) + { + file_part = string; + break; + } + } + + // rjf: grab line/column + { + U64 colon_pos = str8_find_needle(line_part, 0, str8_lit(":"), 0); + if(colon_pos < line_part.size) + { + col_part = str8_skip(line_part, colon_pos+1); + line_part = str8_prefix(line_part, colon_pos); + } + } + + // rjf: convert line/column strings to numerics + U64 line = 0; + U64 column = 0; + try_u64_from_str8_c_rules(line_part, &line); + try_u64_from_str8_c_rules(col_part, &column); + + // rjf: fill + pair.string = file_part; + pair.pt = txt_pt((S64)line, (S64)column); + if(pair.pt.line == 0) { pair.pt.line = 1; } + if(pair.pt.column == 0) { pair.pt.column = 1; } + } + return pair; +} + +//////////////////////////////// +//~ rjf: UTF-8 & UTF-16 Decoding/Encoding + +read_only global U8 utf8_class[32] = { + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,2,2,2,3,3,4,5, +}; + +internal UnicodeDecode +utf8_decode(U8 *str, U64 max){ + UnicodeDecode result = {1, max_U32}; + U8 byte = str[0]; + U8 byte_class = utf8_class[byte >> 3]; + switch (byte_class) + { + case 1: + { + result.codepoint = byte; + }break; + case 2: + { + if (2 < max) + { + U8 cont_byte = str[1]; + if (utf8_class[cont_byte >> 3] == 0) + { + result.codepoint = (byte & bitmask5) << 6; + result.codepoint |= (cont_byte & bitmask6); + result.inc = 2; + } + } + }break; + case 3: + { + if (2 < max) + { + U8 cont_byte[2] = {str[1], str[2]}; + if (utf8_class[cont_byte[0] >> 3] == 0 && + utf8_class[cont_byte[1] >> 3] == 0) + { + result.codepoint = (byte & bitmask4) << 12; + result.codepoint |= ((cont_byte[0] & bitmask6) << 6); + result.codepoint |= (cont_byte[1] & bitmask6); + result.inc = 3; + } + } + }break; + case 4: + { + if (3 < max) + { + U8 cont_byte[3] = {str[1], str[2], str[3]}; + if (utf8_class[cont_byte[0] >> 3] == 0 && + utf8_class[cont_byte[1] >> 3] == 0 && + utf8_class[cont_byte[2] >> 3] == 0) + { + result.codepoint = (byte & bitmask3) << 18; + result.codepoint |= ((cont_byte[0] & bitmask6) << 12); + result.codepoint |= ((cont_byte[1] & bitmask6) << 6); + result.codepoint |= (cont_byte[2] & bitmask6); + result.inc = 4; + } + } + } + } + return(result); +} + +internal UnicodeDecode +utf16_decode(U16 *str, U64 max){ + UnicodeDecode result = {1, max_U32}; + result.codepoint = str[0]; + result.inc = 1; + if (max > 1 && 0xD800 <= str[0] && str[0] < 0xDC00 && 0xDC00 <= str[1] && str[1] < 0xE000){ + result.codepoint = ((str[0] - 0xD800) << 10) | ((str[1] - 0xDC00) + 0x10000); + result.inc = 2; + } + return(result); +} + +internal U32 +utf8_encode(U8 *str, U32 codepoint){ + U32 inc = 0; + if (codepoint <= 0x7F){ + str[0] = (U8)codepoint; + inc = 1; + } + else if (codepoint <= 0x7FF){ + str[0] = (bitmask2 << 6) | ((codepoint >> 6) & bitmask5); + str[1] = bit8 | (codepoint & bitmask6); + inc = 2; + } + else if (codepoint <= 0xFFFF){ + str[0] = (bitmask3 << 5) | ((codepoint >> 12) & bitmask4); + str[1] = bit8 | ((codepoint >> 6) & bitmask6); + str[2] = bit8 | ( codepoint & bitmask6); + inc = 3; + } + else if (codepoint <= 0x10FFFF){ + str[0] = (bitmask4 << 4) | ((codepoint >> 18) & bitmask3); + str[1] = bit8 | ((codepoint >> 12) & bitmask6); + str[2] = bit8 | ((codepoint >> 6) & bitmask6); + str[3] = bit8 | ( codepoint & bitmask6); + inc = 4; + } + else{ + str[0] = '?'; + inc = 1; + } + return(inc); +} + +internal U32 +utf16_encode(U16 *str, U32 codepoint){ + U32 inc = 1; + if (codepoint == max_U32){ + str[0] = (U16)'?'; + } + else if (codepoint < 0x10000){ + str[0] = (U16)codepoint; + } + else{ + U32 v = codepoint - 0x10000; + str[0] = safe_cast_u16(0xD800 + (v >> 10)); + str[1] = safe_cast_u16(0xDC00 + (v & bitmask10)); + inc = 2; + } + return(inc); +} + +internal U32 +utf8_from_utf32_single(U8 *buffer, U32 character){ + return(utf8_encode(buffer, character)); +} + +//////////////////////////////// +//~ rjf: Unicode String Conversions + +internal String8 +str8_from_16(Arena *arena, String16 in) +{ + String8 result = str8_zero(); + if(in.size) + { + U64 cap = in.size*3; + U8 *str = push_array_no_zero(arena, U8, cap + 1); + U16 *ptr = in.str; + U16 *opl = ptr + in.size; + U64 size = 0; + UnicodeDecode consume; + for(;ptr < opl; ptr += consume.inc) + { + consume = utf16_decode(ptr, opl - ptr); + size += utf8_encode(str + size, consume.codepoint); + } + str[size] = 0; + arena_pop(arena, (cap - size)); + result = str8(str, size); + } + return result; +} + +internal String16 +str16_from_8(Arena *arena, String8 in) +{ + String16 result = str16_zero(); + if(in.size) + { + U64 cap = in.size*2; + U16 *str = push_array_no_zero(arena, U16, cap + 1); + U8 *ptr = in.str; + U8 *opl = ptr + in.size; + U64 size = 0; + UnicodeDecode consume; + for(;ptr < opl; ptr += consume.inc) + { + consume = utf8_decode(ptr, opl - ptr); + size += utf16_encode(str + size, consume.codepoint); + } + str[size] = 0; + arena_pop(arena, (cap - size)*2); + result = str16(str, size); + } + return result; +} + +internal String8 +str8_from_32(Arena *arena, String32 in) +{ + String8 result = str8_zero(); + if(in.size) + { + U64 cap = in.size*4; + U8 *str = push_array_no_zero(arena, U8, cap + 1); + U32 *ptr = in.str; + U32 *opl = ptr + in.size; + U64 size = 0; + for(;ptr < opl; ptr += 1) + { + size += utf8_encode(str + size, *ptr); + } + str[size] = 0; + arena_pop(arena, (cap - size)); + result = str8(str, size); + } + return result; +} + +internal String32 +str32_from_8(Arena *arena, String8 in) +{ + String32 result = str32_zero(); + if(in.size) + { + U64 cap = in.size; + U32 *str = push_array_no_zero(arena, U32, cap + 1); + U8 *ptr = in.str; + U8 *opl = ptr + in.size; + U64 size = 0; + UnicodeDecode consume; + for(;ptr < opl; ptr += consume.inc) + { + consume = utf8_decode(ptr, opl - ptr); + str[size] = consume.codepoint; + size += 1; + } + str[size] = 0; + arena_pop(arena, (cap - size)*4); + result = str32(str, size); + } + return result; +} + +//////////////////////////////// +//~ String -> Enum Conversions + +read_only global struct +{ + String8 string; + OperatingSystem os; +} g_os_enum_map[] = +{ + { str8_lit_comp(""), OperatingSystem_Null }, + { str8_lit_comp("Windows"), OperatingSystem_Windows, }, + { str8_lit_comp("Linux"), OperatingSystem_Linux, }, + { str8_lit_comp("Mac"), OperatingSystem_Mac, }, +}; +StaticAssert(ArrayCount(g_os_enum_map) == OperatingSystem_COUNT, g_os_enum_map_count_check); + +internal OperatingSystem +operating_system_from_string(String8 string) +{ + for(U64 i = 0; i < ArrayCount(g_os_enum_map); ++i) + { + if(str8_match(g_os_enum_map[i].string, string, StringMatchFlag_CaseInsensitive)) + { + return g_os_enum_map[i].os; + } + } + return OperatingSystem_Null; +} + +//////////////////////////////// +//~ rjf: Basic Types & Space Enum -> String Conversions + +internal String8 +string_from_dimension(Dimension dimension){ + local_persist String8 strings[] = { + str8_lit_comp("X"), + str8_lit_comp("Y"), + str8_lit_comp("Z"), + str8_lit_comp("W"), + }; + String8 result = str8_lit("error"); + if ((U32)dimension < 4){ + result = strings[dimension]; + } + return(result); +} + +internal String8 +string_from_side(Side side){ + local_persist String8 strings[] = { + str8_lit_comp("Min"), + str8_lit_comp("Max"), + }; + String8 result = str8_lit("error"); + if ((U32)side < 2){ + result = strings[side]; + } + return(result); +} + +internal String8 +string_from_operating_system(OperatingSystem os) +{ + String8 result = g_os_enum_map[OperatingSystem_Null].string; + if(os < ArrayCount(g_os_enum_map)) + { + result = g_os_enum_map[os].string; + } + return result; +} + +internal String8 +string_from_arch(Arch arch){ + local_persist String8 strings[] = { + str8_lit_comp("Null"), + str8_lit_comp("x64"), + str8_lit_comp("x86"), + str8_lit_comp("arm64"), + str8_lit_comp("arm32"), + }; + String8 result = str8_lit("error"); + if (arch < Arch_COUNT){ + result = strings[arch]; + } + return(result); +} + +//////////////////////////////// +//~ rjf: Time Types -> String + +internal String8 +string_from_week_day(WeekDay week_day){ + local_persist String8 strings[] = { + str8_lit_comp("Sun"), + str8_lit_comp("Mon"), + str8_lit_comp("Tue"), + str8_lit_comp("Wed"), + str8_lit_comp("Thu"), + str8_lit_comp("Fri"), + str8_lit_comp("Sat"), + }; + String8 result = str8_lit("Err"); + if ((U32)week_day < WeekDay_COUNT){ + result = strings[week_day]; + } + return(result); +} + +internal String8 +string_from_month(Month month){ + local_persist String8 strings[] = { + str8_lit_comp("Jan"), + str8_lit_comp("Feb"), + str8_lit_comp("Mar"), + str8_lit_comp("Apr"), + str8_lit_comp("May"), + str8_lit_comp("Jun"), + str8_lit_comp("Jul"), + str8_lit_comp("Aug"), + str8_lit_comp("Sep"), + str8_lit_comp("Oct"), + str8_lit_comp("Nov"), + str8_lit_comp("Dec"), + }; + String8 result = str8_lit("Err"); + if ((U32)month < Month_COUNT){ + result = strings[month]; + } + return(result); +} + +internal String8 +push_date_time_string(Arena *arena, DateTime *date_time){ + char *mon_str = (char*)string_from_month(date_time->month).str; + U32 adjusted_hour = date_time->hour%12; + if (adjusted_hour == 0){ + adjusted_hour = 12; + } + char *ampm = "am"; + if (date_time->hour >= 12){ + ampm = "pm"; + } + String8 result = push_str8f(arena, "%d %s %d, %02d:%02d:%02d %s", + date_time->day, mon_str, date_time->year, + adjusted_hour, date_time->min, date_time->sec, ampm); + return(result); +} + +internal String8 +push_file_name_date_time_string(Arena *arena, DateTime *date_time){ + char *mon_str = (char*)string_from_month(date_time->month).str; + String8 result = push_str8f(arena, "%d-%s-%0d--%02d-%02d-%02d", + date_time->year, mon_str, date_time->day, + date_time->hour, date_time->min, date_time->sec); + return(result); +} + +internal String8 +string_from_elapsed_time(Arena *arena, DateTime dt){ + Temp scratch = scratch_begin(&arena, 1); + String8List list = {0}; + if (dt.year){ + str8_list_pushf(scratch.arena, &list, "%dy", dt.year); + str8_list_pushf(scratch.arena, &list, "%um", dt.mon); + str8_list_pushf(scratch.arena, &list, "%ud", dt.day); + } else if (dt.mon){ + str8_list_pushf(scratch.arena, &list, "%um", dt.mon); + str8_list_pushf(scratch.arena, &list, "%ud", dt.day); + } else if (dt.day){ + str8_list_pushf(scratch.arena, &list, "%ud", dt.day); + } + str8_list_pushf(scratch.arena, &list, "%u:%u:%u:%u ms", dt.hour, dt.min, dt.sec, dt.msec); + StringJoin join = { str8_lit_comp(""), str8_lit_comp(" "), str8_lit_comp("") }; + String8 result = str8_list_join(arena, &list, &join); + scratch_end(scratch); + return(result); +} + +//////////////////////////////// +//~ Globally UNique Ids + +internal String8 +string_from_guid(Arena *arena, Guid guid) +{ + String8 result = push_str8f(arena, "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", + guid.data1, + guid.data2, + guid.data3, + guid.data4[0], + guid.data4[1], + guid.data4[2], + guid.data4[3], + guid.data4[4], + guid.data4[5], + guid.data4[6], + guid.data4[7]); + return result; +} + +internal B32 +try_guid_from_string(String8 string, Guid *guid_out) +{ + Temp scratch = scratch_begin(0,0); + B32 is_parsed = 0; + String8List list = str8_split_by_string_chars(scratch.arena, string, str8_lit("-"), StringSplitFlag_KeepEmpties); + if(list.node_count == 5) + { + String8 data1_str = list.first->string; + String8 data2_str = list.first->next->string; + String8 data3_str = list.first->next->next->string; + String8 data4_hi_str = list.first->next->next->next->string; + String8 data4_lo_str = list.first->next->next->next->next->string; + if(str8_is_integer(data1_str, 16) && + str8_is_integer(data2_str, 16) && + str8_is_integer(data3_str, 16) && + str8_is_integer(data4_hi_str, 16) && + str8_is_integer(data4_lo_str, 16)) + { + U64 data1 = u64_from_str8(data1_str, 16); + U64 data2 = u64_from_str8(data2_str, 16); + U64 data3 = u64_from_str8(data3_str, 16); + U64 data4_hi = u64_from_str8(data4_hi_str, 16); + U64 data4_lo = u64_from_str8(data4_lo_str, 16); + if(data1 <= max_U32 && + data2 <= max_U16 && + data3 <= max_U16 && + data4_hi <= max_U16 && + data4_lo <= 0xffffffffffff) + { + guid_out->data1 = (U32)data1; + guid_out->data2 = (U16)data2; + guid_out->data3 = (U16)data3; + U64 data4 = (data4_hi << 48) | data4_lo; + MemoryCopy(&guid_out->data4[0], &data4, sizeof(data4)); + is_parsed = 1; + } + } + } + scratch_end(scratch); + return is_parsed; +} + +internal Guid +guid_from_string(String8 string) +{ + Guid guid = {0}; + try_guid_from_string(string, &guid); + return guid; +} + +//////////////////////////////// +//~ rjf: Basic Text Indentation + +internal String8 +indented_from_string(Arena *arena, String8 string) +{ + Temp scratch = scratch_begin(&arena, 1); + read_only local_persist U8 indentation_bytes[] = " "; + String8List indented_strings = {0}; + S64 depth = 0; + S64 next_depth = 0; + U64 line_begin_off = 0; + for(U64 off = 0; off <= string.size; off += 1) + { + U8 byte = off width_this_line){ + String8 line = str8_substr(string, line_range); + if (wrapped_indent_level > 0){ + line = push_str8f(arena, "%.*s%S", wrapped_indent_level, spaces, line); + } + str8_list_push(arena, &list, line); + line_range = r1u64(line_range.max+1, candidate_line_range.max); + wrapped_indent_level = ClampTop(64, wrap_indent); + } + else{ + line_range = candidate_line_range; + } + } + } + if (line_range.min < string.size && line_range.max > line_range.min){ + String8 line = str8_substr(string, line_range); + if (wrapped_indent_level > 0){ + line = push_str8f(arena, "%.*s%S", wrapped_indent_level, spaces, line); + } + str8_list_push(arena, &list, line); + } + return list; +} + +//////////////////////////////// +//~ rjf: String <-> Color + +internal String8 +hex_string_from_rgba_4f32(Arena *arena, Vec4F32 rgba) +{ + String8 hex_string = push_str8f(arena, "%02x%02x%02x%02x", (U8)(rgba.x*255.f), (U8)(rgba.y*255.f), (U8)(rgba.z*255.f), (U8)(rgba.w*255.f)); + return hex_string; +} + +internal Vec4F32 +rgba_from_hex_string_4f32(String8 hex_string) +{ + U8 byte_text[8] = {0}; + U64 byte_text_idx = 0; + for(U64 idx = 0; idx < hex_string.size && byte_text_idx < ArrayCount(byte_text); idx += 1) + { + if(char_is_digit(hex_string.str[idx], 16)) + { + byte_text[byte_text_idx] = char_to_lower(hex_string.str[idx]); + byte_text_idx += 1; + } + } + U8 byte_vals[4] = {0}; + for(U64 idx = 0; idx < 4; idx += 1) + { + byte_vals[idx] = (U8)u64_from_str8(str8(&byte_text[idx*2], 2), 16); + } + Vec4F32 rgba = v4f32(byte_vals[0]/255.f, byte_vals[1]/255.f, byte_vals[2]/255.f, byte_vals[3]/255.f); + return rgba; +} + +//////////////////////////////// +//~ rjf: String Fuzzy Matching + +internal FuzzyMatchRangeList +fuzzy_match_find(Arena *arena, String8 needle, String8 haystack) +{ + FuzzyMatchRangeList result = {0}; + Temp scratch = scratch_begin(&arena, 1); + String8List needles = str8_split(scratch.arena, needle, (U8*)" ", 1, 0); + result.needle_part_count = needles.node_count; + for(String8Node *needle_n = needles.first; needle_n != 0; needle_n = needle_n->next) + { + U64 find_pos = 0; + for(;find_pos < haystack.size;) + { + find_pos = str8_find_needle(haystack, find_pos, needle_n->string, StringMatchFlag_CaseInsensitive); + B32 is_in_gathered_ranges = 0; + for(FuzzyMatchRangeNode *n = result.first; n != 0; n = n->next) + { + if(n->range.min <= find_pos && find_pos < n->range.max) + { + is_in_gathered_ranges = 1; + find_pos = n->range.max; + break; + } + } + if(!is_in_gathered_ranges) + { + break; + } + } + if(find_pos < haystack.size) + { + Rng1U64 range = r1u64(find_pos, find_pos+needle_n->string.size); + FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1); + n->range = range; + SLLQueuePush(result.first, result.last, n); + result.count += 1; + result.total_dim += dim_1u64(range); + } + } + scratch_end(scratch); + return result; +} + +internal FuzzyMatchRangeList +fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src) +{ + FuzzyMatchRangeList dst = {0}; + for(FuzzyMatchRangeNode *src_n = src->first; src_n != 0; src_n = src_n->next) + { + FuzzyMatchRangeNode *dst_n = push_array(arena, FuzzyMatchRangeNode, 1); + SLLQueuePush(dst.first, dst.last, dst_n); + dst_n->range = src_n->range; + } + dst.count = src->count; + dst.needle_part_count = src->needle_part_count; + dst.total_dim = src->total_dim; + return dst; +} + +//////////////////////////////// +//~ NOTE(allen): Serialization Helpers + +internal void +str8_serial_begin(Arena *arena, String8List *srl){ + String8Node *node = push_array(arena, String8Node, 1); + node->string.str = push_array_no_zero(arena, U8, 0); + srl->first = srl->last = node; + srl->node_count = 1; + srl->total_size = 0; +} + +internal String8 +str8_serial_end(Arena *arena, String8List *srl){ + U64 size = srl->total_size; + U8 *out = push_array_no_zero(arena, U8, size); + str8_serial_write_to_dst(srl, out); + String8 result = str8(out, size); + return result; +} + +internal void +str8_serial_write_to_dst(String8List *srl, void *out){ + U8 *ptr = (U8*)out; + for (String8Node *node = srl->first; + node != 0; + node = node->next){ + U64 size = node->string.size; + MemoryCopy(ptr, node->string.str, size); + ptr += size; + } +} + +internal U64 +str8_serial_push_align(Arena *arena, String8List *srl, U64 align){ + Assert(IsPow2(align)); + + U64 pos = srl->total_size; + U64 new_pos = AlignPow2(pos, align); + U64 size = (new_pos - pos); + + if(size != 0) + { + U8 *buf = push_array(arena, U8, size); + + String8 *str = &srl->last->string; + if (str->str + str->size == buf){ + srl->last->string.size += size; + srl->total_size += size; + } + else{ + str8_list_push(arena, srl, str8(buf, size)); + } + } + return size; +} + +internal void * +str8_serial_push_size(Arena *arena, String8List *srl, U64 size) +{ + void *result = 0; + if(size != 0) + { + U8 *buf = push_array_no_zero(arena, U8, size); + String8 *str = &srl->last->string; + if (str->str + str->size == buf){ + srl->last->string.size += size; + srl->total_size += size; + } + else{ + str8_list_push(arena, srl, str8(buf, size)); + } + result = buf; + } + return result; +} + +internal void * +str8_serial_push_data(Arena *arena, String8List *srl, void *data, U64 size){ + void *result = str8_serial_push_size(arena, srl, size); + if(result != 0) + { + MemoryCopy(result, data, size); + } + return result; +} + +internal void +str8_serial_push_data_list(Arena *arena, String8List *srl, String8Node *first){ + for (String8Node *node = first; + node != 0; + node = node->next){ + str8_serial_push_data(arena, srl, node->string.str, node->string.size); + } +} + +internal void +str8_serial_push_u64(Arena *arena, String8List *srl, U64 x){ + U8 *buf = push_array_no_zero(arena, U8, 8); + MemoryCopy(buf, &x, 8); + String8 *str = &srl->last->string; + if (str->str + str->size == buf){ + srl->last->string.size += 8; + srl->total_size += 8; + } + else{ + str8_list_push(arena, srl, str8(buf, 8)); + } +} + +internal void +str8_serial_push_u32(Arena *arena, String8List *srl, U32 x){ + U8 *buf = push_array_no_zero(arena, U8, 4); + MemoryCopy(buf, &x, 4); + String8 *str = &srl->last->string; + if (str->str + str->size == buf){ + srl->last->string.size += 4; + srl->total_size += 4; + } + else{ + str8_list_push(arena, srl, str8(buf, 4)); + } +} + +internal void +str8_serial_push_u16(Arena *arena, String8List *srl, U16 x){ + str8_serial_push_data(arena, srl, &x, sizeof(x)); +} + +internal void +str8_serial_push_u8(Arena *arena, String8List *srl, U8 x){ + str8_serial_push_data(arena, srl, &x, sizeof(x)); +} + +internal void +str8_serial_push_cstr(Arena *arena, String8List *srl, String8 str){ + str8_serial_push_data(arena, srl, str.str, str.size); + str8_serial_push_u8(arena, srl, 0); +} + +internal void +str8_serial_push_string(Arena *arena, String8List *srl, String8 str){ + str8_serial_push_data(arena, srl, str.str, str.size); +} + +//////////////////////////////// +//~ rjf: Deserialization Helpers + +internal U64 +str8_deserial_read(String8 string, U64 off, void *read_dst, U64 read_size, U64 granularity) +{ + U64 bytes_left = string.size-Min(off, string.size); + U64 actually_readable_size = Min(bytes_left, read_size); + U64 legally_readable_size = actually_readable_size - actually_readable_size%granularity; + if(legally_readable_size > 0) + { + MemoryCopy(read_dst, string.str+off, legally_readable_size); + } + return legally_readable_size; +} + +internal U64 +str8_deserial_find_first_match(String8 string, U64 off, U16 scan_val) +{ + U64 cursor = off; + for (;;) { + U16 val = 0; + str8_deserial_read_struct(string, cursor, &val); + if (val == scan_val) { + break; + } + cursor += sizeof(val); + } + return cursor; +} + +internal void * +str8_deserial_get_raw_ptr(String8 string, U64 off, U64 size) +{ + void *raw_ptr = 0; + if (off + size <= string.size) { + raw_ptr = string.str + off; + } + return raw_ptr; +} + +internal U64 +str8_deserial_read_cstr(String8 string, U64 off, String8 *cstr_out) +{ + U64 cstr_size = 0; + if (off < string.size) { + U8 *ptr = string.str + off; + U8 *cap = string.str + string.size; + *cstr_out = str8_cstring_capped(ptr, cap); + cstr_size = (cstr_out->size + 1); + } + return cstr_size; +} + +internal U64 +str8_deserial_read_windows_utf16_string16(String8 string, U64 off, String16 *str_out) +{ + U64 null_off = str8_deserial_find_first_match(string, off, 0); + U64 size = null_off - off; + U16 *str = (U16 *)str8_deserial_get_raw_ptr(string, off, size); + U64 count = size / sizeof(*str); + *str_out = str16(str, count); + + U64 read_size_with_null = size + sizeof(*str); + return read_size_with_null; +} + +internal U64 +str8_deserial_read_block(String8 string, U64 off, U64 size, String8 *block_out) +{ + Rng1U64 range = rng_1u64(off, off + size); + *block_out = str8_substr(string, range); + return block_out->size; +} + +internal U64 +str8_deserial_read_uleb128(String8 string, U64 off, U64 *value_out) +{ + U64 value = 0; + U64 shift = 0; + U64 cursor = off; + for(;;) + { + U8 byte = 0; + U64 bytes_read = str8_deserial_read_struct(string, cursor, &byte); + + if(bytes_read != sizeof(byte)) + { + break; + } + + U8 val = byte & 0x7fu; + value |= ((U64)val) << shift; + + cursor += bytes_read; + shift += 7u; + + if((byte & 0x80u) == 0) + { + break; + } + } + if(value_out != 0) + { + *value_out = value; + } + U64 bytes_read = cursor - off; + return bytes_read; +} + +internal U64 +str8_deserial_read_sleb128(String8 string, U64 off, S64 *value_out) +{ + U64 value = 0; + U64 shift = 0; + U64 cursor = off; + for(;;) + { + U8 byte; + U64 bytes_read = str8_deserial_read_struct(string, cursor, &byte); + if(bytes_read != sizeof(byte)) + { + break; + } + + U8 val = byte & 0x7fu; + value |= ((U64)val) << shift; + + cursor += bytes_read; + shift += 7u; + + if((byte & 0x80u) == 0) + { + if(shift < sizeof(value) * 8 && (byte & 0x40u) != 0) + { + value |= -(S64)(1ull << shift); + } + break; + } + } + if(value_out != 0) + { + *value_out = value; + } + U64 bytes_read = cursor - off; + return bytes_read; +} + diff --git a/src/base/base_strings.h b/src/base/base_strings.h index 67e90349a..e66d19c8f 100644 --- a/src/base/base_strings.h +++ b/src/base/base_strings.h @@ -1,415 +1,417 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -#ifndef BASE_STRINGS_H -#define BASE_STRINGS_H - -//////////////////////////////// -//~ rjf: Third Party Includes - -#define STB_SPRINTF_DECORATE(name) raddbg_##name -#include "third_party/stb/stb_sprintf.h" - -//////////////////////////////// -//~ rjf: String Types - -typedef struct String8 String8; -struct String8 -{ - U8 *str; - U64 size; -}; - -typedef struct String16 String16; -struct String16 -{ - U16 *str; - U64 size; -}; - -typedef struct String32 String32; -struct String32 -{ - U32 *str; - U64 size; -}; - -//////////////////////////////// -//~ rjf: String List & Array Types - -typedef struct String8Node String8Node; -struct String8Node -{ - String8Node *next; - String8 string; -}; - -typedef struct String8MetaNode String8MetaNode; -struct String8MetaNode -{ - String8MetaNode *next; - String8Node *node; -}; - -typedef struct String8List String8List; -struct String8List -{ - String8Node *first; - String8Node *last; - U64 node_count; - U64 total_size; -}; - -typedef struct String8Array String8Array; -struct String8Array -{ - String8 *v; - U64 count; -}; - -//////////////////////////////// -//~ rjf: String Matching, Splitting, & Joining Types - -typedef U32 StringMatchFlags; -enum -{ - StringMatchFlag_CaseInsensitive = (1 << 0), - StringMatchFlag_RightSideSloppy = (1 << 1), - StringMatchFlag_SlashInsensitive = (1 << 2), -}; - -typedef U32 StringSplitFlags; -enum -{ - StringSplitFlag_KeepEmpties = (1 << 0), -}; - -typedef enum PathStyle -{ - PathStyle_Null, - PathStyle_Relative, - PathStyle_WindowsAbsolute, - PathStyle_UnixAbsolute, - -#if OS_WINDOWS - PathStyle_SystemAbsolute = PathStyle_WindowsAbsolute -#elif OS_LINUX - PathStyle_SystemAbsolute = PathStyle_UnixAbsolute -#else -# error "absolute path style is undefined for this OS" -#endif -} -PathStyle; - -typedef struct StringJoin StringJoin; -struct StringJoin -{ - String8 pre; - String8 sep; - String8 post; -}; - -//////////////////////////////// -//~ rjf: String Pair Types - -typedef struct String8TxtPtPair String8TxtPtPair; -struct String8TxtPtPair -{ - String8 string; - TxtPt pt; -}; - -//////////////////////////////// -//~ rjf: UTF Decoding Types - -typedef struct UnicodeDecode UnicodeDecode; -struct UnicodeDecode -{ - U32 inc; - U32 codepoint; -}; - -//////////////////////////////// -//~ rjf: String Fuzzy Matching Types - -typedef struct FuzzyMatchRangeNode FuzzyMatchRangeNode; -struct FuzzyMatchRangeNode -{ - FuzzyMatchRangeNode *next; - Rng1U64 range; -}; - -typedef struct FuzzyMatchRangeList FuzzyMatchRangeList; -struct FuzzyMatchRangeList -{ - FuzzyMatchRangeNode *first; - FuzzyMatchRangeNode *last; - U64 count; - U64 needle_part_count; - U64 total_dim; -}; - -//////////////////////////////// -//~ rjf: Character Classification & Conversion Functions - -internal B32 char_is_space(U8 c); -internal B32 char_is_upper(U8 c); -internal B32 char_is_lower(U8 c); -internal B32 char_is_alpha(U8 c); -internal B32 char_is_slash(U8 c); -internal B32 char_is_digit(U8 c, U32 base); -internal U8 char_to_lower(U8 c); -internal U8 char_to_upper(U8 c); -internal U8 char_to_correct_slash(U8 c); - -//////////////////////////////// -//~ rjf: C-String Measurement - -internal U64 cstring8_length(U8 *c); -internal U64 cstring16_length(U16 *c); -internal U64 cstring32_length(U32 *c); - -//////////////////////////////// -//~ rjf: String Constructors - -#define str8_lit(S) str8((U8*)(S), sizeof(S) - 1) -#define str8_lit_comp(S) {(U8*)(S), sizeof(S) - 1,} -#define str8_varg(S) (int)((S).size), ((S).str) - -#define str8_array(S,C) str8((U8*)(S), sizeof(*(S))*(C)) -#define str8_array_fixed(S) str8((U8*)(S), sizeof(S)) -#define str8_struct(S) str8((U8*)(S), sizeof(*(S))) - -internal String8 str8(U8 *str, U64 size); -internal String8 str8_range(U8 *first, U8 *one_past_last); -internal String8 str8_zero(void); -internal String16 str16(U16 *str, U64 size); -internal String16 str16_range(U16 *first, U16 *one_past_last); -internal String16 str16_zero(void); -internal String32 str32(U32 *str, U64 size); -internal String32 str32_range(U32 *first, U32 *one_past_last); -internal String32 str32_zero(void); -internal String8 str8_cstring(char *c); -internal String16 str16_cstring(U16 *c); -internal String32 str32_cstring(U32 *c); -internal String8 str8_cstring_capped(void *cstr, void *cap); -internal String16 str16_cstring_capped(void *cstr, void *cap); -internal String8 str8_cstring_capped_reverse(void *raw_start, void *raw_cap); - -//////////////////////////////// -//~ rjf: String Stylization - -internal String8 upper_from_str8(Arena *arena, String8 string); -internal String8 lower_from_str8(Arena *arena, String8 string); -internal String8 backslashed_from_str8(Arena *arena, String8 string); - -//////////////////////////////// -//~ rjf: String Matching - -#define str8_match_lit(a_lit, b, flags) str8_match(str8_lit(a_lit), (b), (flags)) -#define str8_match_cstr(a_cstr, b, flags) str8_match(str8_cstring(a_cstr), (b), (flags)) -internal B32 str8_match(String8 a, String8 b, StringMatchFlags flags); -internal U64 str8_find_needle(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags); -internal U64 str8_find_needle_reverse(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags); -internal B32 str8_ends_with(String8 string, String8 end, StringMatchFlags flags); -#define str8_ends_with_lit(string, end_lit, flags) str8_ends_with((string), str8_lit(end_lit), (flags)) - -//////////////////////////////// -//~ rjf: String Slicing - -internal String8 str8_substr(String8 str, Rng1U64 range); -internal String8 str8_prefix(String8 str, U64 size); -internal String8 str8_skip(String8 str, U64 amt); -internal String8 str8_postfix(String8 str, U64 size); -internal String8 str8_chop(String8 str, U64 amt); -internal String8 str8_skip_chop_whitespace(String8 string); - -//////////////////////////////// -//~ rjf: String Formatting & Copying - -internal String8 push_str8_cat(Arena *arena, String8 s1, String8 s2); -internal String8 push_str8_copy(Arena *arena, String8 s); -internal String8 push_str8fv(Arena *arena, char *fmt, va_list args); -internal String8 push_str8f(Arena *arena, char *fmt, ...); - -//////////////////////////////// -//~ rjf: String <=> Integer Conversions - -//- rjf: string -> integer -internal S64 sign_from_str8(String8 string, String8 *string_tail); -internal B32 str8_is_integer(String8 string, U32 radix); - -internal U64 u64_from_str8(String8 string, U32 radix); -internal S64 s64_from_str8(String8 string, U32 radix); -internal U32 u32_from_str8(String8 string, U32 radix); -internal S32 s32_from_str8(String8 string, U32 radix); -internal B32 try_u64_from_str8_c_rules(String8 string, U64 *x); -internal B32 try_s64_from_str8_c_rules(String8 string, S64 *x); - -//- rjf: integer -> string -internal String8 str8_from_memory_size(Arena *arena, U64 size); -internal String8 str8_from_count(Arena *arena, U64 count); -internal String8 str8_from_bits_u32(Arena *arena, U32 x); -internal String8 str8_from_bits_u64(Arena *arena, U64 x); -internal String8 str8_from_u64(Arena *arena, U64 u64, U32 radix, U8 min_digits, U8 digit_group_separator); -internal String8 str8_from_s64(Arena *arena, S64 s64, U32 radix, U8 min_digits, U8 digit_group_separator); - -//////////////////////////////// -//~ rjf: String <=> Float Conversions - -internal F64 f64_from_str8(String8 string); - -//////////////////////////////// -//~ rjf: String List Construction Functions - -internal String8Node* str8_list_push_node(String8List *list, String8Node *node); -internal String8Node* str8_list_push_node_set_string(String8List *list, String8Node *node, String8 string); -internal String8Node* str8_list_push_node_front(String8List *list, String8Node *node); -internal String8Node* str8_list_push_node_front_set_string(String8List *list, String8Node *node, String8 string); -internal String8Node* str8_list_push(Arena *arena, String8List *list, String8 string); -internal String8Node* str8_list_push_front(Arena *arena, String8List *list, String8 string); -internal void str8_list_concat_in_place(String8List *list, String8List *to_push); -internal String8Node* str8_list_push_aligner(Arena *arena, String8List *list, U64 min, U64 align); -internal String8Node* str8_list_pushf(Arena *arena, String8List *list, char *fmt, ...); -internal String8Node* str8_list_push_frontf(Arena *arena, String8List *list, char *fmt, ...); -internal String8List str8_list_copy(Arena *arena, String8List *list); -#define str8_list_first(list) ((list)->first ? (list)->first->string : str8_zero()) - -//////////////////////////////// -//~ rjf: String Splitting & Joining - -internal String8List str8_split(Arena *arena, String8 string, U8 *split_chars, U64 split_char_count, StringSplitFlags flags); -internal String8List str8_split_by_string_chars(Arena *arena, String8 string, String8 split_chars, StringSplitFlags flags); -internal String8List str8_list_split_by_string_chars(Arena *arena, String8List list, String8 split_chars, StringSplitFlags flags); -internal String8 str8_list_join(Arena *arena, String8List *list, StringJoin *optional_params); -internal void str8_list_from_flags(Arena *arena, String8List *list, U32 flags, String8 *flag_string_table, U32 flag_string_count); - -//////////////////////////////// -//~ rjf; String Arrays - -internal String8Array str8_array_from_list(Arena *arena, String8List *list); -internal String8Array str8_array_reserve(Arena *arena, U64 count); - -//////////////////////////////// -//~ rjf: String Path Helpers - -internal String8 str8_chop_last_slash(String8 string); -internal String8 str8_skip_last_slash(String8 string); -internal String8 str8_chop_last_dot(String8 string); -internal String8 str8_skip_last_dot(String8 string); - -internal PathStyle path_style_from_str8(String8 string); -internal String8List str8_split_path(Arena *arena, String8 string); -internal void str8_path_list_resolve_dots_in_place(String8List *path, PathStyle style); -internal String8 str8_path_list_join_by_style(Arena *arena, String8List *path, PathStyle style); - -internal String8TxtPtPair str8_txt_pt_pair_from_string(String8 string); - -//////////////////////////////// -//~ rjf: UTF-8 & UTF-16 Decoding/Encoding - -internal UnicodeDecode utf8_decode(U8 *str, U64 max); -internal UnicodeDecode utf16_decode(U16 *str, U64 max); -internal U32 utf8_encode(U8 *str, U32 codepoint); -internal U32 utf16_encode(U16 *str, U32 codepoint); -internal U32 utf8_from_utf32_single(U8 *buffer, U32 character); - -//////////////////////////////// -//~ rjf: Unicode String Conversions - -internal String8 str8_from_16(Arena *arena, String16 in); -internal String16 str16_from_8(Arena *arena, String8 in); -internal String8 str8_from_32(Arena *arena, String32 in); -internal String32 str32_from_8(Arena *arena, String8 in); - -//////////////////////////////// -//~ String -> Enum Conversions - -internal OperatingSystem operating_system_from_string(String8 string); - -//////////////////////////////// -//~ rjf: Basic Types & Space Enum -> String Conversions - -internal String8 string_from_dimension(Dimension dimension); -internal String8 string_from_side(Side side); -internal String8 string_from_operating_system(OperatingSystem os); -internal String8 string_from_arch(Arch arch); - -//////////////////////////////// -//~ rjf: Time Types -> String - -internal String8 string_from_week_day(WeekDay week_day); -internal String8 string_from_month(Month month); -internal String8 push_date_time_string(Arena *arena, DateTime *date_time); -internal String8 push_file_name_date_time_string(Arena *arena, DateTime *date_time); -internal String8 string_from_elapsed_time(Arena *arena, DateTime dt); - -//////////////////////////////// -//~ Globally Unique Ids - -internal String8 string_from_guid(Arena *arena, Guid guid); -internal B32 try_guid_from_string(String8 string, Guid *guid_out); -internal Guid guid_from_string(String8 string); - -//////////////////////////////// -//~ rjf: Basic Text Indentation - -internal String8 indented_from_string(Arena *arena, String8 string); - -//////////////////////////////// -//~ rjf: Text Escaping - -internal String8 escaped_from_raw_str8(Arena *arena, String8 string); -internal String8 raw_from_escaped_str8(Arena *arena, String8 string); - -//////////////////////////////// -//~ rjf: Text Wrapping - -internal String8List wrapped_lines_from_string(Arena *arena, String8 string, U64 first_line_max_width, U64 max_width, U64 wrap_indent); - -//////////////////////////////// -//~ rjf: String <-> Color - -internal String8 hex_string_from_rgba_4f32(Arena *arena, Vec4F32 rgba); -internal Vec4F32 rgba_from_hex_string_4f32(String8 hex_string); - -//////////////////////////////// -//~ rjf: String Fuzzy Matching - -internal FuzzyMatchRangeList fuzzy_match_find(Arena *arena, String8 needle, String8 haystack); -internal FuzzyMatchRangeList fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src); - -//////////////////////////////// -//~ NOTE(allen): Serialization Helpers - -internal void str8_serial_begin(Arena *arena, String8List *srl); -internal String8 str8_serial_end(Arena *arena, String8List *srl); -internal void str8_serial_write_to_dst(String8List *srl, void *out); -internal U64 str8_serial_push_align(Arena *arena, String8List *srl, U64 align); -internal void * str8_serial_push_size(Arena *arena, String8List *srl, U64 size); -internal void * str8_serial_push_data(Arena *arena, String8List *srl, void *data, U64 size); -internal void str8_serial_push_data_list(Arena *arena, String8List *srl, String8Node *first); -internal void str8_serial_push_u64(Arena *arena, String8List *srl, U64 x); -internal void str8_serial_push_u32(Arena *arena, String8List *srl, U32 x); -internal void str8_serial_push_u16(Arena *arena, String8List *srl, U16 x); -internal void str8_serial_push_u8(Arena *arena, String8List *srl, U8 x); -internal void str8_serial_push_cstr(Arena *arena, String8List *srl, String8 str); -internal void str8_serial_push_string(Arena *arena, String8List *srl, String8 str); -#define str8_serial_push_array(arena, srl, ptr, count) str8_serial_push_data(arena, srl, ptr, sizeof(*(ptr)) * (count)) -#define str8_serial_push_struct(arena, srl, ptr) str8_serial_push_array(arena, srl, ptr, 1) - -//////////////////////////////// -//~ rjf: Deserialization Helpers - -internal U64 str8_deserial_read(String8 string, U64 off, void *read_dst, U64 read_size, U64 granularity); -internal U64 str8_deserial_find_first_match(String8 string, U64 off, U16 scan_val); -internal void * str8_deserial_get_raw_ptr(String8 string, U64 off, U64 size); -internal U64 str8_deserial_read_cstr(String8 string, U64 off, String8 *cstr_out); -internal U64 str8_deserial_read_windows_utf16_string16(String8 string, U64 off, String16 *str_out); -internal U64 str8_deserial_read_block(String8 string, U64 off, U64 size, String8 *block_out); -internal U64 str8_deserial_read_uleb128(String8 string, U64 off, U64 *value_out); -internal U64 str8_deserial_read_sleb128(String8 string, U64 off, S64 *value_out); -#define str8_deserial_read_array(string, off, ptr, count) str8_deserial_read((string), (off), (ptr), sizeof(*(ptr))*(count), sizeof(*(ptr))) -#define str8_deserial_read_struct(string, off, ptr) str8_deserial_read_array(string, off, ptr, 1) - -#endif // BASE_STRINGS_H +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +#ifndef BASE_STRINGS_H +#define BASE_STRINGS_H + +//////////////////////////////// +//~ rjf: Third Party Includes + +#define STB_SPRINTF_DECORATE(name) raddbg_##name +#include "third_party/stb/stb_sprintf.h" + +//////////////////////////////// +//~ rjf: String Types + +typedef struct String8 String8; +struct String8 +{ + U8 *str; + U64 size; +}; + +typedef struct String16 String16; +struct String16 +{ + U16 *str; + U64 size; +}; + +typedef struct String32 String32; +struct String32 +{ + U32 *str; + U64 size; +}; + +//////////////////////////////// +//~ rjf: String List & Array Types + +typedef struct String8Node String8Node; +struct String8Node +{ + String8Node *next; + String8 string; +}; + +typedef struct String8MetaNode String8MetaNode; +struct String8MetaNode +{ + String8MetaNode *next; + String8Node *node; +}; + +typedef struct String8List String8List; +struct String8List +{ + String8Node *first; + String8Node *last; + U64 node_count; + U64 total_size; +}; + +typedef struct String8Array String8Array; +struct String8Array +{ + String8 *v; + U64 count; +}; + +//////////////////////////////// +//~ rjf: String Matching, Splitting, & Joining Types + +typedef U32 StringMatchFlags; +enum +{ + StringMatchFlag_CaseInsensitive = (1 << 0), + StringMatchFlag_RightSideSloppy = (1 << 1), + StringMatchFlag_SlashInsensitive = (1 << 2), +}; + +typedef U32 StringSplitFlags; +enum +{ + StringSplitFlag_KeepEmpties = (1 << 0), +}; + +typedef enum PathStyle +{ + PathStyle_Null, + PathStyle_Relative, + PathStyle_WindowsAbsolute, + PathStyle_UnixAbsolute, + +#if OS_WINDOWS + PathStyle_SystemAbsolute = PathStyle_WindowsAbsolute +#elif OS_LINUX + PathStyle_SystemAbsolute = PathStyle_UnixAbsolute +#else +# error "absolute path style is undefined for this OS" +#endif +} +PathStyle; + +typedef struct StringJoin StringJoin; +struct StringJoin +{ + String8 pre; + String8 sep; + String8 post; +}; + +//////////////////////////////// +//~ rjf: String Pair Types + +typedef struct String8TxtPtPair String8TxtPtPair; +struct String8TxtPtPair +{ + String8 string; + TxtPt pt; +}; + +//////////////////////////////// +//~ rjf: UTF Decoding Types + +typedef struct UnicodeDecode UnicodeDecode; +struct UnicodeDecode +{ + U32 inc; + U32 codepoint; +}; + +//////////////////////////////// +//~ rjf: String Fuzzy Matching Types + +typedef struct FuzzyMatchRangeNode FuzzyMatchRangeNode; +struct FuzzyMatchRangeNode +{ + FuzzyMatchRangeNode *next; + Rng1U64 range; +}; + +typedef struct FuzzyMatchRangeList FuzzyMatchRangeList; +struct FuzzyMatchRangeList +{ + FuzzyMatchRangeNode *first; + FuzzyMatchRangeNode *last; + U64 count; + U64 needle_part_count; + U64 total_dim; +}; + +//////////////////////////////// +//~ rjf: Character Classification & Conversion Functions + +internal B32 char_is_space(U8 c); +internal B32 char_is_upper(U8 c); +internal B32 char_is_lower(U8 c); +internal B32 char_is_alpha(U8 c); +internal B32 char_is_slash(U8 c); +internal B32 char_is_digit(U8 c, U32 base); +internal U8 char_to_lower(U8 c); +internal U8 char_to_upper(U8 c); +internal U8 char_to_correct_slash(U8 c); + +//////////////////////////////// +//~ rjf: C-String Measurement + +internal U64 cstring8_length(U8 *c); +internal U64 cstring16_length(U16 *c); +internal U64 cstring32_length(U32 *c); + +//////////////////////////////// +//~ rjf: String Constructors + +#define str8_lit(S) str8((U8*)(S), sizeof(S) - 1) +#define str8_lit_comp(S) {(U8*)(S), sizeof(S) - 1,} +#define str8_varg(S) (int)((S).size), ((S).str) + +#define str8_array(S,C) str8((U8*)(S), sizeof(*(S))*(C)) +#define str8_array_fixed(S) str8((U8*)(S), sizeof(S)) +#define str8_struct(S) str8((U8*)(S), sizeof(*(S))) + +internal String8 str8(U8 *str, U64 size); +internal String8 str8_range(U8 *first, U8 *one_past_last); +internal String8 str8_zero(void); +internal String16 str16(U16 *str, U64 size); +internal String16 str16_range(U16 *first, U16 *one_past_last); +internal String16 str16_zero(void); +internal String32 str32(U32 *str, U64 size); +internal String32 str32_range(U32 *first, U32 *one_past_last); +internal String32 str32_zero(void); +internal String8 str8_cstring(char *c); +internal String16 str16_cstring(U16 *c); +internal String32 str32_cstring(U32 *c); +internal String8 str8_cstring_capped(void *cstr, void *cap); +internal String16 str16_cstring_capped(void *cstr, void *cap); +internal String8 str8_cstring_capped_reverse(void *raw_start, void *raw_cap); + +//////////////////////////////// +//~ rjf: String Stylization + +internal String8 upper_from_str8(Arena *arena, String8 string); +internal String8 lower_from_str8(Arena *arena, String8 string); +internal String8 backslashed_from_str8(Arena *arena, String8 string); + +//////////////////////////////// +//~ rjf: String Matching + +#define str8_match_lit(a_lit, b, flags) str8_match(str8_lit(a_lit), (b), (flags)) +#define str8_match_cstr(a_cstr, b, flags) str8_match(str8_cstring(a_cstr), (b), (flags)) +internal B32 str8_match(String8 a, String8 b, StringMatchFlags flags); +internal U64 str8_find_needle(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags); +internal U64 str8_find_needle_reverse(String8 string, U64 start_pos, String8 needle, StringMatchFlags flags); +internal B32 str8_starts_with(String8 string, String8 start, StringMatchFlags flags); +#define str8_starts_with_lit(string, start_lit, flags) str8_starts_with((string), str8_lit(start_lit), (flags)) +internal B32 str8_ends_with(String8 string, String8 end, StringMatchFlags flags); +#define str8_ends_with_lit(string, end_lit, flags) str8_ends_with((string), str8_lit(end_lit), (flags)) + +//////////////////////////////// +//~ rjf: String Slicing + +internal String8 str8_substr(String8 str, Rng1U64 range); +internal String8 str8_prefix(String8 str, U64 size); +internal String8 str8_skip(String8 str, U64 amt); +internal String8 str8_postfix(String8 str, U64 size); +internal String8 str8_chop(String8 str, U64 amt); +internal String8 str8_skip_chop_whitespace(String8 string); + +//////////////////////////////// +//~ rjf: String Formatting & Copying + +internal String8 push_str8_cat(Arena *arena, String8 s1, String8 s2); +internal String8 push_str8_copy(Arena *arena, String8 s); +internal String8 push_str8fv(Arena *arena, char *fmt, va_list args); +internal String8 push_str8f(Arena *arena, char *fmt, ...); + +//////////////////////////////// +//~ rjf: String <=> Integer Conversions + +//- rjf: string -> integer +internal S64 sign_from_str8(String8 string, String8 *string_tail); +internal B32 str8_is_integer(String8 string, U32 radix); + +internal U64 u64_from_str8(String8 string, U32 radix); +internal S64 s64_from_str8(String8 string, U32 radix); +internal U32 u32_from_str8(String8 string, U32 radix); +internal S32 s32_from_str8(String8 string, U32 radix); +internal B32 try_u64_from_str8_c_rules(String8 string, U64 *x); +internal B32 try_s64_from_str8_c_rules(String8 string, S64 *x); + +//- rjf: integer -> string +internal String8 str8_from_memory_size(Arena *arena, U64 size); +internal String8 str8_from_count(Arena *arena, U64 count); +internal String8 str8_from_bits_u32(Arena *arena, U32 x); +internal String8 str8_from_bits_u64(Arena *arena, U64 x); +internal String8 str8_from_u64(Arena *arena, U64 u64, U32 radix, U8 min_digits, U8 digit_group_separator); +internal String8 str8_from_s64(Arena *arena, S64 s64, U32 radix, U8 min_digits, U8 digit_group_separator); + +//////////////////////////////// +//~ rjf: String <=> Float Conversions + +internal F64 f64_from_str8(String8 string); + +//////////////////////////////// +//~ rjf: String List Construction Functions + +internal String8Node* str8_list_push_node(String8List *list, String8Node *node); +internal String8Node* str8_list_push_node_set_string(String8List *list, String8Node *node, String8 string); +internal String8Node* str8_list_push_node_front(String8List *list, String8Node *node); +internal String8Node* str8_list_push_node_front_set_string(String8List *list, String8Node *node, String8 string); +internal String8Node* str8_list_push(Arena *arena, String8List *list, String8 string); +internal String8Node* str8_list_push_front(Arena *arena, String8List *list, String8 string); +internal void str8_list_concat_in_place(String8List *list, String8List *to_push); +internal String8Node* str8_list_push_aligner(Arena *arena, String8List *list, U64 min, U64 align); +internal String8Node* str8_list_pushf(Arena *arena, String8List *list, char *fmt, ...); +internal String8Node* str8_list_push_frontf(Arena *arena, String8List *list, char *fmt, ...); +internal String8List str8_list_copy(Arena *arena, String8List *list); +#define str8_list_first(list) ((list)->first ? (list)->first->string : str8_zero()) + +//////////////////////////////// +//~ rjf: String Splitting & Joining + +internal String8List str8_split(Arena *arena, String8 string, U8 *split_chars, U64 split_char_count, StringSplitFlags flags); +internal String8List str8_split_by_string_chars(Arena *arena, String8 string, String8 split_chars, StringSplitFlags flags); +internal String8List str8_list_split_by_string_chars(Arena *arena, String8List list, String8 split_chars, StringSplitFlags flags); +internal String8 str8_list_join(Arena *arena, String8List *list, StringJoin *optional_params); +internal void str8_list_from_flags(Arena *arena, String8List *list, U32 flags, String8 *flag_string_table, U32 flag_string_count); + +//////////////////////////////// +//~ rjf; String Arrays + +internal String8Array str8_array_from_list(Arena *arena, String8List *list); +internal String8Array str8_array_reserve(Arena *arena, U64 count); + +//////////////////////////////// +//~ rjf: String Path Helpers + +internal String8 str8_chop_last_slash(String8 string); +internal String8 str8_skip_last_slash(String8 string); +internal String8 str8_chop_last_dot(String8 string); +internal String8 str8_skip_last_dot(String8 string); + +internal PathStyle path_style_from_str8(String8 string); +internal String8List str8_split_path(Arena *arena, String8 string); +internal void str8_path_list_resolve_dots_in_place(String8List *path, PathStyle style); +internal String8 str8_path_list_join_by_style(Arena *arena, String8List *path, PathStyle style); + +internal String8TxtPtPair str8_txt_pt_pair_from_string(String8 string); + +//////////////////////////////// +//~ rjf: UTF-8 & UTF-16 Decoding/Encoding + +internal UnicodeDecode utf8_decode(U8 *str, U64 max); +internal UnicodeDecode utf16_decode(U16 *str, U64 max); +internal U32 utf8_encode(U8 *str, U32 codepoint); +internal U32 utf16_encode(U16 *str, U32 codepoint); +internal U32 utf8_from_utf32_single(U8 *buffer, U32 character); + +//////////////////////////////// +//~ rjf: Unicode String Conversions + +internal String8 str8_from_16(Arena *arena, String16 in); +internal String16 str16_from_8(Arena *arena, String8 in); +internal String8 str8_from_32(Arena *arena, String32 in); +internal String32 str32_from_8(Arena *arena, String8 in); + +//////////////////////////////// +//~ String -> Enum Conversions + +internal OperatingSystem operating_system_from_string(String8 string); + +//////////////////////////////// +//~ rjf: Basic Types & Space Enum -> String Conversions + +internal String8 string_from_dimension(Dimension dimension); +internal String8 string_from_side(Side side); +internal String8 string_from_operating_system(OperatingSystem os); +internal String8 string_from_arch(Arch arch); + +//////////////////////////////// +//~ rjf: Time Types -> String + +internal String8 string_from_week_day(WeekDay week_day); +internal String8 string_from_month(Month month); +internal String8 push_date_time_string(Arena *arena, DateTime *date_time); +internal String8 push_file_name_date_time_string(Arena *arena, DateTime *date_time); +internal String8 string_from_elapsed_time(Arena *arena, DateTime dt); + +//////////////////////////////// +//~ Globally Unique Ids + +internal String8 string_from_guid(Arena *arena, Guid guid); +internal B32 try_guid_from_string(String8 string, Guid *guid_out); +internal Guid guid_from_string(String8 string); + +//////////////////////////////// +//~ rjf: Basic Text Indentation + +internal String8 indented_from_string(Arena *arena, String8 string); + +//////////////////////////////// +//~ rjf: Text Escaping + +internal String8 escaped_from_raw_str8(Arena *arena, String8 string); +internal String8 raw_from_escaped_str8(Arena *arena, String8 string); + +//////////////////////////////// +//~ rjf: Text Wrapping + +internal String8List wrapped_lines_from_string(Arena *arena, String8 string, U64 first_line_max_width, U64 max_width, U64 wrap_indent); + +//////////////////////////////// +//~ rjf: String <-> Color + +internal String8 hex_string_from_rgba_4f32(Arena *arena, Vec4F32 rgba); +internal Vec4F32 rgba_from_hex_string_4f32(String8 hex_string); + +//////////////////////////////// +//~ rjf: String Fuzzy Matching + +internal FuzzyMatchRangeList fuzzy_match_find(Arena *arena, String8 needle, String8 haystack); +internal FuzzyMatchRangeList fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src); + +//////////////////////////////// +//~ NOTE(allen): Serialization Helpers + +internal void str8_serial_begin(Arena *arena, String8List *srl); +internal String8 str8_serial_end(Arena *arena, String8List *srl); +internal void str8_serial_write_to_dst(String8List *srl, void *out); +internal U64 str8_serial_push_align(Arena *arena, String8List *srl, U64 align); +internal void * str8_serial_push_size(Arena *arena, String8List *srl, U64 size); +internal void * str8_serial_push_data(Arena *arena, String8List *srl, void *data, U64 size); +internal void str8_serial_push_data_list(Arena *arena, String8List *srl, String8Node *first); +internal void str8_serial_push_u64(Arena *arena, String8List *srl, U64 x); +internal void str8_serial_push_u32(Arena *arena, String8List *srl, U32 x); +internal void str8_serial_push_u16(Arena *arena, String8List *srl, U16 x); +internal void str8_serial_push_u8(Arena *arena, String8List *srl, U8 x); +internal void str8_serial_push_cstr(Arena *arena, String8List *srl, String8 str); +internal void str8_serial_push_string(Arena *arena, String8List *srl, String8 str); +#define str8_serial_push_array(arena, srl, ptr, count) str8_serial_push_data(arena, srl, ptr, sizeof(*(ptr)) * (count)) +#define str8_serial_push_struct(arena, srl, ptr) str8_serial_push_array(arena, srl, ptr, 1) + +//////////////////////////////// +//~ rjf: Deserialization Helpers + +internal U64 str8_deserial_read(String8 string, U64 off, void *read_dst, U64 read_size, U64 granularity); +internal U64 str8_deserial_find_first_match(String8 string, U64 off, U16 scan_val); +internal void * str8_deserial_get_raw_ptr(String8 string, U64 off, U64 size); +internal U64 str8_deserial_read_cstr(String8 string, U64 off, String8 *cstr_out); +internal U64 str8_deserial_read_windows_utf16_string16(String8 string, U64 off, String16 *str_out); +internal U64 str8_deserial_read_block(String8 string, U64 off, U64 size, String8 *block_out); +internal U64 str8_deserial_read_uleb128(String8 string, U64 off, U64 *value_out); +internal U64 str8_deserial_read_sleb128(String8 string, U64 off, S64 *value_out); +#define str8_deserial_read_array(string, off, ptr, count) str8_deserial_read((string), (off), (ptr), sizeof(*(ptr))*(count), sizeof(*(ptr))) +#define str8_deserial_read_struct(string, off, ptr) str8_deserial_read_array(string, off, ptr, 1) + +#endif // BASE_STRINGS_H diff --git a/src/font_provider/font_provider_inc.c b/src/font_provider/font_provider_inc.c index d5c7665bd..c589b27ae 100644 --- a/src/font_provider/font_provider_inc.c +++ b/src/font_provider/font_provider_inc.c @@ -5,6 +5,8 @@ #if FP_BACKEND == FP_BACKEND_DWRITE # include "dwrite/font_provider_dwrite.c" +#elif FP_BACKEND == FP_BACKEND_FREETYPE +# include "freetype/font_provider_freetype.c" #else # error Font provider backend not specified. #endif diff --git a/src/font_provider/freetype/font_provider_freetype.c b/src/font_provider/freetype/font_provider_freetype.c index 8ac6bce5f..e3d7bbd7c 100644 --- a/src/font_provider/freetype/font_provider_freetype.c +++ b/src/font_provider/freetype/font_provider_freetype.c @@ -1,2 +1,467 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//////////////////////////////// +//~ dan: Backend Implementations + +fp_hook void +fp_init(void) +{ + ProfBeginFunction(); + + //- dan: initialize main state + { + Arena *arena = arena_alloc(); + fp_freetype_state = push_array(arena, FP_Freetype_State, 1); + fp_freetype_state->arena = arena; + } + + //- dan: initialize freetype library + FT_Error error = FT_Init_FreeType(&fp_freetype_state->library); + if(error) + { + log_infof("fp_init: FT_Init_FreeType failed with error %d", error); + } + // Enable LCD filtering for potentially better subpixel rendering + FT_Library_SetLcdFilter(fp_freetype_state->library, FT_LCD_FILTER_DEFAULT); + + ProfEnd(); +} + +fp_hook FP_Handle +fp_font_open(String8 path) +{ + ProfBeginFunction(); + FP_Handle handle = {0}; + FT_Face face; + Temp scratch = scratch_begin(0, 0); + + // Freetype needs a null-terminated path + String8 path_copy = push_str8_copy(scratch.arena, path); + + FT_Error error = FT_New_Face(fp_freetype_state->library, + (const char *)path_copy.str, + 0, // Use first face index + &face); + + if(!error) + { + handle.u64[0] = (U64)face; + } + else + { + log_infof("fp_font_open: FT_New_Face failed for path '%S' with error %d", path_copy, error); + } + + scratch_end(scratch); + ProfEnd(); + return handle; +} + +fp_hook FP_Handle +fp_font_open_from_static_data_string(String8 *data_ptr) +{ + ProfBeginFunction(); + FP_Handle handle = {0}; + FT_Face face = 0; // Initialize face to 0 + + if(data_ptr != 0 && data_ptr->str != 0 && data_ptr->size > 0) + { + // FT_New_Memory_Face requires a pointer to the data and its size. + FT_Error error = FT_New_Memory_Face(fp_freetype_state->library, + (const FT_Byte *)data_ptr->str, + (FT_Long)data_ptr->size, + 0, // Use first face index + &face); + + if(!error) + { + handle.u64[0] = (U64)face; + } + else + { + log_infof("fp_font_open_from_static_data_string: FT_New_Memory_Face failed with error %d", error); + face = 0; // Ensure face is 0 on error + } + } + // If data_ptr is null or invalid, handle remains zero. + + ProfEnd(); + return handle; +} + +fp_hook void +fp_font_close(FP_Handle handle) +{ + ProfBeginFunction(); + FT_Face face = (FT_Face)handle.u64[0]; + if(face != 0) + { + FT_Error error = FT_Done_Face(face); + if(error) + { + log_infof("fp_font_close: FT_Done_Face failed with error %d", error); + } + } + ProfEnd(); +} + +fp_hook FP_Metrics +fp_metrics_from_font(FP_Handle handle) +{ + ProfBeginFunction(); + FP_Metrics result = {0}; + FT_Face face = (FT_Face)handle.u64[0]; + + if(face != 0) + { + result.design_units_per_em = (F32)face->units_per_EM; + result.ascent = (F32)face->ascender; + // FreeType descender is typically negative, FP_Metrics likely expects positive. + result.descent = (F32)face->descender; + if (result.descent < 0) + { + result.descent = -result.descent; + } + // face->height is the distance between baselines. line_gap = height - (ascender - descender) + result.line_gap = (F32)(face->height - (face->ascender - face->descender)); + + // Capital height retrieval from OS/2 table + result.capital_height = 0; // Default to 0 + FT_Byte* os2_table = (FT_Byte*)FT_Get_Sfnt_Table(face, ft_sfnt_os2); + if (os2_table) + { + // OS/2 table version determines where sCapHeight is located. + // Read version (ushort at offset 0) + // We need fields up to sCapHeight (offset 88 in version >= 2) + // Check table size first. Minimum size for version 0/1 is 78 bytes. + // For version >= 2, minimum size is 96 bytes. + // FT_ULong length = 0; // FT_Get_Sfnt_Table does not provide length directly + // Need another way to check table size or rely on FreeType's internal handling. + // Let's assume FreeType provides a valid pointer if the table exists. + + FT_UShort version = (FT_UShort)((os2_table[0] << 8) | os2_table[1]); + + // sCapHeight is at offset 88 (bytes) in OS/2 version 2 and later. + // It's optional and might be 0 even if present. + // Check if version >= 2 (assuming table is large enough) + // A more robust check would involve FT_Load_Sfnt_Table if length was needed. + if (version >= 2) + { + // Read sCapHeight (short at offset 88) + FT_Short cap_height_units = (FT_Short)((os2_table[88] << 8) | os2_table[89]); + if (cap_height_units > 0) // Use only if positive + { + result.capital_height = (F32)cap_height_units; + } + } + + // If cap height is still 0 (not found or invalid), use ascender as fallback + if (result.capital_height == 0) + { + result.capital_height = (F32)face->ascender; + } + } + else + { + // Fallback if OS/2 table doesn't exist: Use ascender + result.capital_height = (F32)face->ascender; + } + } + + ProfEnd(); + return result; +} + +fp_hook NO_ASAN FP_RasterResult +fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, String8 string) +{ + ProfBeginFunction(); + FP_RasterResult result = {0}; + FT_Face face = (FT_Face)font_handle.u64[0]; + if (face == 0) + { + ProfEnd(); + return result; + } + + Temp scratch = scratch_begin(&arena, 1); + String32 string32 = str32_from_8(scratch.arena, string); + if (string32.size == 0) + { + scratch_end(scratch); + ProfEnd(); + return result; + } + + //- dan: Define glyph render info storage + typedef struct GlyphRenderInfo GlyphRenderInfo; + struct GlyphRenderInfo { + FT_UInt glyph_index; + S32 render_pen_x; // Pen position before this glyph (26.6 fixed point) + S32 advance_x; // Advance width for this glyph (26.6 fixed point) + }; + + //- dan: Set pixel size + FT_UInt pixel_height = (FT_UInt)(size + 0.5f); + FT_Error error = FT_Set_Pixel_Sizes(face, 0, pixel_height); + if (error) + { + log_infof("fp_raster: FT_Set_Pixel_Sizes failed for size %.2f with error %d", size, error); + scratch_end(scratch); + ProfEnd(); + return result; + } + + //- dan: Determine FreeType load flags based on FP_RasterFlags + FT_Int32 load_flags = FT_LOAD_DEFAULT | FT_LOAD_COLOR; + FT_Render_Mode render_mode = FT_RENDER_MODE_NORMAL; + // Determine the appropriate kerning mode based on hinting + FT_Kerning_Mode kerning_mode = FT_KERNING_DEFAULT; // Default (scaled, hinted) + + switch (flags) + { + case 0: // Sharp, Unhinted + load_flags |= FT_LOAD_NO_HINTING | FT_LOAD_TARGET_NORMAL; + render_mode = FT_RENDER_MODE_NORMAL; + kerning_mode = FT_KERNING_UNFITTED; // Unhinted kerning + break; + + case FP_RasterFlag_Hinted: // Sharp, Hinted + load_flags |= FT_LOAD_TARGET_NORMAL; // Hinting enabled + render_mode = FT_RENDER_MODE_NORMAL; + kerning_mode = FT_KERNING_DEFAULT; // Hinted kerning + break; + + case FP_RasterFlag_Smooth: // Smooth, Unhinted + load_flags |= FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LCD; + render_mode = FT_RENDER_MODE_LCD; + kerning_mode = FT_KERNING_UNFITTED; // Unhinted kerning + // NOTE: Current implementation converts LCD to grayscale alpha. + break; + + case FP_RasterFlag_Smooth | FP_RasterFlag_Hinted: // Smooth, Hinted + load_flags |= FT_LOAD_TARGET_LCD; // Hinting enabled + render_mode = FT_RENDER_MODE_LCD; + kerning_mode = FT_KERNING_DEFAULT; // Hinted kerning + // NOTE: Current implementation converts LCD to grayscale alpha. + break; + + default: // Fallback to default (Sharp, Hinted) + load_flags |= FT_LOAD_TARGET_NORMAL; + render_mode = FT_RENDER_MODE_NORMAL; + kerning_mode = FT_KERNING_DEFAULT; + break; + } + + //- dan: First pass: Calculate layout metrics and store glyph positions + GlyphRenderInfo *glyph_infos = push_array(scratch.arena, GlyphRenderInfo, string32.size); + MemoryZero(glyph_infos, sizeof(GlyphRenderInfo) * string32.size); + + S32 total_width_fixed = 0; // Use 26.6 for total width calculation + S32 max_ascent = 0; + S32 max_descent = 0; + S32 pen_x = 0; // Use 26.6 fixed-point + FT_UInt prev_glyph_index = 0; + + for (U64 i = 0; i < string32.size; ++i) + { + FT_UInt glyph_index = FT_Get_Char_Index(face, string32.str[i]); + glyph_infos[i].glyph_index = glyph_index; + + // Load glyph metrics *using the final load flags* + // FT_LOAD_NO_BITMAP could optimize this, but FT_LOAD_DEFAULT is okay. + error = FT_Load_Glyph(face, glyph_index, load_flags); // <<< USE CONSISTENT load_flags + if (error) { + log_infof("fp_raster: FT_Load_Glyph (metrics pass) failed for glyph index %u (char 0x%x) with error %d", glyph_index, string32.str[i], error); + glyph_infos[i].render_pen_x = pen_x; + glyph_infos[i].advance_x = 0; + prev_glyph_index = glyph_index; + continue; + } + + FT_GlyphSlot slot = face->glyph; + + // Apply kerning *using the consistent kerning mode* + if (prev_glyph_index && glyph_index && FT_HAS_KERNING(face)) + { + FT_Vector delta; + FT_Get_Kerning(face, prev_glyph_index, glyph_index, kerning_mode, &delta); // <<< USE CONSISTENT kerning_mode + pen_x += delta.x; + } + + glyph_infos[i].render_pen_x = pen_x; // Store pen position before advance + + // Use metrics (ascent/descent) from the loaded glyph (now potentially hinted) + S32 glyph_ascent = slot->bitmap_top; + S32 glyph_descent = (S32)slot->bitmap.rows - slot->bitmap_top; + if (glyph_ascent > max_ascent) { max_ascent = glyph_ascent; } + if (glyph_descent > max_descent) { max_descent = glyph_descent; } + + // Use advance from the potentially hinted glyph + pen_x += slot->advance.x; + glyph_infos[i].advance_x = slot->advance.x; // Store potentially hinted advance + + prev_glyph_index = glyph_index; + } + + // Calculate total width and height based on layout (using potentially hinted metrics) + total_width_fixed = pen_x; + S32 total_width_pixels = (total_width_fixed + 63) >> 6; + + // TODO: Re-evaluate this padding/alignment for clarity/necessity + total_width_pixels += 2; + total_width_pixels += 7; + total_width_pixels -= total_width_pixels % 8; + + S32 total_height = max_ascent + max_descent; + if (total_width_pixels <= 0 || total_height <= 0) + { + // Handle zero-size case more carefully + scratch_end(scratch); + ProfEnd(); + result.advance = (F32)(total_width_fixed >> 6); // Still report advance + result.atlas_dim = v2s16(0,0); + result.atlas = 0; // Or point to zero-size allocation? Original was unclear. + return result; + } + + //- dan: Allocate atlas buffer + result.atlas_dim = v2s16((S16)total_width_pixels, (S16)total_height); + result.atlas = push_array(arena, U8, total_width_pixels * total_height * 4); + MemoryZero(result.atlas, (U64)total_width_pixels * total_height * 4); + U8 *out_base = (U8 *)result.atlas; + U64 out_pitch = (U64)total_width_pixels * 4; + + //- dan: Second pass: Render and blit glyphs using stored positions + U64 non_empty_pixel_count = 0; + + for (U64 i = 0; i < string32.size; ++i) + { + FT_UInt glyph_index = glyph_infos[i].glyph_index; + S32 render_pen_x_fixed = glyph_infos[i].render_pen_x; // Stored 26.6 pen position + + // Load glyph again (might be necessary if FT_Load_Glyph doesn't cache everything) + // or ensure first pass loaded everything needed. Reloading is safer. + error = FT_Load_Glyph(face, glyph_index, load_flags); // <<< Use final load_flags + if (error) { + // log_infof(...) + continue; + } + + // Render glyph if not already a bitmap + if (face->glyph->format != FT_GLYPH_FORMAT_BITMAP) + { + error = FT_Render_Glyph(face->glyph, render_mode); // <<< Use final render_mode + if (error) { + // log_infof(...) + continue; + } + } + + FT_GlyphSlot slot = face->glyph; + FT_Bitmap *bitmap = &slot->bitmap; + + // Calculate blit position using consistent metrics + S32 blit_x = (render_pen_x_fixed >> 6) + slot->bitmap_left; + S32 blit_y = max_ascent - slot->bitmap_top; // max_ascent also calculated consistently now + + // Blit the bitmap to the RGBA atlas + U8 *in_row = bitmap->buffer; + for (unsigned int y = 0; y < bitmap->rows; ++y) + { + S32 atlas_y = blit_y + (S32)y; + if (atlas_y >= 0 && atlas_y < total_height) // Clip vertically + { + U8 *out_row = out_base + atlas_y * out_pitch; + // Handle different pixel modes + if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) + { + U8 *in_pixel_ptr = in_row; + for (unsigned int x = 0; x < bitmap->width; ++x) + { + S32 atlas_x = blit_x + (S32)x; + if (atlas_x >= 0 && atlas_x < total_width_pixels) // Clip horizontally + { + U8 *out_pixel = out_row + atlas_x * 4; + U8 in_val = *in_pixel_ptr; + out_pixel[0] = 255; + out_pixel[1] = 255; + out_pixel[2] = 255; + out_pixel[3] = in_val; + if (in_val > 0) { + non_empty_pixel_count++; + } + } + in_pixel_ptr++; + } + } + else if (bitmap->pixel_mode == FT_PIXEL_MODE_LCD) + { + U8 *in_pixel_rgb = in_row; + for (unsigned int x = 0; x < bitmap->width; ++x) + { + S32 atlas_x = blit_x + (S32)x; + if (atlas_x >= 0 && atlas_x < total_width_pixels) // Clip horizontally + { + U8 *out_pixel = out_row + atlas_x * 4; + // Convert RGB coverage to single alpha (suboptimal for true subpixel) + U8 r = in_pixel_rgb[0]; + U8 g = in_pixel_rgb[1]; + U8 b = in_pixel_rgb[2]; + U32 alpha_u32 = (54*r + 183*g + 19*b) >> 8; + U8 alpha = (U8)(alpha_u32 > 255 ? 255 : alpha_u32); + out_pixel[0] = 255; + out_pixel[1] = 255; + out_pixel[2] = 255; + out_pixel[3] = alpha; + if (alpha > 0) { + non_empty_pixel_count++; + } + } + in_pixel_rgb += 3; + } + } + else if (bitmap->pixel_mode == FT_PIXEL_MODE_BGRA) // BGRA Color Bitmap + { + U8 *in_pixel_bgra = in_row; + for (unsigned int x = 0; x < bitmap->width; ++x) + { + S32 atlas_x = blit_x + (S32)x; + if (atlas_x >= 0 && atlas_x < total_width_pixels) // Clip horizontally + { + U8 *out_pixel = out_row + atlas_x * 4; + U8 b = in_pixel_bgra[0]; + U8 g = in_pixel_bgra[1]; + U8 r = in_pixel_bgra[2]; + U8 a = in_pixel_bgra[3]; + out_pixel[0] = r; + out_pixel[1] = g; + out_pixel[2] = b; + out_pixel[3] = a; + if (a > 0) { + non_empty_pixel_count++; + } + } + in_pixel_bgra += 4; + } + } + } + in_row += bitmap->pitch; + } + } + + //- dan: Finalize result + result.advance = (F32)(total_width_fixed >> 6); // Use final fixed-point width from first pass + + // If nothing visible rendered, ensure atlas dimensions reflect that. + if (non_empty_pixel_count == 0) + { + result.atlas_dim = v2s16(0, 0); + // Consider freeing result.atlas or ensuring it's handled correctly downstream. + } + + scratch_end(scratch); + ProfEnd(); + return result; +} diff --git a/src/font_provider/freetype/font_provider_freetype.h b/src/font_provider/freetype/font_provider_freetype.h index 2efe2a500..ae7a2dee4 100644 --- a/src/font_provider/freetype/font_provider_freetype.h +++ b/src/font_provider/freetype/font_provider_freetype.h @@ -4,4 +4,33 @@ #ifndef FONT_PROVIDER_FREETYPE_H #define FONT_PROVIDER_FREETYPE_H +//////////////////////////////// +//~ rjf: Includes + +#pragma push_macro("internal") +#undef internal +#include +#include FT_FREETYPE_H +#include FT_LCD_FILTER_H // For FT_Library_SetLcdFilter, FT_LCD_FILTER_DEFAULT +#include FT_SFNT_NAMES_H // For FT_Get_Sfnt_Table, FT_SFNT_OS2 +#include FT_TRUETYPE_TABLES_H // For TT_OS2, FT_SFNT_OS2 +#pragma pop_macro("internal") + +#define internal static + +//////////////////////////////// +//~ rjf: Types + +typedef struct FP_Freetype_State FP_Freetype_State; +struct FP_Freetype_State +{ + Arena *arena; + FT_Library library; +}; + +//////////////////////////////// +//~ rjf: Globals + +global FP_Freetype_State *fp_freetype_state = 0; + #endif // FONT_PROVIDER_FREETYPE_H diff --git a/src/os/core/linux/os_core_linux.c b/src/os/core/linux/os_core_linux.c index 3b2c73b1c..654dbe5df 100644 --- a/src/os/core/linux/os_core_linux.c +++ b/src/os/core/linux/os_core_linux.c @@ -1,1330 +1,2398 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -//////////////////////////////// -//~ rjf: Helpers - -internal DateTime -os_lnx_date_time_from_tm(tm in, U32 msec) -{ - DateTime dt = {0}; - dt.sec = in.tm_sec; - dt.min = in.tm_min; - dt.hour = in.tm_hour; - dt.day = in.tm_mday-1; - dt.mon = in.tm_mon; - dt.year = in.tm_year+1900; - dt.msec = msec; - return dt; -} - -internal tm -os_lnx_tm_from_date_time(DateTime dt) -{ - tm result = {0}; - result.tm_sec = dt.sec; - result.tm_min = dt.min; - result.tm_hour= dt.hour; - result.tm_mday= dt.day+1; - result.tm_mon = dt.mon; - result.tm_year= dt.year-1900; - return result; -} - -internal timespec -os_lnx_timespec_from_date_time(DateTime dt) -{ - tm tm_val = os_lnx_tm_from_date_time(dt); - time_t seconds = timegm(&tm_val); - timespec result = {0}; - result.tv_sec = seconds; - return result; -} - -internal DenseTime -os_lnx_dense_time_from_timespec(timespec in) -{ - DenseTime result = 0; - { - struct tm tm_time = {0}; - gmtime_r(&in.tv_sec, &tm_time); - DateTime date_time = os_lnx_date_time_from_tm(tm_time, in.tv_nsec/Million(1)); - result = dense_time_from_date_time(date_time); - } - return result; -} - -internal FileProperties -os_lnx_file_properties_from_stat(struct stat *s) -{ - FileProperties props = {0}; - props.size = s->st_size; - props.created = os_lnx_dense_time_from_timespec(s->st_ctim); - props.modified = os_lnx_dense_time_from_timespec(s->st_mtim); - if(s->st_mode & S_IFDIR) - { - props.flags |= FilePropertyFlag_IsFolder; - } - return props; -} - -internal void -os_lnx_safe_call_sig_handler(int x) -{ - OS_LNX_SafeCallChain *chain = os_lnx_safe_call_chain; - if(chain != 0 && chain->fail_handler != 0) - { - chain->fail_handler(chain->ptr); - } - abort(); -} - -//////////////////////////////// -//~ rjf: Entities - -internal OS_LNX_Entity * -os_lnx_entity_alloc(OS_LNX_EntityKind kind) -{ - OS_LNX_Entity *entity = 0; - DeferLoop(pthread_mutex_lock(&os_lnx_state.entity_mutex), - pthread_mutex_unlock(&os_lnx_state.entity_mutex)) - { - entity = os_lnx_state.entity_free; - if(entity) - { - SLLStackPop(os_lnx_state.entity_free); - } - else - { - entity = push_array_no_zero(os_lnx_state.entity_arena, OS_LNX_Entity, 1); - } - } - MemoryZeroStruct(entity); - entity->kind = kind; - return entity; -} - -internal void -os_lnx_entity_release(OS_LNX_Entity *entity) -{ - DeferLoop(pthread_mutex_lock(&os_lnx_state.entity_mutex), - pthread_mutex_unlock(&os_lnx_state.entity_mutex)) - { - SLLStackPush(os_lnx_state.entity_free, entity); - } -} - -//////////////////////////////// -//~ rjf: Thread Entry Point - -internal void * -os_lnx_thread_entry_point(void *ptr) -{ - OS_LNX_Entity *entity = (OS_LNX_Entity *)ptr; - OS_ThreadFunctionType *func = entity->thread.func; - void *thread_ptr = entity->thread.ptr; - TCTX tctx_; - tctx_init_and_equip(&tctx_); - func(thread_ptr); - tctx_release(); - return 0; -} - -//////////////////////////////// -//~ rjf: @os_hooks System/Process Info (Implemented Per-OS) - -internal OS_SystemInfo * -os_get_system_info(void) -{ - return &os_lnx_state.system_info; -} - -internal OS_ProcessInfo * -os_get_process_info(void) -{ - return &os_lnx_state.process_info; -} - -internal String8 -os_get_current_path(Arena *arena) -{ - char *cwdir = getcwd(0, 0); - String8 string = push_str8_copy(arena, str8_cstring(cwdir)); - free(cwdir); - return string; -} - -internal U32 -os_get_process_start_time_unix(void) -{ - Temp scratch = scratch_begin(0,0); - U64 start_time = 0; - pid_t pid = getpid(); - String8 path = push_str8f(scratch.arena, "/proc/%u", pid); - struct stat st; - int err = stat((char*)path.str, &st); - if(err == 0) - { - start_time = st.st_mtime; - } - scratch_end(scratch); - return (U32)start_time; -} - -//////////////////////////////// -//~ rjf: @os_hooks Memory Allocation (Implemented Per-OS) - -//- rjf: basic - -internal void * -os_reserve(U64 size) -{ - void *result = mmap(0, size, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); - if(result == MAP_FAILED) - { - result = 0; - } - return result; -} - -internal B32 -os_commit(void *ptr, U64 size) -{ - mprotect(ptr, size, PROT_READ|PROT_WRITE); - return 1; -} - -internal void -os_decommit(void *ptr, U64 size) -{ - madvise(ptr, size, MADV_DONTNEED); - mprotect(ptr, size, PROT_NONE); -} - -internal void -os_release(void *ptr, U64 size) -{ - munmap(ptr, size); -} - -//- rjf: large pages - -internal void * -os_reserve_large(U64 size) -{ - void *result = mmap(0, size, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0); - if(result == MAP_FAILED) - { - result = 0; - } - return result; -} - -internal B32 -os_commit_large(void *ptr, U64 size) -{ - mprotect(ptr, size, PROT_READ|PROT_WRITE); - return 1; -} - -//////////////////////////////// -//~ rjf: @os_hooks Thread Info (Implemented Per-OS) - -internal U32 -os_tid(void) -{ - U32 result = gettid(); - return result; -} - -internal void -os_set_thread_name(String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String8 name_copy = push_str8_copy(scratch.arena, name); - pthread_t current_thread = pthread_self(); - pthread_setname_np(current_thread, (char *)name_copy.str); - scratch_end(scratch); -} - -//////////////////////////////// -//~ rjf: @os_hooks Aborting (Implemented Per-OS) - -internal void -os_abort(S32 exit_code) -{ - exit(exit_code); -} - -//////////////////////////////// -//~ rjf: @os_hooks File System (Implemented Per-OS) - -//- rjf: files - -internal OS_Handle -os_file_open(OS_AccessFlags flags, String8 path) -{ - Temp scratch = scratch_begin(0, 0); - String8 path_copy = push_str8_copy(scratch.arena, path); - int lnx_flags = 0; - if(flags & OS_AccessFlag_Read && flags & OS_AccessFlag_Write) - { - lnx_flags = O_RDWR; - } - else if(flags & OS_AccessFlag_Write) - { - lnx_flags = O_WRONLY; - } - else if(flags & OS_AccessFlag_Read) - { - lnx_flags = O_RDONLY; - } - if(flags & OS_AccessFlag_Append) - { - lnx_flags |= O_APPEND; - } - if(flags & (OS_AccessFlag_Write|OS_AccessFlag_Append)) - { - lnx_flags |= O_CREAT; - } - int fd = open((char *)path_copy.str, lnx_flags, 0755); - OS_Handle handle = {0}; - if(fd != -1) - { - handle.u64[0] = fd; - } - scratch_end(scratch); - return handle; -} - -internal void -os_file_close(OS_Handle file) -{ - if(os_handle_match(file, os_handle_zero())) { return; } - int fd = (int)file.u64[0]; - close(fd); -} - -internal U64 -os_file_read(OS_Handle file, Rng1U64 rng, void *out_data) -{ - if(os_handle_match(file, os_handle_zero())) { return 0; } - int fd = (int)file.u64[0]; - U64 total_num_bytes_to_read = dim_1u64(rng); - U64 total_num_bytes_read = 0; - U64 total_num_bytes_left_to_read = total_num_bytes_to_read; - for(;total_num_bytes_left_to_read > 0;) - { - int read_result = pread(fd, (U8 *)out_data + total_num_bytes_read, total_num_bytes_left_to_read, rng.min + total_num_bytes_read); - if(read_result >= 0) - { - total_num_bytes_read += read_result; - total_num_bytes_left_to_read -= read_result; - } - else if(errno != EINTR) - { - break; - } - } - return total_num_bytes_read; -} - -internal U64 -os_file_write(OS_Handle file, Rng1U64 rng, void *data) -{ - if(os_handle_match(file, os_handle_zero())) { return 0; } - int fd = (int)file.u64[0]; - U64 total_num_bytes_to_write = dim_1u64(rng); - U64 total_num_bytes_written = 0; - U64 total_num_bytes_left_to_write = total_num_bytes_to_write; - for(;total_num_bytes_left_to_write > 0;) - { - int write_result = pwrite(fd, (U8 *)data + total_num_bytes_written, total_num_bytes_left_to_write, rng.min + total_num_bytes_written); - if(write_result >= 0) - { - total_num_bytes_written += write_result; - total_num_bytes_left_to_write -= write_result; - } - else if(errno != EINTR) - { - break; - } - } - return total_num_bytes_written; -} - -internal B32 -os_file_set_times(OS_Handle file, DateTime date_time) -{ - if(os_handle_match(file, os_handle_zero())) { return 0; } - int fd = (int)file.u64[0]; - timespec time = os_lnx_timespec_from_date_time(date_time); - timespec times[2] = {time, time}; - int futimens_result = futimens(fd, times); - B32 good = (futimens_result != -1); - return good; -} - -internal FileProperties -os_properties_from_file(OS_Handle file) -{ - if(os_handle_match(file, os_handle_zero())) { return (FileProperties){0}; } - int fd = (int)file.u64[0]; - struct stat fd_stat = {0}; - int fstat_result = fstat(fd, &fd_stat); - FileProperties props = {0}; - if(fstat_result != -1) - { - props = os_lnx_file_properties_from_stat(&fd_stat); - } - return props; -} - -internal OS_FileID -os_id_from_file(OS_Handle file) -{ - if(os_handle_match(file, os_handle_zero())) { return (OS_FileID){0}; } - int fd = (int)file.u64[0]; - struct stat fd_stat = {0}; - int fstat_result = fstat(fd, &fd_stat); - OS_FileID id = {0}; - if(fstat_result != -1) - { - id.v[0] = fd_stat.st_dev; - id.v[1] = fd_stat.st_ino; - } - return id; -} - -internal B32 -os_delete_file_at_path(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - B32 result = 0; - String8 path_copy = push_str8_copy(scratch.arena, path); - if(remove((char*)path_copy.str) != -1) - { - result = 1; - } - scratch_end(scratch); - return result; -} - -internal B32 -os_copy_file_path(String8 dst, String8 src) -{ - B32 result = 0; - OS_Handle src_h = os_file_open(OS_AccessFlag_Read, src); - OS_Handle dst_h = os_file_open(OS_AccessFlag_Write, dst); - if(!os_handle_match(src_h, os_handle_zero()) && - !os_handle_match(dst_h, os_handle_zero())) - { - int src_fd = (int)src_h.u64[0]; - int dst_fd = (int)dst_h.u64[0]; - FileProperties src_props = os_properties_from_file(src_h); - U64 size = src_props.size; - U64 total_bytes_copied = 0; - U64 bytes_left_to_copy = size; - for(;bytes_left_to_copy > 0;) - { - off_t sendfile_off = total_bytes_copied; - int send_result = sendfile(dst_fd, src_fd, &sendfile_off, bytes_left_to_copy); - if(send_result <= 0) - { - break; - } - U64 bytes_copied = (U64)send_result; - bytes_left_to_copy -= bytes_copied; - total_bytes_copied += bytes_copied; - } - } - os_file_close(src_h); - os_file_close(dst_h); - return result; -} - -internal String8 -os_full_path_from_path(Arena *arena, String8 path) -{ - Temp scratch = scratch_begin(&arena, 1); - String8 path_copy = push_str8_copy(scratch.arena, path); - char buffer[PATH_MAX] = {0}; - realpath((char *)path_copy.str, buffer); - String8 result = push_str8_copy(arena, str8_cstring(buffer)); - scratch_end(scratch); - return result; -} - -internal B32 -os_file_path_exists(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - String8 path_copy = push_str8_copy(scratch.arena, path); - int access_result = access((char *)path_copy.str, F_OK); - B32 result = 0; - if(access_result == 0) - { - result = 1; - } - scratch_end(scratch); - return result; -} - -internal B32 -os_folder_path_exists(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - B32 exists = 0; - String8 path_copy = push_str8_copy(scratch.arena, path); - DIR *handle = opendir((char*)path_copy.str); - if(handle) - { - closedir(handle); - exists = 1; - } - scratch_end(scratch); - return exists; -} - -internal FileProperties -os_properties_from_file_path(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - String8 path_copy = push_str8_copy(scratch.arena, path); - struct stat f_stat = {0}; - int stat_result = stat((char *)path_copy.str, &f_stat); - FileProperties props = {0}; - if(stat_result != -1) - { - props = os_lnx_file_properties_from_stat(&f_stat); - } - scratch_end(scratch); - return props; -} - -//- rjf: file maps - -internal OS_Handle -os_file_map_open(OS_AccessFlags flags, OS_Handle file) -{ - OS_Handle map = file; - return map; -} - -internal void -os_file_map_close(OS_Handle map) -{ - // NOTE(rjf): nothing to do; `map` handles are the same as `file` handles in - // the linux implementation (on Windows they require separate handles) -} - -internal void * -os_file_map_view_open(OS_Handle map, OS_AccessFlags flags, Rng1U64 range) -{ - if(os_handle_match(map, os_handle_zero())) { return 0; } - int fd = (int)map.u64[0]; - int prot_flags = 0; - if(flags & OS_AccessFlag_Write) { prot_flags |= PROT_WRITE; } - if(flags & OS_AccessFlag_Read) { prot_flags |= PROT_READ; } - int map_flags = MAP_PRIVATE; - void *base = mmap(0, dim_1u64(range), prot_flags, map_flags, fd, range.min); - if(base == MAP_FAILED) - { - base = 0; - } - return base; -} - -internal void -os_file_map_view_close(OS_Handle map, void *ptr, Rng1U64 range) -{ - munmap(ptr, dim_1u64(range)); -} - -//- rjf: directory iteration - -internal OS_FileIter * -os_file_iter_begin(Arena *arena, String8 path, OS_FileIterFlags flags) -{ - OS_FileIter *base_iter = push_array(arena, OS_FileIter, 1); - base_iter->flags = flags; - OS_LNX_FileIter *iter = (OS_LNX_FileIter *)base_iter->memory; - { - String8 path_copy = push_str8_copy(arena, path); - iter->dir = opendir((char *)path_copy.str); - iter->path = path_copy; - } - return base_iter; -} - -internal B32 -os_file_iter_next(Arena *arena, OS_FileIter *iter, OS_FileInfo *info_out) -{ - B32 good = 0; - OS_LNX_FileIter *lnx_iter = (OS_LNX_FileIter *)iter->memory; - for(;;) - { - // rjf: get next entry - lnx_iter->dp = readdir(lnx_iter->dir); - good = (lnx_iter->dp != 0); - - // rjf: unpack entry info - struct stat st = {0}; - int stat_result = 0; - if(good) - { - Temp scratch = scratch_begin(&arena, 1); - String8 full_path = push_str8f(scratch.arena, "%S/%s", lnx_iter->path, lnx_iter->dp->d_name); - stat_result = stat((char *)full_path.str, &st); - scratch_end(scratch); - } - - // rjf: determine if filtered - B32 filtered = 0; - if(good) - { - filtered = ((st.st_mode == S_IFDIR && iter->flags & OS_FileIterFlag_SkipFolders) || - (st.st_mode == S_IFREG && iter->flags & OS_FileIterFlag_SkipFiles) || - (lnx_iter->dp->d_name[0] == '.' && lnx_iter->dp->d_name[1] == 0) || - (lnx_iter->dp->d_name[0] == '.' && lnx_iter->dp->d_name[1] == '.' && lnx_iter->dp->d_name[2] == 0)); - } - - // rjf: output & exit, if good & unfiltered - if(good && !filtered) - { - info_out->name = push_str8_copy(arena, str8_cstring(lnx_iter->dp->d_name)); - if(stat_result != -1) - { - info_out->props = os_lnx_file_properties_from_stat(&st); - } - break; - } - - // rjf: exit if not good - if(!good) - { - break; - } - } - return good; -} - -internal void -os_file_iter_end(OS_FileIter *iter) -{ - OS_LNX_FileIter *lnx_iter = (OS_LNX_FileIter *)iter->memory; - closedir(lnx_iter->dir); -} - -//- rjf: directory creation - -internal B32 -os_make_directory(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - B32 result = 0; - String8 path_copy = push_str8_copy(scratch.arena, path); - if(mkdir((char*)path_copy.str, 0755) != -1) - { - result = 1; - } - scratch_end(scratch); - return result; -} - -//////////////////////////////// -//~ rjf: @os_hooks Shared Memory (Implemented Per-OS) - -internal OS_Handle -os_shared_memory_alloc(U64 size, String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String8 name_copy = push_str8_copy(scratch.arena, name); - int id = shm_open((char *)name_copy.str, O_RDWR, 0); - ftruncate(id, size); - OS_Handle result = {(U64)id}; - scratch_end(scratch); - return result; -} - -internal OS_Handle -os_shared_memory_open(String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String8 name_copy = push_str8_copy(scratch.arena, name); - int id = shm_open((char *)name_copy.str, O_RDWR, 0); - OS_Handle result = {(U64)id}; - scratch_end(scratch); - return result; -} - -internal void -os_shared_memory_close(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())){return;} - int id = (int)handle.u64[0]; - close(id); -} - -internal void * -os_shared_memory_view_open(OS_Handle handle, Rng1U64 range) -{ - if(os_handle_match(handle, os_handle_zero())){return 0;} - int id = (int)handle.u64[0]; - void *base = mmap(0, dim_1u64(range), PROT_READ|PROT_WRITE, MAP_SHARED, id, range.min); - if(base == MAP_FAILED) - { - base = 0; - } - return base; -} - -internal void -os_shared_memory_view_close(OS_Handle handle, void *ptr, Rng1U64 range) -{ - if(os_handle_match(handle, os_handle_zero())){return;} - munmap(ptr, dim_1u64(range)); -} - -//////////////////////////////// -//~ rjf: @os_hooks Time (Implemented Per-OS) - -internal U64 -os_now_microseconds(void) -{ - struct timespec t; - clock_gettime(CLOCK_MONOTONIC, &t); - U64 result = t.tv_sec*Million(1) + (t.tv_nsec/Thousand(1)); - return result; -} - -internal U32 -os_now_unix(void) -{ - time_t t = time(0); - return (U32)t; -} - -internal DateTime -os_now_universal_time(void) -{ - time_t t = 0; - time(&t); - struct tm universal_tm = {0}; - gmtime_r(&t, &universal_tm); - DateTime result = os_lnx_date_time_from_tm(universal_tm, 0); - return result; -} - -internal DateTime -os_universal_time_from_local(DateTime *date_time) -{ - // rjf: local DateTime -> universal time_t - tm local_tm = os_lnx_tm_from_date_time(*date_time); - local_tm.tm_isdst = -1; - time_t universal_t = mktime(&local_tm); - - // rjf: universal time_t -> DateTime - tm universal_tm = {0}; - gmtime_r(&universal_t, &universal_tm); - DateTime result = os_lnx_date_time_from_tm(universal_tm, 0); - return result; -} - -internal DateTime -os_local_time_from_universal(DateTime *date_time) -{ - // rjf: universal DateTime -> local time_t - tm universal_tm = os_lnx_tm_from_date_time(*date_time); - universal_tm.tm_isdst = -1; - time_t universal_t = timegm(&universal_tm); - tm local_tm = {0}; - localtime_r(&universal_t, &local_tm); - - // rjf: local tm -> DateTime - DateTime result = os_lnx_date_time_from_tm(local_tm, 0); - return result; -} - -internal void -os_sleep_milliseconds(U32 msec) -{ - usleep(msec*Thousand(1)); -} - -//////////////////////////////// -//~ rjf: @os_hooks Child Processes (Implemented Per-OS) - -internal OS_Handle -os_process_launch(OS_ProcessLaunchParams *params) -{ - NotImplemented; -} - -internal B32 -os_process_join(OS_Handle handle, U64 endt_us) -{ - NotImplemented; -} - -internal void -os_process_detach(OS_Handle handle) -{ - NotImplemented; -} - -//////////////////////////////// -//~ rjf: @os_hooks Threads (Implemented Per-OS) - -internal OS_Handle -os_thread_launch(OS_ThreadFunctionType *func, void *ptr, void *params) -{ - OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Thread); - entity->thread.func = func; - entity->thread.ptr = ptr; - { - int pthread_result = pthread_create(&entity->thread.handle, 0, os_lnx_thread_entry_point, entity); - if(pthread_result == -1) - { - os_lnx_entity_release(entity); - entity = 0; - } - } - OS_Handle handle = {(U64)entity}; - return handle; -} - -internal B32 -os_thread_join(OS_Handle handle, U64 endt_us) -{ - if(os_handle_match(handle, os_handle_zero())) { return 0; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)handle.u64[0]; - int join_result = pthread_join(entity->thread.handle, 0); - B32 result = (join_result == 0); - os_lnx_entity_release(entity); - return result; -} - -internal void -os_thread_detach(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)handle.u64[0]; - os_lnx_entity_release(entity); -} - -//////////////////////////////// -//~ rjf: @os_hooks Synchronization Primitives (Implemented Per-OS) - -//- rjf: mutexes - -internal OS_Handle -os_mutex_alloc(void) -{ - OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Mutex); - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); - int init_result = pthread_mutex_init(&entity->mutex_handle, &attr); - pthread_mutexattr_destroy(&attr); - if(init_result == -1) - { - os_lnx_entity_release(entity); - entity = 0; - } - OS_Handle handle = {(U64)entity}; - return handle; -} - -internal void -os_mutex_release(OS_Handle mutex) -{ - if(os_handle_match(mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)mutex.u64[0]; - pthread_mutex_destroy(&entity->mutex_handle); - os_lnx_entity_release(entity); -} - -internal void -os_mutex_take(OS_Handle mutex) -{ - if(os_handle_match(mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)mutex.u64[0]; - pthread_mutex_lock(&entity->mutex_handle); -} - -internal void -os_mutex_drop(OS_Handle mutex) -{ - if(os_handle_match(mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)mutex.u64[0]; - pthread_mutex_unlock(&entity->mutex_handle); -} - -//- rjf: reader/writer mutexes - -internal OS_Handle -os_rw_mutex_alloc(void) -{ - OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_RWMutex); - int init_result = pthread_rwlock_init(&entity->rwmutex_handle, 0); - if(init_result == -1) - { - os_lnx_entity_release(entity); - entity = 0; - } - OS_Handle handle = {(U64)entity}; - return handle; -} - -internal void -os_rw_mutex_release(OS_Handle rw_mutex) -{ - if(os_handle_match(rw_mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; - pthread_rwlock_destroy(&entity->rwmutex_handle); - os_lnx_entity_release(entity); -} - -internal void -os_rw_mutex_take_r(OS_Handle rw_mutex) -{ - if(os_handle_match(rw_mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; - pthread_rwlock_rdlock(&entity->rwmutex_handle); -} - -internal void -os_rw_mutex_drop_r(OS_Handle rw_mutex) -{ - if(os_handle_match(rw_mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; - pthread_rwlock_unlock(&entity->rwmutex_handle); -} - -internal void -os_rw_mutex_take_w(OS_Handle rw_mutex) -{ - if(os_handle_match(rw_mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; - pthread_rwlock_wrlock(&entity->rwmutex_handle); -} - -internal void -os_rw_mutex_drop_w(OS_Handle rw_mutex) -{ - if(os_handle_match(rw_mutex, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; - pthread_rwlock_unlock(&entity->rwmutex_handle); -} - -//- rjf: condition variables - -internal OS_Handle -os_condition_variable_alloc(void) -{ - OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_ConditionVariable); - int init_result = pthread_cond_init(&entity->cv.cond_handle, 0); - if(init_result == -1) - { - os_lnx_entity_release(entity); - entity = 0; - } - int init2_result = 0; - if(entity) - { - init2_result = pthread_mutex_init(&entity->cv.rwlock_mutex_handle, 0); - } - if(init2_result == -1) - { - pthread_cond_destroy(&entity->cv.cond_handle); - os_lnx_entity_release(entity); - entity = 0; - } - OS_Handle handle = {(U64)entity}; - return handle; -} - -internal void -os_condition_variable_release(OS_Handle cv) -{ - if(os_handle_match(cv, os_handle_zero())) { return; } - OS_LNX_Entity *entity = (OS_LNX_Entity *)cv.u64[0]; - pthread_cond_destroy(&entity->cv.cond_handle); - pthread_mutex_destroy(&entity->cv.rwlock_mutex_handle); - os_lnx_entity_release(entity); -} - -internal B32 -os_condition_variable_wait(OS_Handle cv, OS_Handle mutex, U64 endt_us) -{ - if(os_handle_match(cv, os_handle_zero())) { return 0; } - if(os_handle_match(mutex, os_handle_zero())) { return 0; } - OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; - OS_LNX_Entity *mutex_entity = (OS_LNX_Entity *)mutex.u64[0]; - struct timespec endt_timespec; - endt_timespec.tv_sec = endt_us/Million(1); - endt_timespec.tv_nsec = Thousand(1) * (endt_us - (endt_us/Million(1))*Million(1)); - int wait_result = pthread_cond_timedwait(&cv_entity->cv.cond_handle, &mutex_entity->mutex_handle, &endt_timespec); - B32 result = (wait_result != ETIMEDOUT); - return result; -} - -internal B32 -os_condition_variable_wait_rw_r(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) -{ - // TODO(rjf): because pthread does not supply cv/rw natively, I had to hack - // this together, but this would probably just be a lot better if we just - // implemented the primitives ourselves with e.g. futexes - // - if(os_handle_match(cv, os_handle_zero())) { return 0; } - if(os_handle_match(mutex_rw, os_handle_zero())) { return 0; } - OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; - OS_LNX_Entity *rw_mutex_entity = (OS_LNX_Entity *)mutex_rw.u64[0]; - struct timespec endt_timespec; - endt_timespec.tv_sec = endt_us/Million(1); - endt_timespec.tv_nsec = Thousand(1) * (endt_us - (endt_us/Million(1))*Million(1)); - B32 result = 0; - for(;;) - { - pthread_mutex_lock(&cv_entity->cv.rwlock_mutex_handle); - int wait_result = pthread_cond_timedwait(&cv_entity->cv.cond_handle, &cv_entity->cv.rwlock_mutex_handle, &endt_timespec); - if(wait_result != ETIMEDOUT) - { - pthread_rwlock_rdlock(&rw_mutex_entity->rwmutex_handle); - pthread_mutex_unlock(&cv_entity->cv.rwlock_mutex_handle); - result = 1; - break; - } - pthread_mutex_unlock(&cv_entity->cv.rwlock_mutex_handle); - if(wait_result == ETIMEDOUT) - { - break; - } - } - return result; -} - -internal B32 -os_condition_variable_wait_rw_w(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) -{ - // TODO(rjf): because pthread does not supply cv/rw natively, I had to hack - // this together, but this would probably just be a lot better if we just - // implemented the primitives ourselves with e.g. futexes - // - if(os_handle_match(cv, os_handle_zero())) { return 0; } - if(os_handle_match(mutex_rw, os_handle_zero())) { return 0; } - OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; - OS_LNX_Entity *rw_mutex_entity = (OS_LNX_Entity *)mutex_rw.u64[0]; - struct timespec endt_timespec; - endt_timespec.tv_sec = endt_us/Million(1); - endt_timespec.tv_nsec = Thousand(1) * (endt_us - (endt_us/Million(1))*Million(1)); - B32 result = 0; - for(;;) - { - pthread_mutex_lock(&cv_entity->cv.rwlock_mutex_handle); - int wait_result = pthread_cond_timedwait(&cv_entity->cv.cond_handle, &cv_entity->cv.rwlock_mutex_handle, &endt_timespec); - if(wait_result != ETIMEDOUT) - { - pthread_rwlock_wrlock(&rw_mutex_entity->rwmutex_handle); - pthread_mutex_unlock(&cv_entity->cv.rwlock_mutex_handle); - result = 1; - break; - } - pthread_mutex_unlock(&cv_entity->cv.rwlock_mutex_handle); - if(wait_result == ETIMEDOUT) - { - break; - } - } - return result; -} - -internal void -os_condition_variable_signal(OS_Handle cv) -{ - if(os_handle_match(cv, os_handle_zero())) { return; } - OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; - pthread_cond_signal(&cv_entity->cv.cond_handle); -} - -internal void -os_condition_variable_broadcast(OS_Handle cv) -{ - if(os_handle_match(cv, os_handle_zero())) { return; } - OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; - pthread_cond_broadcast(&cv_entity->cv.cond_handle); -} - -//- rjf: cross-process semaphores - -internal OS_Handle -os_semaphore_alloc(U32 initial_count, U32 max_count, String8 name) -{ - OS_Handle result = {0}; - if (name.size > 0) { - // TODO: we need to allocate shared memory to store sem_t - NotImplemented; - } else { - sem_t *s = mmap(0, sizeof(*s), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); - AssertAlways(s != MAP_FAILED); - int err = sem_init(s, 0, initial_count); - if (err == 0) { - result.u64[0] = (U64)s; - } - } - return result; -} - -internal void -os_semaphore_release(OS_Handle semaphore) -{ - int err = munmap((void*)semaphore.u64[0], sizeof(sem_t)); - AssertAlways(err == 0); -} - -internal OS_Handle -os_semaphore_open(String8 name) -{ - NotImplemented; -} - -internal void -os_semaphore_close(OS_Handle semaphore) -{ - NotImplemented; -} - -internal B32 -os_semaphore_take(OS_Handle semaphore, U64 endt_us) -{ - AssertAlways(endt_us == max_U64); - for (;;) { - int err = sem_wait((sem_t*)semaphore.u64[0]); - if (err == 0) { - break; - } else { - if (errno == EAGAIN) { - continue; - } - } - InvalidPath; - break; - } - return 1; -} - -internal void -os_semaphore_drop(OS_Handle semaphore) -{ - for (;;) { - int err = sem_post((sem_t*)semaphore.u64[0]); - if (err == 0) { - break; - } else { - if (errno == EAGAIN) { - continue; - } - } - InvalidPath; - break; - } -} - -//////////////////////////////// -//~ rjf: @os_hooks Dynamically-Loaded Libraries (Implemented Per-OS) - -internal OS_Handle -os_library_open(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - char *path_cstr = (char *)push_str8_copy(scratch.arena, path).str; - void *so = dlopen(path_cstr, RTLD_LAZY|RTLD_LOCAL); - OS_Handle lib = { (U64)so }; - scratch_end(scratch); - return lib; -} - -internal VoidProc* -os_library_load_proc(OS_Handle lib, String8 name) -{ - Temp scratch = scratch_begin(0, 0); - void *so = (void *)lib.u64; - char *name_cstr = (char *)push_str8_copy(scratch.arena, name).str; - VoidProc *proc = (VoidProc *)dlsym(so, name_cstr); - scratch_end(scratch); - return proc; -} - -internal void -os_library_close(OS_Handle lib) -{ - void *so = (void *)lib.u64; - dlclose(so); -} - -//////////////////////////////// -//~ rjf: @os_hooks Safe Calls (Implemented Per-OS) - -internal void -os_safe_call(OS_ThreadFunctionType *func, OS_ThreadFunctionType *fail_handler, void *ptr) -{ - // rjf: push handler to chain - OS_LNX_SafeCallChain chain = {0}; - SLLStackPush(os_lnx_safe_call_chain, &chain); - chain.fail_handler = fail_handler; - chain.ptr = ptr; - - // rjf: set up sig handler info - struct sigaction new_act = {0}; - new_act.sa_handler = os_lnx_safe_call_sig_handler; - int signals_to_handle[] = - { - SIGILL, SIGFPE, SIGSEGV, SIGBUS, SIGTRAP, - }; - struct sigaction og_act[ArrayCount(signals_to_handle)] = {0}; - - // rjf: attach handler info for all signals - for(U32 i = 0; i < ArrayCount(signals_to_handle); i += 1) - { - sigaction(signals_to_handle[i], &new_act, &og_act[i]); - } - - // rjf: call function - func(ptr); - - // rjf: reset handler info for all signals - for(U32 i = 0; i < ArrayCount(signals_to_handle); i += 1) - { - sigaction(signals_to_handle[i], &og_act[i], 0); - } -} - -//////////////////////////////// -//~ rjf: @os_hooks GUIDs (Implemented Per-OS) - -internal Guid -os_make_guid(void) -{ - Guid guid = {0}; - getrandom(guid.v, sizeof(guid.v), 0); - guid.data3 &= 0x0fff; - guid.data3 |= (4 << 12); - guid.data4[0] &= 0x3f; - guid.data4[0] |= 0x80; - return guid; -} - -//////////////////////////////// -//~ rjf: @os_hooks Entry Points (Implemented Per-OS) - -int -main(int argc, char **argv) -{ - //- rjf: set up OS layer - { - //- rjf: get statically-allocated system/process info - { - OS_SystemInfo *info = &os_lnx_state.system_info; - info->logical_processor_count = (U32)get_nprocs(); - info->page_size = (U64)getpagesize(); - info->large_page_size = MB(2); - info->allocation_granularity = info->page_size; - } - { - OS_ProcessInfo *info = &os_lnx_state.process_info; - info->pid = (U32)getpid(); - } - - //- rjf: set up thread context - local_persist TCTX tctx; - tctx_init_and_equip(&tctx); - - //- rjf: set up dynamically allocated state - os_lnx_state.arena = arena_alloc(); - os_lnx_state.entity_arena = arena_alloc(); - pthread_mutex_init(&os_lnx_state.entity_mutex, 0); - - //- rjf: grab dynamically allocated system info - { - Temp scratch = scratch_begin(0, 0); - OS_SystemInfo *info = &os_lnx_state.system_info; - - // rjf: get machine name - B32 got_final_result = 0; - U8 *buffer = 0; - int size = 0; - for(S64 cap = 4096, r = 0; r < 4; cap *= 2, r += 1) - { - scratch_end(scratch); - buffer = push_array_no_zero(scratch.arena, U8, cap); - size = gethostname((char*)buffer, cap); - if(size < cap) - { - got_final_result = 1; - break; - } - } - - // rjf: save name to info - if(got_final_result && size > 0) - { - info->machine_name.size = size; - info->machine_name.str = push_array_no_zero(os_lnx_state.arena, U8, info->machine_name.size + 1); - MemoryCopy(info->machine_name.str, buffer, info->machine_name.size); - info->machine_name.str[info->machine_name.size] = 0; - } - - scratch_end(scratch); - } - - //- rjf: grab dynamically allocated process info - { - Temp scratch = scratch_begin(0, 0); - OS_ProcessInfo *info = &os_lnx_state.process_info; - - // rjf: grab binary path - { - // rjf: get self string - B32 got_final_result = 0; - U8 *buffer = 0; - int size = 0; - for(S64 cap = PATH_MAX, r = 0; r < 4; cap *= 2, r += 1) - { - scratch_end(scratch); - buffer = push_array_no_zero(scratch.arena, U8, cap); - size = readlink("/proc/self/exe", (char*)buffer, cap); - if(size < cap) - { - got_final_result = 1; - break; - } - } - - // rjf: save - if(got_final_result && size > 0) - { - String8 full_name = str8(buffer, size); - String8 name_chopped = str8_chop_last_slash(full_name); - info->binary_path = push_str8_copy(os_lnx_state.arena, name_chopped); - } - } - - // rjf: grab initial directory - { - info->initial_path = os_get_current_path(os_lnx_state.arena); - } - - // rjf: grab home directory - { - char *home = getenv("HOME"); - info->user_program_data_path = str8_cstring(home); - } - - scratch_end(scratch); - } - } - - //- rjf: call into "real" entry point - main_thread_base_entry_point(argc, argv); -} +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//////////////////////////////// +//~ dan: Helpers + +internal DateTime +os_lnx_date_time_from_tm(tm in, U32 msec) +{ + DateTime dt = {0}; + dt.sec = in.tm_sec; + dt.min = in.tm_min; + dt.hour = in.tm_hour; + dt.day = in.tm_mday-1; + dt.mon = in.tm_mon; + dt.year = in.tm_year+1900; + dt.msec = msec; + return dt; +} + +internal tm +os_lnx_tm_from_date_time(DateTime dt) +{ + tm result = {0}; + result.tm_sec = dt.sec; + result.tm_min = dt.min; + result.tm_hour= dt.hour; + result.tm_mday= dt.day+1; + result.tm_mon = dt.mon; + result.tm_year= dt.year-1900; + return result; +} + +internal timespec +os_lnx_timespec_from_date_time(DateTime dt) +{ + tm tm_val = os_lnx_tm_from_date_time(dt); + time_t seconds = timegm(&tm_val); + timespec result = {0}; + result.tv_sec = seconds; + return result; +} + +internal DenseTime +os_lnx_dense_time_from_timespec(timespec in) +{ + DenseTime result = 0; + // Check for zero timespec before converting + if (in.tv_sec != 0 || in.tv_nsec != 0) + { + struct tm tm_time = {0}; + // Use gmtime_r for thread safety + time_t seconds = in.tv_sec; // Copy to time_t + if (gmtime_r(&seconds, &tm_time) != NULL) // Pass pointer to time_t + { + // Note: st_mtime/st_ctime etc. are usually UTC + DateTime date_time = os_lnx_date_time_from_tm(tm_time, in.tv_nsec / Million(1)); + result = dense_time_from_date_time(date_time); + } + } + return result; +} + +internal DenseTime +os_lnx_dense_time_from_statx_timestamp(struct statx_timestamp in) +{ + DenseTime result = 0; + // Check for zero timestamp before converting + if (in.tv_sec != 0 || in.tv_nsec != 0) + { + struct tm tm_time = {0}; + // Use gmtime_r for thread safety + time_t seconds = in.tv_sec; // Copy to time_t + if (gmtime_r(&seconds, &tm_time) != NULL) // Pass pointer to time_t + { + // Convert nanoseconds to milliseconds for DateTime + DateTime date_time = os_lnx_date_time_from_tm(tm_time, in.tv_nsec / Million(1)); + result = dense_time_from_date_time(date_time); + } + } + return result; +} + +internal FileProperties +os_lnx_file_properties_from_stat(struct stat *s) +{ + FileProperties props = {0}; + props.size = s->st_size; + // NOTE(rjf): Fallback path for creation time. `os_properties_from_file*` functions + // will attempt `statx` first. This function handles `stat` results. +#if defined(st_birthtim) // Check if st_birthtim is available (more specific than st_ctim) + props.created = os_lnx_dense_time_from_timespec(s->st_birthtim); +#else // Fallback to st_ctim if st_birthtim is not available + props.created = os_lnx_dense_time_from_timespec(s->st_ctim); +#endif + props.modified = os_lnx_dense_time_from_timespec(s->st_mtim); + if(S_ISDIR(s->st_mode)) // Use S_ISDIR macro for clarity + { + props.flags |= FilePropertyFlag_IsFolder; + } + return props; +} + +internal int +os_lnx_futex(atomic_uint *uaddr, int futex_op, unsigned int val, const struct timespec *timeout, unsigned int val3) +{ + // NOTE(rjf): `syscall` needs non-const pointer for timeout for some reason. + struct timespec *timeout_nonconst = (struct timespec*)timeout; + return syscall(SYS_futex, uaddr, futex_op, val, timeout_nonconst, NULL, val3); +} + +internal int +os_lnx_futex_wait(atomic_uint *uaddr, unsigned int expected_val, const struct timespec *timeout) +{ + // NOTE(rjf): timeout can be NULL for infinite wait. FUTEX_WAIT_PRIVATE is used for intra-process sync. + return os_lnx_futex(uaddr, FUTEX_WAIT_PRIVATE, expected_val, timeout, 0); +} + +internal int +os_lnx_futex_wake(atomic_uint *uaddr, int num_to_wake) +{ + // NOTE(rjf): FUTEX_WAKE_PRIVATE is used for intra-process sync. + return os_lnx_futex(uaddr, FUTEX_WAKE_PRIVATE, num_to_wake, NULL, 0); +} + +//////////////////////////////// +//~ dan: Entities + +internal OS_LNX_Entity * +os_lnx_entity_alloc(OS_LNX_EntityKind kind) +{ + OS_LNX_Entity *entity = 0; + DeferLoop(pthread_mutex_lock(&os_lnx_state.entity_mutex), + pthread_mutex_unlock(&os_lnx_state.entity_mutex)) + { + entity = os_lnx_state.entity_free; + if(entity) + { + SLLStackPop(os_lnx_state.entity_free); + } + else + { + entity = push_array_no_zero(os_lnx_state.entity_arena, OS_LNX_Entity, 1); + } + } + MemoryZeroStruct(entity); + entity->kind = kind; + + // Initialize based on kind + switch(kind) + { + case OS_LNX_EntityKind_Mutex: + atomic_init(&entity->mutex.futex, 0); // 0: unlocked + atomic_init(&entity->mutex.owner_tid, 0); + atomic_init(&entity->mutex.recursion_depth, 0); + break; + case OS_LNX_EntityKind_RWMutex: + atomic_init(&entity->rw_mutex.futex, 0); + break; + case OS_LNX_EntityKind_ConditionVariable: + atomic_init(&entity->cv.futex, 0); // Initialize sequence number/generation count + break; + case OS_LNX_EntityKind_SafeCallChain: + // Initialization happens in os_safe_call + break; + // Other kinds (Thread, Process, Semaphore) are initialized elsewhere or don't need specific init here + default: break; + } + + return entity; +} + +internal void +os_lnx_entity_release(OS_LNX_Entity *entity) +{ + // NOTE(rjf): Additional cleanup per-kind can go here if needed, e.g., free name strings for semaphores. + // For SafeCallChain, cleanup (handler restoration) happens within os_safe_call itself. + DeferLoop(pthread_mutex_lock(&os_lnx_state.entity_mutex), + pthread_mutex_unlock(&os_lnx_state.entity_mutex)) + { + SLLStackPush(os_lnx_state.entity_free, entity); + } +} + +//////////////////////////////// +//~ dan: Thread Entry Point + +internal void * +os_lnx_thread_entry_point(void *ptr) +{ + OS_LNX_Entity *entity = (OS_LNX_Entity *)ptr; + OS_ThreadFunctionType *func = entity->thread.func; + void *thread_ptr = entity->thread.ptr; + TCTX tctx_; + tctx_init_and_equip(&tctx_); + func(thread_ptr); + tctx_release(); + return 0; +} + +//////////////////////////////// +//~ dan: @os_hooks System/Process Info (Implemented Per-OS) + +internal OS_SystemInfo * +os_get_system_info(void) +{ + return &os_lnx_state.system_info; +} + +internal OS_ProcessInfo * +os_get_process_info(void) +{ + return &os_lnx_state.process_info; +} + +internal String8 +os_get_current_path(Arena *arena) +{ + char *cwdir = getcwd(0, 0); + String8 string = push_str8_copy(arena, str8_cstring(cwdir)); + free(cwdir); + return string; +} + +internal U32 +os_get_process_start_time_unix(void) +{ + Temp scratch = scratch_begin(0,0); + U64 start_time = 0; + pid_t pid = getpid(); + String8 path = push_str8f(scratch.arena, "/proc/%u", pid); + struct stat st; + int err = stat((char*)path.str, &st); + if(err == 0) + { + start_time = st.st_mtime; + } + scratch_end(scratch); + return (U32)start_time; +} + +//////////////////////////////// +//~ dan: @os_hooks Memory Allocation (Implemented Per-OS) + +//- rjf: basic + +internal void * +os_reserve(U64 size) +{ + void *result = mmap(0, size, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); + if(result == MAP_FAILED) + { + result = 0; + } + return result; +} + +internal B32 +os_commit(void *ptr, U64 size) +{ + int result = mprotect(ptr, size, PROT_READ|PROT_WRITE); + if (result == -1) { + perror("mprotect commit"); + // Consider returning 0 on failure, matching Win32 VirtualAlloc failure. + // For now, maintain original behavior but log error. + // return 0; + } + return 1; // Original behavior: always return success +} + +internal void +os_decommit(void *ptr, U64 size) +{ + int madvise_result = madvise(ptr, size, MADV_DONTNEED); + if (madvise_result == -1) { + perror("madvise decommit"); + } + int mprotect_result = mprotect(ptr, size, PROT_NONE); + if (mprotect_result == -1) { + perror("mprotect decommit"); + } +} + +internal void +os_release(void *ptr, U64 size) +{ + int result = munmap(ptr, size); + if (result == -1) { + perror("munmap release"); + } +} + +//- rjf: large pages + +internal void * +os_reserve_large(U64 size) +{ + void *result = mmap(0, size, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0); + if(result == MAP_FAILED) + { + result = 0; + } + return result; +} + +internal B32 +os_commit_large(void *ptr, U64 size) +{ + int result = mprotect(ptr, size, PROT_READ|PROT_WRITE); + if (result == -1) { + perror("mprotect commit_large"); + return 0; + } + return 1; +} + +//////////////////////////////// +//~ dan: @os_hooks Thread Info (Implemented Per-OS) + +internal U32 +os_tid(void) +{ + U32 result = gettid(); + return result; +} + +internal void +os_set_thread_name(String8 name) +{ + Temp scratch = scratch_begin(0, 0); + String8 name_copy = push_str8_copy(scratch.arena, name); + pthread_t current_thread = pthread_self(); + int err = pthread_setname_np(current_thread, (char *)name_copy.str); + if (err != 0) { + // Optional: Log error, but it's often non-critical + // fprintf(stderr, "Warning: Failed to set thread name: %s\n", strerror(err)); + } + scratch_end(scratch); +} + +//////////////////////////////// +//~ dan: @os_hooks Aborting (Implemented Per-OS) + +internal void +os_abort(S32 exit_code) +{ + exit(exit_code); +} + +//////////////////////////////// +//~ dan: @os_hooks File System (Implemented Per-OS) + +//- rjf: files + +internal OS_Handle +os_file_open(OS_AccessFlags flags, String8 path) +{ + Temp scratch = scratch_begin(0, 0); + String8 path_copy = push_str8_copy(scratch.arena, path); + int lnx_flags = 0; + if(flags & OS_AccessFlag_Read && flags & OS_AccessFlag_Write) + { + lnx_flags = O_RDWR; + } + else if(flags & OS_AccessFlag_Write) + { + lnx_flags = O_WRONLY; + } + else if(flags & OS_AccessFlag_Read) + { + lnx_flags = O_RDONLY; + } + if(flags & OS_AccessFlag_Append) + { + lnx_flags |= O_APPEND; + } + if(flags & (OS_AccessFlag_Write|OS_AccessFlag_Append)) + { + lnx_flags |= O_CREAT; + } + int fd = open((char *)path_copy.str, lnx_flags, 0755); + OS_Handle handle = {0}; + if(fd != -1) + { + handle.u64[0] = fd; + // Set FD_CLOEXEC based on inheritance flag + if (!(flags & OS_AccessFlag_Inherited)) + { + int fd_flags = fcntl(fd, F_GETFD); + if (fd_flags != -1) + { + fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC); + } + else + { + // Handle fcntl error (optional: log? close fd?) + close(fd); + fd = -1; + handle = os_handle_zero(); + } + } + } + scratch_end(scratch); + return handle; +} + +internal void +os_file_close(OS_Handle file) +{ + if(os_handle_match(file, os_handle_zero())) { return; } + int fd = (int)file.u64[0]; + close(fd); +} + +internal U64 +os_file_read(OS_Handle file, Rng1U64 rng, void *out_data) +{ + if(os_handle_match(file, os_handle_zero())) { return 0; } + int fd = (int)file.u64[0]; + U64 total_num_bytes_to_read = dim_1u64(rng); + U64 total_num_bytes_read = 0; + U64 total_num_bytes_left_to_read = total_num_bytes_to_read; + for(;total_num_bytes_left_to_read > 0;) + { + int read_result = pread(fd, (U8 *)out_data + total_num_bytes_read, total_num_bytes_left_to_read, rng.min + total_num_bytes_read); + if(read_result >= 0) + { + total_num_bytes_read += read_result; + total_num_bytes_left_to_read -= read_result; + } + else if(errno != EINTR) + { + break; + } + } + return total_num_bytes_read; +} + +internal U64 +os_file_write(OS_Handle file, Rng1U64 rng, void *data) +{ + if(os_handle_match(file, os_handle_zero())) { return 0; } + int fd = (int)file.u64[0]; + U64 total_num_bytes_to_write = dim_1u64(rng); + U64 total_num_bytes_written = 0; + U64 total_num_bytes_left_to_write = total_num_bytes_to_write; + for(;total_num_bytes_left_to_write > 0;) + { + int write_result = pwrite(fd, (U8 *)data + total_num_bytes_written, total_num_bytes_left_to_write, rng.min + total_num_bytes_written); + if(write_result >= 0) + { + total_num_bytes_written += write_result; + total_num_bytes_left_to_write -= write_result; + } + else if(errno != EINTR) + { + break; + } + } + return total_num_bytes_written; +} + +internal B32 +os_file_set_times(OS_Handle file, DateTime date_time) +{ + if(os_handle_match(file, os_handle_zero())) { return 0; } + int fd = (int)file.u64[0]; + timespec time = os_lnx_timespec_from_date_time(date_time); + timespec times[2] = {time, time}; + int futimens_result = futimens(fd, times); + B32 good = (futimens_result != -1); + return good; +} + +internal FileProperties +os_properties_from_file(OS_Handle file) +{ + if(os_handle_match(file, os_handle_zero())) { return (FileProperties){0}; } + int fd = (int)file.u64[0]; + FileProperties props = {0}; + +#if defined(__NR_statx) // Check if statx syscall number is defined + struct statx stx = {0}; + // Try statx first to get birth time if available + if (syscall(__NR_statx, fd, "", AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW, STATX_BTIME | STATX_MTIME | STATX_SIZE | STATX_TYPE, &stx) == 0) + { + // Populate common properties + props.size = stx.stx_size; + props.modified = os_lnx_dense_time_from_statx_timestamp(stx.stx_mtime); + if(S_ISDIR(stx.stx_mode)) { props.flags |= FilePropertyFlag_IsFolder; } + + // Use birth time if available + if (stx.stx_mask & STATX_BTIME) { + props.created = os_lnx_dense_time_from_statx_timestamp(stx.stx_btime); + } else { + // statx succeeded but didn't return birth time, fall back to fstat/st_ctim/st_birthtim logic + struct stat fd_stat = {0}; + if (fstat(fd, &fd_stat) == 0) { + props = os_lnx_file_properties_from_stat(&fd_stat); // Re-use stat logic for fallback create time + // Keep size, modified, flags from statx as they are likely more accurate or available + props.size = stx.stx_size; + props.modified = os_lnx_dense_time_from_statx_timestamp(stx.stx_mtime); + props.flags = 0; // Reset flags and re-set based on statx mode + if(S_ISDIR(stx.stx_mode)) { props.flags |= FilePropertyFlag_IsFolder; } + } + // If fstat fails here, props remain partially filled from statx, created time is zero. + } + return props; // Return props obtained from statx (potentially with fallback create time) + } + // If statx failed, fall through to fstat below +#endif // __NR_statx + + // Fallback to fstat if statx is not available or failed + struct stat fd_stat = {0}; + int fstat_result = fstat(fd, &fd_stat); + if(fstat_result != -1) + { + props = os_lnx_file_properties_from_stat(&fd_stat); + } + return props; +} + +internal OS_FileID +os_id_from_file(OS_Handle file) +{ + if(os_handle_match(file, os_handle_zero())) { return (OS_FileID){0}; } + int fd = (int)file.u64[0]; + struct stat fd_stat = {0}; + int fstat_result = fstat(fd, &fd_stat); + OS_FileID id = {0}; + if(fstat_result != -1) + { + id.v[0] = fd_stat.st_dev; + id.v[1] = fd_stat.st_ino; + } + return id; +} + +internal B32 +os_delete_file_at_path(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + B32 result = 0; + String8 path_copy = push_str8_copy(scratch.arena, path); + if(remove((char*)path_copy.str) != -1) + { + result = 1; + } + scratch_end(scratch); + return result; +} + +internal B32 +os_copy_file_path(String8 dst, String8 src) +{ + B32 result = 0; + OS_Handle src_h = os_file_open(OS_AccessFlag_Read, src); + OS_Handle dst_h = os_file_open(OS_AccessFlag_Write, dst); + if(!os_handle_match(src_h, os_handle_zero()) && + !os_handle_match(dst_h, os_handle_zero())) + { + int src_fd = (int)src_h.u64[0]; + int dst_fd = (int)dst_h.u64[0]; + FileProperties src_props = os_properties_from_file(src_h); + U64 size = src_props.size; + U64 total_bytes_copied = 0; + U64 bytes_left_to_copy = size; + B32 copy_error = 0; + for(;bytes_left_to_copy > 0;) + { + off_t sendfile_off = total_bytes_copied; + // Use ssize_t for sendfile result as it can return -1 on error + ssize_t send_result = sendfile(dst_fd, src_fd, &sendfile_off, bytes_left_to_copy); + if(send_result <= 0) + { + // Error or end of file reached prematurely + copy_error = 1; + break; + } + U64 bytes_copied = (U64)send_result; + bytes_left_to_copy -= bytes_copied; + total_bytes_copied += bytes_copied; + } + + // Check if the entire file was copied without errors + if (!copy_error && total_bytes_copied == size) + { + result = 1; + // Set the destination file times to match the source modification time + DateTime modified_dt = date_time_from_dense_time(src_props.modified); + os_file_set_times(dst_h, modified_dt); + } + } + os_file_close(src_h); + os_file_close(dst_h); + return result; +} + +internal String8 +os_full_path_from_path(Arena *arena, String8 path) +{ + Temp scratch = scratch_begin(&arena, 1); + String8 path_copy = push_str8_copy(scratch.arena, path); + char buffer[PATH_MAX] = {0}; + realpath((char *)path_copy.str, buffer); + String8 result = push_str8_copy(arena, str8_cstring(buffer)); + scratch_end(scratch); + return result; +} + +internal B32 +os_file_path_exists(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + String8 path_copy = push_str8_copy(scratch.arena, path); + int access_result = access((char *)path_copy.str, F_OK); + B32 result = 0; + if(access_result == 0) + { + result = 1; + } + scratch_end(scratch); + return result; +} + +internal B32 +os_folder_path_exists(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + B32 exists = 0; + String8 path_copy = push_str8_copy(scratch.arena, path); + DIR *handle = opendir((char*)path_copy.str); + if(handle) + { + closedir(handle); + exists = 1; + } + scratch_end(scratch); + return exists; +} + +internal FileProperties +os_properties_from_file_path(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + String8 path_copy = push_str8_copy(scratch.arena, path); + FileProperties props = {0}; + +#if defined(__NR_statx) // Check if statx syscall number is defined + struct statx stx = {0}; + // Try statx first to get birth time if available + // Use AT_SYMLINK_NOFOLLOW to match stat() behavior unless symlinks should be followed + if (syscall(__NR_statx, AT_FDCWD, (char *)path_copy.str, AT_SYMLINK_NOFOLLOW, STATX_BTIME | STATX_MTIME | STATX_SIZE | STATX_TYPE, &stx) == 0) + { + // Populate common properties + props.size = stx.stx_size; + props.modified = os_lnx_dense_time_from_statx_timestamp(stx.stx_mtime); + if(S_ISDIR(stx.stx_mode)) { props.flags |= FilePropertyFlag_IsFolder; } + + // Use birth time if available + if (stx.stx_mask & STATX_BTIME) { + props.created = os_lnx_dense_time_from_statx_timestamp(stx.stx_btime); + } else { + // statx succeeded but didn't return birth time, fall back to stat/st_ctim/st_birthtim logic + struct stat f_stat = {0}; + if (stat((char *)path_copy.str, &f_stat) == 0) { + props = os_lnx_file_properties_from_stat(&f_stat); // Re-use stat logic for fallback create time + // Keep size, modified, flags from statx + props.size = stx.stx_size; + props.modified = os_lnx_dense_time_from_statx_timestamp(stx.stx_mtime); + props.flags = 0; // Reset flags and re-set based on statx mode + if(S_ISDIR(stx.stx_mode)) { props.flags |= FilePropertyFlag_IsFolder; } + } + // If stat fails here, props remain partially filled from statx, created time is zero. + } + scratch_end(scratch); + return props; // Return props obtained from statx (potentially with fallback create time) + } + // If statx failed, fall through to stat below +#endif // __NR_statx + + // Fallback to stat if statx is not available or failed + struct stat f_stat = {0}; + int stat_result = stat((char *)path_copy.str, &f_stat); + if(stat_result != -1) + { + props = os_lnx_file_properties_from_stat(&f_stat); + } + scratch_end(scratch); + return props; +} + +//- rjf: file maps + +internal OS_Handle +os_file_map_open(OS_AccessFlags flags, OS_Handle file) +{ + OS_Handle map = file; + return map; +} + +internal void +os_file_map_close(OS_Handle map) +{ + // NOTE(rjf): nothing to do; `map` handles are the same as `file` handles in + // the linux implementation (on Windows they require separate handles) +} + +internal void * +os_file_map_view_open(OS_Handle map, OS_AccessFlags flags, Rng1U64 range) +{ + if(os_handle_match(map, os_handle_zero())) { return 0; } + int fd = (int)map.u64[0]; + int prot_flags = 0; + if(flags & OS_AccessFlag_Write) { prot_flags |= PROT_WRITE; } + if(flags & OS_AccessFlag_Read) { prot_flags |= PROT_READ; } + int map_flags = MAP_PRIVATE; + void *base = mmap(0, dim_1u64(range), prot_flags, map_flags, fd, range.min); + if(base == MAP_FAILED) + { + base = 0; + } + return base; +} + +internal void +os_file_map_view_close(OS_Handle map, void *ptr, Rng1U64 range) +{ + munmap(ptr, dim_1u64(range)); + int result = munmap(ptr, dim_1u64(range)); + if (result == -1) { + perror("munmap file_map_view_close"); + } +} + +//- rjf: directory iteration + +internal OS_FileIter * +os_file_iter_begin(Arena *arena, String8 path, OS_FileIterFlags flags) +{ + OS_FileIter *base_iter = push_array(arena, OS_FileIter, 1); + base_iter->flags = flags; + OS_LNX_FileIter *iter = (OS_LNX_FileIter *)base_iter->memory; + MemoryZeroStruct(iter); // Initialize struct + + if(path.size == 0) + { + // Handle volume iteration (mount points on Linux) + iter->is_volume_iter = 1; + iter->mount_file_stream = setmntent(_PATH_MOUNTED, "r"); // Readonly access to mount table + if (iter->mount_file_stream != NULL) + { + String8List mount_points_list = {0}; + struct mntent *mount_entry; + String8 skip_fs_types[] = { + str8_lit("proc"), str8_lit("sysfs"), str8_lit("devtmpfs"), str8_lit("tmpfs"), + str8_lit("cgroup"), str8_lit("cgroup2"), str8_lit("debugfs"), str8_lit("devpts"), + str8_lit("securityfs"), str8_lit("pstore"), str8_lit("autofs"), str8_lit("mqueue"), + str8_lit("hugetlbfs"), str8_lit("binfmt_misc"), str8_lit("fusectl"), + str8_lit("tracefs"), str8_lit("configfs"), str8_lit("fuse.gvfsd-fuse"), + // Add other pseudo/virtual/network FS types to skip as needed + }; + + while ((mount_entry = getmntent(iter->mount_file_stream)) != NULL) + { + // Filter out pseudo-filesystems and unwanted types + B32 skip = 0; + String8 fs_type = str8_cstring(mount_entry->mnt_type); + for (U64 i = 0; i < ArrayCount(skip_fs_types); ++i) { + if (str8_match(fs_type, skip_fs_types[i], 0)) { + skip = 1; + break; + } + } + if (skip) { + continue; + } + + // TODO(rjf): Consider filtering based on mnt_opts (e.g., skip "noauto")? + + String8 mount_dir = str8_cstring(mount_entry->mnt_dir); + // TODO(rjf): Check for duplicates if the same device is mounted multiple times? + // For now, assume unique mount points reported by getmntent are sufficient. + str8_list_push(arena, &mount_points_list, mount_dir); + } + iter->volume_mount_points = str8_array_from_list(arena, &mount_points_list); + iter->volume_iter_idx = 0; + // We keep mount_file_stream open until os_file_iter_end + } + else + { + // Failed to open mount table, log error? Volume iteration won't work. + iter->volume_mount_points.count = 0; // Ensure iteration ends immediately + } + // If setmntent fails, volume_mount_points will be empty, and iteration will end immediately. + } + else + { + // Handle regular directory iteration + iter->is_volume_iter = 0; + String8 path_copy = push_str8_copy(arena, path); + iter->dir = opendir((char *)path_copy.str); + iter->path = path_copy; + // iter->mount_file_stream will be NULL + } + + return base_iter; +} + +internal B32 +os_file_iter_next(Arena *arena, OS_FileIter *iter, OS_FileInfo *info_out) +{ + B32 good = 0; + OS_LNX_FileIter *lnx_iter = (OS_LNX_FileIter *)iter->memory; + + if (lnx_iter->is_volume_iter) + { + // Volume (Mount Point) Iteration - Unchanged + if (lnx_iter->volume_iter_idx < lnx_iter->volume_mount_points.count) + { + MemoryZeroStruct(info_out); + info_out->name = lnx_iter->volume_mount_points.v[lnx_iter->volume_iter_idx]; + info_out->props.flags |= FilePropertyFlag_IsFolder; + // Props remain minimal for volume iteration + lnx_iter->volume_iter_idx++; + good = 1; + } + } + else + { + // Regular Directory Iteration + if (lnx_iter->dir == 0) // Check if opendir failed in begin + { + iter->flags |= OS_FileIterFlag_Done; + return 0; + } + + for (;;) // Loop until a valid, unfiltered entry is found or the directory ends + { + // 1. Read next directory entry + errno = 0; // Reset errno before readdir + lnx_iter->dp = readdir(lnx_iter->dir); + + if (lnx_iter->dp == 0) // End of directory or error + { + if (errno != 0) { + // An error occurred during readdir, treat as end of iteration for now. + // A more robust implementation might log the error here via errno. + // Optional: Log error? + } + good = 0; // Mark as not found this iteration + iter->flags |= OS_FileIterFlag_Done; // Mark iteration as finished + break; // Exit loop + } + + // 2. Filter "." and ".." + char *d_name = lnx_iter->dp->d_name; + if (d_name[0] == '.') { + if (d_name[1] == 0 || (d_name[1] == '.' && d_name[2] == 0)) { + continue; // Skip "." and ".." + } + // 3. Filter hidden files if requested + if (iter->flags & OS_FileIterFlag_SkipHiddenFiles) { + continue; // Skip hidden files (e.g., ".bashrc") + } + } + + // 4. Always use stat for definitive type and properties + Temp scratch = scratch_begin(&arena, 1); + String8 full_path = {0}; + if (lnx_iter->path.size == 1 && lnx_iter->path.str[0] == '/') { // Handle root case + full_path = push_str8f(scratch.arena, "/%s", d_name); + } else { + full_path = push_str8f(scratch.arena, "%S/%s", lnx_iter->path, d_name); + } + struct stat st = {0}; + // Use stat() to follow symlinks, matching FindFirstFile behavior. + int stat_result = stat((char *)full_path.str, &st); + scratch_end(scratch); + + // 5. Handle stat result and apply filters + if (stat_result != 0) { + // Stat failed (e.g., broken link, permissions). Skip this entry. + // Optional: Log errno here? For now, just skip. + continue; + } + + // Stat succeeded, check filters based on actual type + B32 is_dir = S_ISDIR(st.st_mode); + B32 filtered = 0; + if (is_dir && (iter->flags & OS_FileIterFlag_SkipFolders)) { + filtered = 1; + } else if (!is_dir && (iter->flags & OS_FileIterFlag_SkipFiles)) { + filtered = 1; + } + + if (!filtered) { + // 6. Populate output and break loop + info_out->name = push_str8_copy(arena, str8_cstring(d_name)); + info_out->props = os_lnx_file_properties_from_stat(&st); + good = 1; // Found a valid entry + break; // Exit loop + } + // If filtered, continue the loop to find the next entry + } + } + + // Return true only if a valid item was found *this call* + return good; +} + +internal void +os_file_iter_end(OS_FileIter *iter) +{ + OS_LNX_FileIter *lnx_iter = (OS_LNX_FileIter *)iter->memory; + if (lnx_iter->is_volume_iter) + { + if (lnx_iter->mount_file_stream != 0) + { + endmntent(lnx_iter->mount_file_stream); + lnx_iter->mount_file_stream = 0; + } + } + else + { + if (lnx_iter->dir != 0) + { + closedir(lnx_iter->dir); + lnx_iter->dir = 0; + } + } +} + +//- rjf: directory creation + +internal B32 +os_make_directory(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + B32 result = 0; + String8 path_copy = push_str8_copy(scratch.arena, path); + if(mkdir((char*)path_copy.str, 0755) != -1) + { + result = 1; + } + scratch_end(scratch); + return result; +} + +//////////////////////////////// +//~ dan: @os_hooks Shared Memory (Implemented Per-OS) + +internal OS_Handle +os_shared_memory_alloc(U64 size, String8 name) +{ + OS_Handle result = os_handle_zero(); // Initialize to zero handle + Temp scratch = scratch_begin(0, 0); + String8 name_copy = push_str8_copy(scratch.arena, name); + + // Use O_CREAT | O_RDWR to create if not exists, or open if it does. + // Use appropriate permissions (e.g., 0666). + int id = shm_open((char *)name_copy.str, O_CREAT | O_RDWR, 0666); + + if (id != -1) // Check if shm_open succeeded + { + // Truncate the file to the desired size. + if (ftruncate(id, size) == 0) // Check if ftruncate succeeded + { + result.u64[0] = (U64)id; // Success path + } + else + { + perror("ftruncate shared memory"); + close(id); // Close the descriptor on ftruncate failure + // Optional: shm_unlink the name if we created it and failed to size it? + // Might be too aggressive if another process already had it open. + // For now, just close and return zero handle. + } + } + else + { + perror("shm_open shared memory"); + // shm_open failed, result remains zero handle + } + + scratch_end(scratch); + return result; +} + +internal OS_Handle +os_shared_memory_open(String8 name) +{ + Temp scratch = scratch_begin(0, 0); + String8 name_copy = push_str8_copy(scratch.arena, name); + int id = shm_open((char *)name_copy.str, O_RDWR, 0); + OS_Handle result = {(U64)id}; + scratch_end(scratch); + return result; +} + +internal void +os_shared_memory_close(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())){return;} + int id = (int)handle.u64[0]; + close(id); +} + +internal void * +os_shared_memory_view_open(OS_Handle handle, Rng1U64 range) +{ + if(os_handle_match(handle, os_handle_zero())){return 0;} + int id = (int)handle.u64[0]; + void *base = mmap(0, dim_1u64(range), PROT_READ|PROT_WRITE, MAP_SHARED, id, range.min); + if(base == MAP_FAILED) + { + base = 0; + } + return base; +} + +internal void +os_shared_memory_view_close(OS_Handle handle, void *ptr, Rng1U64 range) +{ + if(os_handle_match(handle, os_handle_zero())){return;} + munmap(ptr, dim_1u64(range)); + int result = munmap(ptr, dim_1u64(range)); + if (result == -1) { + perror("munmap shared_memory_view_close"); + } +} + +//////////////////////////////// +//~ dan: @os_hooks Time (Implemented Per-OS) + +internal U64 +os_now_microseconds(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + U64 result = t.tv_sec*Million(1) + (t.tv_nsec/Thousand(1)); + return result; +} + +internal U32 +os_now_unix(void) +{ + time_t t = time(0); + return (U32)t; +} + +internal DateTime +os_now_universal_time(void) +{ + time_t t = 0; + time(&t); + struct tm universal_tm = {0}; + gmtime_r(&t, &universal_tm); + DateTime result = os_lnx_date_time_from_tm(universal_tm, 0); + return result; +} + +internal DateTime +os_universal_time_from_local(DateTime *date_time) +{ + // local DateTime -> universal time_t + tm local_tm = os_lnx_tm_from_date_time(*date_time); + local_tm.tm_isdst = -1; + time_t universal_t = mktime(&local_tm); + + // universal time_t -> DateTime + tm universal_tm = {0}; + gmtime_r(&universal_t, &universal_tm); + DateTime result = os_lnx_date_time_from_tm(universal_tm, 0); + return result; +} + +internal DateTime +os_local_time_from_universal(DateTime *date_time) +{ + // universal DateTime -> local time_t + tm universal_tm = os_lnx_tm_from_date_time(*date_time); + universal_tm.tm_isdst = -1; + time_t universal_t = timegm(&universal_tm); + tm local_tm = {0}; + localtime_r(&universal_t, &local_tm); + + // local tm -> DateTime + DateTime result = os_lnx_date_time_from_tm(local_tm, 0); + return result; +} + +internal void +os_sleep_milliseconds(U32 msec) +{ + usleep(msec*Thousand(1)); +} + +//////////////////////////////// +//~ dan: @os_hooks Child Processes (Implemented Per-OS) + +internal OS_Handle +os_process_launch(OS_ProcessLaunchParams *params) +{ + OS_Handle result = {0}; + Temp scratch = scratch_begin(0, 0); + + pid_t pid = fork(); + + if (pid == -1) + { + // Fork failed + perror("fork"); + } + else if (pid == 0) + { + // Child process + + // Redirect stdout if requested + if (!os_handle_match(params->stdout_file, os_handle_zero())) + { + int fd = (int)params->stdout_file.u64[0]; + if (dup2(fd, STDOUT_FILENO) == -1) { perror("dup2 stdout"); _exit(1); } + // Close the original fd in child, not strictly necessary as exec replaces fds, + // but good practice if exec fails. If Inherited flag was set, this might + // close the parent's handle too if not careful about handle inheritance flags + // during file open. Assuming non-inherited handles for simplicity here. + // close(fd); // Let's skip closing for now, relies on correct OS_AccessFlag_Inherited usage + } + + // Redirect stderr if requested + if (!os_handle_match(params->stderr_file, os_handle_zero())) + { + int fd = (int)params->stderr_file.u64[0]; + if (dup2(fd, STDERR_FILENO) == -1) { perror("dup2 stderr"); _exit(1); } + // close(fd); + } + + // Redirect stdin if requested + if (!os_handle_match(params->stdin_file, os_handle_zero())) + { + int fd = (int)params->stdin_file.u64[0]; + if (dup2(fd, STDIN_FILENO) == -1) { perror("dup2 stdin"); _exit(1); } + // close(fd); + } + + // Change directory if requested + if (params->path.size > 0) + { + String8 path_copy = push_str8_copy(scratch.arena, params->path); + if (chdir((char *)path_copy.str) == -1) + { + perror("chdir"); + _exit(1); + } + } + + // Prepare command line arguments + char **argv = push_array(scratch.arena, char *, params->cmd_line.node_count + 1); + U64 argv_idx = 0; + for(String8Node *n = params->cmd_line.first; n != 0; n = n->next) + { + String8 arg_copy = push_str8_copy(scratch.arena, n->string); + argv[argv_idx++] = (char *)arg_copy.str; + } + argv[argv_idx] = NULL; + + // Prepare environment + char **envp = NULL; + if (params->inherit_env == 0 && params->env.node_count > 0) + { + // Use only specified environment variables + envp = push_array(scratch.arena, char *, params->env.node_count + 1); + U64 envp_idx = 0; + for (String8Node *n = params->env.first; n != 0; n = n->next) + { + String8 env_copy = push_str8_copy(scratch.arena, n->string); + envp[envp_idx++] = (char *)env_copy.str; + } + envp[envp_idx] = NULL; + } + else if (params->inherit_env != 0 && params->env.node_count > 0) + { + // Inherit existing environment and append/overwrite specified ones + extern char **environ; + String8List merged_env_list = {0}; + + // Pass 1: Add inherited variables if they are not overridden by custom ones. + if (environ) + { + for (char **existing_env = environ; *existing_env; ++existing_env) + { + String8 existing_var = str8_cstring(*existing_env); + U64 eq_pos_existing = str8_find_needle(existing_var, 0, str8_lit("="), 0); + String8 existing_key = str8_prefix(existing_var, eq_pos_existing); + B32 overwritten = 0; + + // Check if this key exists in the custom environment variables + for (String8Node *n = params->env.first; n != 0; n = n->next) + { + String8 custom_var = n->string; + U64 eq_pos_custom = str8_find_needle(custom_var, 0, str8_lit("="), 0); + String8 custom_key = str8_prefix(custom_var, eq_pos_custom); + if (str8_match(existing_key, custom_key, 0)) { + overwritten = 1; + break; + } + } + + if (!overwritten) + { + // Add the inherited variable to the list + str8_list_push(scratch.arena, &merged_env_list, existing_var); + } + } + } + + // Pass 2: Add all custom variables (handles appending new and overwriting existing) + for (String8Node *n = params->env.first; n != 0; n = n->next) + { + str8_list_push(scratch.arena, &merged_env_list, n->string); + } + + // Convert the list to the final envp array + envp = push_array(scratch.arena, char *, merged_env_list.node_count + 1); + U64 envp_idx = 0; + for (String8Node *n = merged_env_list.first; n != 0; n = n->next) + { + // Need to copy the strings again as they might point to transient memory (environ) + String8 env_copy = push_str8_copy(scratch.arena, n->string); + envp[envp_idx++] = (char *)env_copy.str; + } + envp[envp_idx] = NULL; // Null-terminate the list + } + else if (params->inherit_env == 0 && params->env.node_count == 0) + { + // Use minimal empty environment + envp = push_array(scratch.arena, char *, 1); + envp[0] = NULL; + } + else // inherit_env == 1 and params->env.node_count == 0 + { + // Inherit existing environment (default behavior of execvp) + extern char **environ; + envp = environ; + } + + // Execute the command using execvpe which allows specifying environment + execvpe(argv[0], argv, envp); + + // execvpe only returns on error + perror("execvpe"); + _exit(127); // Indicate exec failed + } + else + { + // Parent process + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Process); + if (entity) + { + entity->process.pid = pid; + result.u64[0] = (U64)entity; + } + else + { + // Failed to allocate entity, kill the child? Or let it become a zombie? + // Let's just return zero handle for now. + kill(pid, SIGKILL); // Or SIGTERM + waitpid(pid, NULL, 0); // Reap the killed child + } + } + + scratch_end(scratch); + return result; +} + +internal B32 +os_process_join(OS_Handle handle, U64 endt_us) +{ + if (os_handle_match(handle, os_handle_zero())) { return 0; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)handle.u64[0]; + if (entity == 0 || entity->kind != OS_LNX_EntityKind_Process) { return 0; } + + pid_t pid = entity->process.pid; + B32 joined = 0; + + for (;;) + { + int status; + pid_t result = waitpid(pid, &status, WNOHANG); + + if (result == pid) + { + // Process exited or was signalled + joined = 1; + break; + } + else if (result == 0) + { + // Process still running + U64 now_us = os_now_microseconds(); + if (now_us >= endt_us) + { + // Timeout reached + break; + } + + // Calculate sleep time + U64 remaining_us = endt_us - now_us; + U32 sleep_ms = (U32)(remaining_us / 1000); + // Prevent sleeping for too long if timeout is very close, minimum sleep 1ms + sleep_ms = Max(1, Min(sleep_ms, 100)); + os_sleep_milliseconds(sleep_ms); + } + else + { + // Error in waitpid (e.g., process doesn't exist, permissions issue) + perror("waitpid"); + break; + } + } + + if (joined) + { + os_lnx_entity_release(entity); + } + + return joined; +} + +internal void +os_process_detach(OS_Handle handle) +{ + if (os_handle_match(handle, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)handle.u64[0]; + if (entity != 0 && entity->kind == OS_LNX_EntityKind_Process) + { + // We just release our tracking entity. The OS will handle the actual + // process becoming an orphan or being re-parented to init (pid 1). + os_lnx_entity_release(entity); + } +} + +//////////////////////////////// +//~ dan: @os_hooks Threads (Implemented Per-OS) + +internal OS_Handle +os_thread_launch(OS_ThreadFunctionType *func, void *ptr, void *params) +{ + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Thread); + entity->thread.func = func; + entity->thread.ptr = ptr; + { + int pthread_result = pthread_create(&entity->thread.handle, 0, os_lnx_thread_entry_point, entity); + if(pthread_result == -1) + { + os_lnx_entity_release(entity); + entity = 0; + } + } + OS_Handle handle = {(U64)entity}; + return handle; +} + +internal B32 +os_thread_join(OS_Handle handle, U64 endt_us) +{ + if(os_handle_match(handle, os_handle_zero())) { return 0; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)handle.u64[0]; + B32 result = 0; + int join_result = 0; + + if (endt_us == max_U64) + { + // Infinite wait + join_result = pthread_join(entity->thread.handle, 0); + } + else + { + // Timed wait using absolute deadline + struct timespec ts_deadline; + ts_deadline.tv_sec = endt_us / Million(1); + ts_deadline.tv_nsec = (endt_us % Million(1)) * Thousand(1); + + // NOTE: Requires _GNU_SOURCE, which is defined in os_core_linux.h + join_result = pthread_timedjoin_np(entity->thread.handle, 0, &ts_deadline); + } + + // Join succeeded if return code is 0. ETIMEDOUT indicates timeout. + result = (join_result == 0); + + // Release the entity regardless of join success, similar to Win32 closing the handle. + // The caller should not attempt to join again. + os_lnx_entity_release(entity); + return result; +} + +internal void +os_thread_detach(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)handle.u64[0]; + os_lnx_entity_release(entity); +} + +//////////////////////////////// +//~ dan: @os_hooks Synchronization Primitives (Implemented Per-OS) + +//- rjf: mutexes + +internal OS_Handle +os_mutex_alloc(void) +{ + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Mutex); + // Initialization is now done in os_lnx_entity_alloc + if(entity == 0) // Check if allocation failed + { + return os_handle_zero(); + } + OS_Handle handle = {(U64)entity}; + return handle; +} + +internal void +os_mutex_release(OS_Handle mutex) +{ + if(os_handle_match(mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)mutex.u64[0]; + // No explicit destruction needed for atomics + os_lnx_entity_release(entity); +} + +internal void +os_mutex_take(OS_Handle mutex) +{ + if(os_handle_match(mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)mutex.u64[0]; + U32 tid = os_tid(); + + // Check for recursion + if (atomic_load_explicit(&entity->mutex.owner_tid, memory_order_relaxed) == tid) { + atomic_fetch_add_explicit(&entity->mutex.recursion_depth, 1, memory_order_relaxed); + return; + } + + // Try to acquire the lock uncontended + unsigned int zero = 0; + if (atomic_compare_exchange_strong_explicit(&entity->mutex.futex, &zero, 1, memory_order_acquire, memory_order_relaxed)) + { + // Acquired uncontended + atomic_store_explicit(&entity->mutex.owner_tid, tid, memory_order_relaxed); + atomic_store_explicit(&entity->mutex.recursion_depth, 1, memory_order_relaxed); + return; + } + + // Lock is contended, prepare to wait + do { + // Mark as contended if it's currently locked uncontended (state 1) + unsigned int one = 1; + if (atomic_compare_exchange_strong_explicit(&entity->mutex.futex, &one, 2, memory_order_relaxed, memory_order_relaxed)) + { + // Successfully marked as contended, now wait + os_lnx_futex_wait(&entity->mutex.futex, 2, NULL); // Wait indefinitely if it's 2 (contended) + } + + // After waking up or if it was already contended (state 2), try to acquire again. + // We need to transition from 0 (unlocked) to 2 (contended lock) because other waiters might exist. + zero = 0; + } while (!atomic_compare_exchange_strong_explicit(&entity->mutex.futex, &zero, 2, memory_order_acquire, memory_order_relaxed)); + + // Acquired contended lock + atomic_store_explicit(&entity->mutex.owner_tid, tid, memory_order_relaxed); + atomic_store_explicit(&entity->mutex.recursion_depth, 1, memory_order_relaxed); +} + +internal void +os_mutex_drop(OS_Handle mutex) +{ + if(os_handle_match(mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)mutex.u64[0]; + U32 tid = os_tid(); + + // Check ownership (optional but good practice) + Assert(atomic_load_explicit(&entity->mutex.owner_tid, memory_order_relaxed) == tid); + + // Decrement recursion depth + unsigned int rec_depth = atomic_fetch_sub_explicit(&entity->mutex.recursion_depth, 1, memory_order_relaxed); + + // If recursion depth > 1 before decrement, we're done + if (rec_depth > 1) { + return; + } + + // Reset owner TID as we are fully unlocking + atomic_store_explicit(&entity->mutex.owner_tid, 0, memory_order_relaxed); + + // Release the lock + // Transition from 2 (contended) or 1 (uncontended) to 0 (unlocked) + if (atomic_exchange_explicit(&entity->mutex.futex, 0, memory_order_release) == 2) + { + // If it was contended (state 2), wake one waiting thread + os_lnx_futex_wake(&entity->mutex.futex, 1); + } +} + +//- rjf: reader/writer mutexes + +internal OS_Handle +os_rw_mutex_alloc(void) +{ + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_RWMutex); + // Initialization is now done in os_lnx_entity_alloc + if(entity == 0) // Check if allocation failed + { + return os_handle_zero(); + } + OS_Handle handle = {(U64)entity}; + return handle; +} + +internal void +os_rw_mutex_release(OS_Handle rw_mutex) +{ + if(os_handle_match(rw_mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; + // No explicit destruction needed for atomics + os_lnx_entity_release(entity); +} + +internal void +os_rw_mutex_take_r(OS_Handle rw_mutex) +{ + if(os_handle_match(rw_mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; + atomic_uint *futex = &entity->rw_mutex.futex; + + unsigned int current_state = atomic_load_explicit(futex, memory_order_relaxed); + while (1) { + // Check if a writer holds the lock or writers are waiting (writer preference) + if (current_state & (RW_MUTEX_WRITE_HELD_MASK | RW_MUTEX_WRITE_WAIT_MASK)) { + // Mark that readers are waiting + unsigned int expected = current_state; + unsigned int desired = current_state | RW_MUTEX_READ_WAIT_MASK; + // Attempt to set the wait flag, weak is fine as we'll re-check and wait anyway + atomic_compare_exchange_weak_explicit(futex, &expected, desired, memory_order_relaxed, memory_order_relaxed); + // Use the potentially updated state (expected) for waiting + current_state = expected | RW_MUTEX_READ_WAIT_MASK; // Assume wait flag is now set for the wait condition + + // Wait if still necessary (writer held or waiting) + os_lnx_futex_wait(futex, current_state, NULL); + + // Reload state after wake and retry loop + current_state = atomic_load_explicit(futex, memory_order_relaxed); + continue; + } + + // Attempt to increment reader count + unsigned int new_state = current_state + 1; + // Check for reader count overflow (highly unlikely with 16 bits) + if ((new_state & RW_MUTEX_READ_MASK) == 0) { + current_state = atomic_load_explicit(futex, memory_order_relaxed); // Reload state on overflow attempt + continue; + } + + // Try acquiring the read lock using weak CAS + if (atomic_compare_exchange_weak_explicit(futex, ¤t_state, new_state, memory_order_acquire, memory_order_relaxed)) { + // Successfully acquired read lock + break; + } + // CAS failed, loop continues with updated current_state (automatically loaded by failed CAS) + } +} + +internal void +os_rw_mutex_drop_r(OS_Handle rw_mutex) +{ + if(os_handle_match(rw_mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; + atomic_uint *futex = &entity->rw_mutex.futex; + + // Decrement reader count and get the state *before* decrementing + unsigned int prev_state = atomic_fetch_sub_explicit(futex, 1, memory_order_release); + + unsigned int readers_before = prev_state & RW_MUTEX_READ_MASK; + unsigned int writers_waiting = prev_state & RW_MUTEX_WRITE_WAIT_MASK; + + // If this was the last reader AND writers are waiting + if (readers_before == 1 && writers_waiting) { + // Wake one potential writer. The woken writer will handle clearing the wait flag if it acquires the lock. + os_lnx_futex_wake(futex, 1); + } +} + +internal void +os_rw_mutex_take_w(OS_Handle rw_mutex) +{ + if(os_handle_match(rw_mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; + atomic_uint *futex = &entity->rw_mutex.futex; + + unsigned int current_state = atomic_load_explicit(futex, memory_order_relaxed); + while (1) { + if (current_state == 0) { // Lock is free + // Try to acquire uncontended using weak CAS + if (atomic_compare_exchange_weak_explicit(futex, ¤t_state, RW_MUTEX_WRITE_HELD_MASK, memory_order_acquire, memory_order_relaxed)) { + return; // Acquired write lock + } + // CAS failed, loop continues with updated current_state + continue; + } + + // Lock is not free (readers exist, writer holds, or waiters exist) + unsigned int expected = current_state; + unsigned int desired = current_state | RW_MUTEX_WRITE_WAIT_MASK; + + // Mark that a writer is waiting, if not already marked + if (!(current_state & RW_MUTEX_WRITE_WAIT_MASK)) { + atomic_compare_exchange_weak_explicit(futex, &expected, desired, memory_order_relaxed, memory_order_relaxed); + // Use the potentially updated state for the wait check + current_state = expected | RW_MUTEX_WRITE_WAIT_MASK; + } else { + current_state = expected; // Use the state loaded or from failed CAS + } + + // Wait if the lock is held by readers or another writer + if (current_state & (RW_MUTEX_READ_MASK | RW_MUTEX_WRITE_HELD_MASK)) { + os_lnx_futex_wait(futex, current_state, NULL); // Wait on the current state + // Reload state after wake and retry loop + current_state = atomic_load_explicit(futex, memory_order_relaxed); + } else { + // Lock is not held (only waiter flags might be set). Try to acquire. + expected = current_state & ~RW_MUTEX_WRITE_HELD_MASK; // Expect lock not held + desired = (expected | RW_MUTEX_WRITE_HELD_MASK) & ~RW_MUTEX_WRITE_WAIT_MASK; // Set HELD, clear our WAIT bit + + if (atomic_compare_exchange_weak_explicit(futex, &expected, desired, memory_order_acquire, memory_order_relaxed)) { + // Acquired write lock + return; + } + // CAS failed, retry loop with updated current_state (expected) + current_state = expected; + } + } +} + +internal void +os_rw_mutex_drop_w(OS_Handle rw_mutex) +{ + if(os_handle_match(rw_mutex, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)rw_mutex.u64[0]; + atomic_uint *futex = &entity->rw_mutex.futex; + + // Atomically clear the write-held flag, get the state *before* clearing. + unsigned int prev_state = atomic_fetch_and_explicit(futex, ~RW_MUTEX_WRITE_HELD_MASK, memory_order_release); + + // Check wait flags from the previous state to decide who to wake. + if (prev_state & RW_MUTEX_WRITE_WAIT_MASK) { + // Writers were waiting. Wake one. + os_lnx_futex_wake(futex, 1); + } else if (prev_state & RW_MUTEX_READ_WAIT_MASK) { + // No writers waiting, but readers were. Try to clear the read wait flag and wake all readers. + unsigned int current_state_after_drop = atomic_load_explicit(futex, memory_order_relaxed); + while(current_state_after_drop & RW_MUTEX_READ_WAIT_MASK) { + unsigned int desired = current_state_after_drop & ~RW_MUTEX_READ_WAIT_MASK; + if(atomic_compare_exchange_weak_explicit(futex, ¤t_state_after_drop, desired, memory_order_relaxed, memory_order_relaxed)) { + break; // Flag cleared + } + // retry if CAS failed, current_state_after_drop is updated by CAS + } + os_lnx_futex_wake(futex, INT_MAX); // Wake all potentially waiting readers + } +} + +//- rjf: condition variables + +internal OS_Handle +os_condition_variable_alloc(void) +{ + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_ConditionVariable); + // Initialization is now done in os_lnx_entity_alloc + if(entity == 0) // Check if allocation failed + { + return os_handle_zero(); + } + OS_Handle handle = {(U64)entity}; + return handle; +} + +internal void +os_condition_variable_release(OS_Handle cv) +{ + if(os_handle_match(cv, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)cv.u64[0]; + // No explicit destruction needed for atomics + os_lnx_entity_release(entity); +} + +internal B32 +os_condition_variable_wait(OS_Handle cv, OS_Handle mutex, U64 endt_us) +{ + if(os_handle_match(cv, os_handle_zero())) { return 0; } + OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; + OS_LNX_Entity *mutex_entity = (OS_LNX_Entity *)mutex.u64[0]; + struct timespec endt_timespec; + endt_timespec.tv_sec = endt_us/Million(1); + endt_timespec.tv_nsec = Thousand(1) * (endt_us - (endt_us/Million(1))*Million(1)); + + // 1. Read the current sequence number + unsigned int current_seq = atomic_load_explicit(&cv_entity->cv.futex, memory_order_relaxed); + + // 2. Unlock the associated *external* mutex (must be done by caller before wait) + // IMPORTANT: The API expects the mutex passed here is the one protecting the condition. + // The original code's use of rwlock_mutex_handle inside the CV entity was incorrect. + os_mutex_drop(mutex); // Release the *external* mutex + + // 3. Wait on the futex, checking if the sequence number has changed + int wait_result = os_lnx_futex_wait(&cv_entity->cv.futex, current_seq, &endt_timespec); + + // 4. Re-lock the external mutex (must be done by caller *after* wait returns) + os_mutex_take(mutex); + + // 5. Determine result + B32 result = (wait_result != -1 || errno != ETIMEDOUT); + // Futex wait returns 0 on success (woken), -1 on error. Check errno for timeout. + return result; +} + +internal B32 +os_condition_variable_wait_rw_r(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) +{ + if(os_handle_match(cv, os_handle_zero()) || os_handle_match(mutex_rw, os_handle_zero())) { return 0; } + OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; + OS_LNX_Entity *rw_mutex_entity = (OS_LNX_Entity *)mutex_rw.u64[0]; // Assuming mutex_rw is the RW mutex handle + + if (cv_entity == 0 || cv_entity->kind != OS_LNX_EntityKind_ConditionVariable || + rw_mutex_entity == 0 || rw_mutex_entity->kind != OS_LNX_EntityKind_RWMutex) + { + // Invalid handle types + return 0; + } + + struct timespec *timeout_ts_ptr = 0; // Default to infinite wait + struct timespec timeout_ts; + if (endt_us != max_U64) + { + // Check for immediate timeout + U64 now_us = os_now_microseconds(); + if (now_us >= endt_us) { + errno = ETIMEDOUT; // Set errno for timeout + return 0; // Timeout already expired + } + // Use monotonic clock for absolute timeout + timeout_ts.tv_sec = endt_us / Million(1); + timeout_ts.tv_nsec = (endt_us % Million(1)) * 1000; + timeout_ts_ptr = &timeout_ts; + } + + // Similar logic to normal CV wait, but using the RW lock + unsigned int current_seq = atomic_load_explicit(&cv_entity->cv.futex, memory_order_relaxed); + + // Unlock the *external* RW lock (Read mode) + os_rw_mutex_drop_r(mutex_rw); + + // Wait on the futex using the sequence number and timeout + int wait_result = os_lnx_futex_wait(&cv_entity->cv.futex, current_seq, timeout_ts_ptr); + + // Re-lock the *external* RW lock (Read mode) + os_rw_mutex_take_r(mutex_rw); + + // Check result: 0 means woken up, -1 means error. Check errno for timeout. + B32 result = (wait_result == 0); + if (wait_result == -1 && errno == ETIMEDOUT) { + result = 0; // Explicitly return false on timeout + } else if (wait_result == -1) { + // Some other error occurred during wait + perror("os_lnx_futex_wait in os_condition_variable_wait_r"); + result = 0; // Consider other errors as failure + } + + return result; +} + +internal B32 +os_condition_variable_wait_rw_w(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) +{ + if(os_handle_match(cv, os_handle_zero()) || os_handle_match(mutex_rw, os_handle_zero())) { return 0; } + OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; + OS_LNX_Entity *rw_mutex_entity = (OS_LNX_Entity *)mutex_rw.u64[0]; // Assuming mutex_rw is the RW mutex handle + + if (cv_entity == 0 || cv_entity->kind != OS_LNX_EntityKind_ConditionVariable || + rw_mutex_entity == 0 || rw_mutex_entity->kind != OS_LNX_EntityKind_RWMutex) + { + // Invalid handle types + return 0; + } + + struct timespec *timeout_ts_ptr = 0; // Default to infinite wait + struct timespec timeout_ts; + if (endt_us != max_U64) + { + // Check for immediate timeout + U64 now_us = os_now_microseconds(); + if (now_us >= endt_us) { + errno = ETIMEDOUT; // Set errno for timeout + return 0; // Timeout already expired + } + // Use monotonic clock for absolute timeout + timeout_ts.tv_sec = endt_us / Million(1); + timeout_ts.tv_nsec = (endt_us % Million(1)) * 1000; + timeout_ts_ptr = &timeout_ts; + } + + // Similar logic to normal CV wait, but using the RW lock + unsigned int current_seq = atomic_load_explicit(&cv_entity->cv.futex, memory_order_relaxed); + + // Unlock the *external* RW lock (Write mode) + os_rw_mutex_drop_w(mutex_rw); + + // Wait on the futex using the sequence number and timeout + int wait_result = os_lnx_futex_wait(&cv_entity->cv.futex, current_seq, timeout_ts_ptr); + + // Re-lock the *external* RW lock (Write mode) + os_rw_mutex_take_w(mutex_rw); + + // Check result: 0 means woken up, -1 means error. Check errno for timeout. + B32 result = (wait_result == 0); + if (wait_result == -1 && errno == ETIMEDOUT) { + result = 0; // Explicitly return false on timeout + } else if (wait_result == -1) { + // Some other error occurred during wait + perror("os_lnx_futex_wait in os_condition_variable_wait_w"); + result = 0; // Consider other errors as failure + } + + return result; +} + +internal void +os_condition_variable_signal(OS_Handle cv) +{ + if(os_handle_match(cv, os_handle_zero())) { return; } + OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; + // Increment sequence number to mark a change + atomic_fetch_add_explicit(&cv_entity->cv.futex, 1, memory_order_relaxed); + // Wake one waiting thread + os_lnx_futex_wake(&cv_entity->cv.futex, 1); +} + +internal void +os_condition_variable_broadcast(OS_Handle cv) +{ + if(os_handle_match(cv, os_handle_zero())) { return; } + OS_LNX_Entity *cv_entity = (OS_LNX_Entity *)cv.u64[0]; + // Increment sequence number + atomic_fetch_add_explicit(&cv_entity->cv.futex, 1, memory_order_relaxed); + // Wake all waiting threads + os_lnx_futex_wake(&cv_entity->cv.futex, INT_MAX); // INT_MAX wakes all +} + +//- rjf: cross-process semaphores + +internal OS_Handle +os_semaphore_alloc(U32 initial_count, U32 max_count, String8 name) +{ + // NOTE(rjf): max_count ignored on linux - POSIX semaphores don't have a max count concept + // like Windows semaphores do. We clamp initial_count to SEM_VALUE_MAX. + if (initial_count > SEM_VALUE_MAX) + { + initial_count = SEM_VALUE_MAX; + } + + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Semaphore); + if (!entity) + { + return os_handle_zero(); + } + + if (name.size > 0) + { + // Named Semaphore + entity->semaphore.is_named = 1; + Temp scratch = scratch_begin(0, 0); // Use scratch for temporary name manipulation + + // POSIX requires named semaphores to start with '/' + String8 name_copy = name; + if (name.str[0] != '/') + { + name_copy = push_str8f(scratch.arena, "/%S", name); + } + + // Store the final name (potentially prefixed) in the entity arena + entity->semaphore.name = push_str8_copy(os_lnx_state.entity_arena, name_copy); + + // O_EXCL ensures we create a new one or fail if it exists + entity->semaphore.named_handle = sem_open((char *)entity->semaphore.name.str, O_CREAT | O_EXCL | O_RDWR, 0666, initial_count); + scratch_end(scratch); + + if (entity->semaphore.named_handle == SEM_FAILED) + { + perror("sem_open (alloc)"); + // Arena free for name happens when entity is released + os_lnx_entity_release(entity); + return os_handle_zero(); + } + } + else + { + // Unnamed Semaphore (process-local) + entity->semaphore.is_named = 0; + entity->semaphore.name = str8_zero(); + entity->semaphore.named_handle = SEM_FAILED; // Explicitly mark named handle as invalid + + // Initialize the embedded unnamed semaphore (pshared = 0 for process-local) + int err = sem_init(&entity->semaphore.unnamed_handle, 0, initial_count); + if (err != 0) + { + perror("sem_init"); + os_lnx_entity_release(entity); + return os_handle_zero(); + } + } + + OS_Handle result = {(U64)entity}; + return result; +} + +internal void +os_semaphore_release(OS_Handle semaphore) +{ + if (os_handle_match(semaphore, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)semaphore.u64[0]; + if (entity == 0 || entity->kind != OS_LNX_EntityKind_Semaphore) { return; } + + if (entity->semaphore.is_named) + { + if (entity->semaphore.named_handle != SEM_FAILED) + { + sem_close(entity->semaphore.named_handle); + // NOTE(improvement): Removed sem_unlink. Release should only close the handle + // for named semaphores, similar to Win32 CloseHandle, allowing other + // openers to continue using it. Unlinking should be a separate concern + // if needed (e.g., a dedicated destroy function or manual cleanup). + // Name string is freed when entity_arena is managed + } + } + else + { + // Destroy the embedded unnamed semaphore + sem_destroy(&entity->semaphore.unnamed_handle); + } + + os_lnx_entity_release(entity); +} + +internal OS_Handle +os_semaphore_open(String8 name) +{ + if (name.size == 0) { return os_handle_zero(); } // Named semaphores must have a name + + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_Semaphore); + if (!entity) + { + return os_handle_zero(); + } + + entity->semaphore.is_named = 1; + Temp scratch = scratch_begin(0, 0); + + String8 name_copy = name; + if (name.str[0] != '/') + { + name_copy = push_str8f(scratch.arena, "/%S", name); + } + entity->semaphore.name = push_str8_copy(os_lnx_state.entity_arena, name_copy); + + // Open existing named semaphore + entity->semaphore.named_handle = sem_open((char *)entity->semaphore.name.str, O_RDWR); + scratch_end(scratch); + + if (entity->semaphore.named_handle == SEM_FAILED) + { + perror("sem_open (open)"); + // Arena free for name happens when entity is released + os_lnx_entity_release(entity); + return os_handle_zero(); + } + + OS_Handle result = {(U64)entity}; + return result; +} + +internal void +os_semaphore_close(OS_Handle semaphore) +{ + // This function behaves similarly to release for named semaphores, + // but doesn't unlink the name or destroy unnamed semaphores. + // It just closes the handle/connection for this process. + if (os_handle_match(semaphore, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)semaphore.u64[0]; + if (entity == 0 || entity->kind != OS_LNX_EntityKind_Semaphore) { return; } + + if (entity->semaphore.is_named && entity->semaphore.named_handle != SEM_FAILED) + { + sem_close(entity->semaphore.named_handle); + // Do not unlink here, just close the process's handle to it. + } + // For unnamed semaphores, there's no "close" separate from destroy/release. + + // Release the entity tracking structure itself. + os_lnx_entity_release(entity); +} + +internal B32 +os_semaphore_take(OS_Handle semaphore, U64 endt_us) +{ + if (os_handle_match(semaphore, os_handle_zero())) { return 0; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)semaphore.u64[0]; + if (entity == 0 || entity->kind != OS_LNX_EntityKind_Semaphore) { return 0; } + + sem_t *sem_handle = entity->semaphore.is_named ? entity->semaphore.named_handle : &entity->semaphore.unnamed_handle; + if (entity->semaphore.is_named && sem_handle == SEM_FAILED) { return 0; } // Check named handle validity + + int err = 0; + + if (endt_us == max_U64) + { + // Infinite wait + while ((err = sem_wait(sem_handle)) == -1 && errno == EINTR) + { + continue; // Restart if interrupted by handler + } + } + else + { + // Timed wait using monotonic clock + struct timespec ts_deadline; + U64 now_us = os_now_microseconds(); + + if (now_us >= endt_us) { + // Timeout already expired, try once non-blockingly + err = sem_trywait(sem_handle); + if (err == -1 && errno == EAGAIN) { // EAGAIN means would block + errno = ETIMEDOUT; // Report as timeout + } + } else { + // Calculate absolute deadline based on CLOCK_MONOTONIC + ts_deadline.tv_sec = endt_us / Million(1); + ts_deadline.tv_nsec = (endt_us % Million(1)) * 1000; + + // Call sem_clockwait with the absolute monotonic deadline + // Requires _POSIX_TIMEOUTS and _POSIX_MONOTONIC_CLOCK, enabled by _GNU_SOURCE + while ((err = sem_clockwait(sem_handle, CLOCK_MONOTONIC, &ts_deadline)) == -1 && errno == EINTR) + { + continue; // Restart if interrupted by handler + } + } + } + + if (err == 0) + { + return 1; // Success + } + else + { + // ETIMEDOUT is expected on timeout, other errors are reported. + if (errno != ETIMEDOUT) { + // Updated error message context + perror("sem_wait/sem_trywait/sem_clockwait"); + } + return 0; // Timeout or other error + } +} + +internal void +os_semaphore_drop(OS_Handle semaphore) +{ + if (os_handle_match(semaphore, os_handle_zero())) { return; } + OS_LNX_Entity *entity = (OS_LNX_Entity *)semaphore.u64[0]; + if (entity == 0 || entity->kind != OS_LNX_EntityKind_Semaphore) { return; } + + sem_t *sem_handle = entity->semaphore.is_named ? entity->semaphore.named_handle : &entity->semaphore.unnamed_handle; + if (entity->semaphore.is_named && sem_handle == SEM_FAILED) { return; } // Check named handle validity + + int err = 0; + + // sem_post is generally non-blocking unless SEM_VALUE_MAX is reached (on some systems) + while ((err = sem_post(sem_handle)) == -1 && errno == EINTR) + { + continue; // Restart if interrupted by handler + } + + if (err != 0) { + // SEM_OVF is the most likely error if max value is exceeded, + // but other errors are possible. + perror("sem_post"); + } +} + +//////////////////////////////// +//~ dan: @os_hooks Dynamically-Loaded Libraries (Implemented Per-OS) + +internal OS_Handle +os_library_open(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + char *path_cstr = (char *)push_str8_copy(scratch.arena, path).str; + void *so = dlopen(path_cstr, RTLD_LAZY|RTLD_LOCAL); + OS_Handle lib = { (U64)so }; + scratch_end(scratch); + return lib; +} + +internal VoidProc* +os_library_load_proc(OS_Handle lib, String8 name) +{ + Temp scratch = scratch_begin(0, 0); + void *so = (void *)lib.u64; + char *name_cstr = (char *)push_str8_copy(scratch.arena, name).str; + VoidProc *proc = (VoidProc *)dlsym(so, name_cstr); + scratch_end(scratch); + return proc; +} + +internal void +os_library_close(OS_Handle lib) +{ + void *so = (void *)lib.u64; + dlclose(so); +} + +//////////////////////////////// +//~ dan: @os_hooks Safe Calls (Implemented Per-OS) + +internal void +os_safe_call(OS_ThreadFunctionType *func, OS_ThreadFunctionType *fail_handler, void *ptr) +{ + // Allocate an entity for this safe call frame + OS_LNX_Entity *entity = os_lnx_entity_alloc(OS_LNX_EntityKind_SafeCallChain); + if (!entity) { + // Allocation failed, cannot proceed safely. Maybe abort? + os_abort(-1); + return; + } + + // Store parameters and link to previous chain node + entity->safe_call_chain.fail_handler = fail_handler; + entity->safe_call_chain.ptr = ptr; + entity->safe_call_chain.caller_chain_node = os_lnx_safe_call_chain; + entity->safe_call_chain.num_signals_handled = 0; + + // Push this frame onto the thread-local chain + os_lnx_safe_call_chain = entity; + + // Save current signal mask + sigset_t original_mask; + pthread_sigmask(SIG_BLOCK, NULL, &original_mask); + + // Set up signal handlers for critical errors + struct sigaction new_action = {0}; + new_action.sa_handler = os_lnx_safe_call_sig_handler; // The handler now just jumps + sigfillset(&new_action.sa_mask); // Block all signals within the handler + new_action.sa_flags = SA_NODEFER; // Don't block the signal itself in the handler + + int signals_to_handle[] = { + SIGILL, SIGFPE, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP + }; + const int num_signals = ArrayCount(signals_to_handle); + + for(int i = 0; i < num_signals; ++i) + { + int sig = signals_to_handle[i]; + if (sigaction(sig, &new_action, &entity->safe_call_chain.original_actions[i]) == 0) + { + entity->safe_call_chain.signals_handled[entity->safe_call_chain.num_signals_handled] = sig; + entity->safe_call_chain.num_signals_handled++; + } + // TODO(rjf): Consider logging failure to set a handler? + } + + // Use sigsetjmp to save the context. Returns 0 on initial call. + int jmp_result = sigsetjmp(entity->safe_call_chain.jmp_buf, 1); // Save signal mask + + if (jmp_result == 0) + { + // Initial call: execute the user function + func(ptr); + } + else + { + // Jumped back from signal handler (jmp_result is the signal number) + // Call the fail handler *after* unwinding from the signal context. + if(entity->safe_call_chain.fail_handler != 0) + { + entity->safe_call_chain.fail_handler(entity->safe_call_chain.ptr); + } + else + { + // Default behavior if no fail handler: abort? + fprintf(stderr, "Unhandled fatal signal %d in os_safe_call, aborting.\n", jmp_result); + os_abort(jmp_result); // Exit with signal number + } + } + + // Cleanup: Restore original signal handlers + for(int i = 0; i < entity->safe_call_chain.num_signals_handled; ++i) + { + int sig = entity->safe_call_chain.signals_handled[i]; + sigaction(sig, &entity->safe_call_chain.original_actions[i], NULL); + } + + // Restore original signal mask + pthread_sigmask(SIG_SETMASK, &original_mask, NULL); + + // Pop this frame from the thread-local chain + os_lnx_safe_call_chain = entity->safe_call_chain.caller_chain_node; + + // Release the entity for this frame + os_lnx_entity_release(entity); +} + +//////////////////////////////// +//~ dan: @os_hooks GUIDs (Implemented Per-OS) + +internal Guid +os_make_guid(void) +{ + Guid guid = {0}; + getrandom(guid.v, sizeof(guid.v), 0); + guid.data3 &= 0x0fff; + guid.data3 |= (4 << 12); + guid.data4[0] &= 0x3f; + guid.data4[0] |= 0x80; + return guid; +} + +//////////////////////////////// +//~ dan: Signal Handler for Safe Calls + +internal void +os_lnx_safe_call_sig_handler(int sig) +{ + // This handler should be minimal and async-signal-safe. + // It jumps back to the corresponding os_safe_call frame. + if(os_lnx_safe_call_chain != 0) + { + // Restore the original handler for this signal *before* longjmping + // to prevent potential re-entry issues if the signal occurs again + // immediately after the jump. Find the original action for `sig`. + OS_LNX_Entity *current_frame = os_lnx_safe_call_chain; + for(int i = 0; i < current_frame->safe_call_chain.num_signals_handled; ++i) + { + if(current_frame->safe_call_chain.signals_handled[i] == sig) + { + sigaction(sig, ¤t_frame->safe_call_chain.original_actions[i], NULL); + break; // Found and restored + } + } + // Now jump back. Pass the signal number as the return value. + siglongjmp(current_frame->safe_call_chain.jmp_buf, sig); + } + else + { + // Should not happen if os_safe_call is used correctly. + // If it does, we have no context to jump back to. Abort. + fprintf(stderr, "Fatal signal %d outside of os_safe_call context! Aborting.\n", sig); + // Reset to default handler and re-raise? Or just exit? + signal(sig, SIG_DFL); + raise(sig); + _exit(sig); // Force exit if raise doesn't terminate + } +} + +//////////////////////////////// +//~ dan: @os_hooks Entry Points (Implemented Per-OS) + +int +main(int argc, char **argv) +{ + //- rjf: set up OS layer + { + //- rjf: get statically-allocated system/process info + { + OS_SystemInfo *info = &os_lnx_state.system_info; + info->logical_processor_count = (U32)get_nprocs(); + info->page_size = (U64)getpagesize(); + info->large_page_size = MB(2); + info->allocation_granularity = info->page_size; + } + { + OS_ProcessInfo *info = &os_lnx_state.process_info; + info->pid = (U32)getpid(); + } + + //- rjf: set up thread context + local_persist TCTX tctx; + tctx_init_and_equip(&tctx); + + //- rjf: set up dynamically allocated state + os_lnx_state.arena = arena_alloc(); + os_lnx_state.entity_arena = arena_alloc(); + pthread_mutex_init(&os_lnx_state.entity_mutex, 0); + + //- rjf: grab dynamically allocated system info + { + Temp scratch = scratch_begin(0, 0); + OS_SystemInfo *info = &os_lnx_state.system_info; + + // get machine name + B32 got_final_result = 0; + U8 *buffer = 0; + int size = 0; + for(S64 cap = 4096, r = 0; r < 4; cap *= 2, r += 1) + { + scratch_end(scratch); + buffer = push_array_no_zero(scratch.arena, U8, cap); + size = gethostname((char*)buffer, cap); + if(size < cap) + { + got_final_result = 1; + break; + } + } + + // save name to info + if(got_final_result && size > 0) + { + info->machine_name.size = size; + info->machine_name.str = push_array_no_zero(os_lnx_state.arena, U8, info->machine_name.size + 1); + MemoryCopy(info->machine_name.str, buffer, info->machine_name.size); + info->machine_name.str[info->machine_name.size] = 0; + } + + scratch_end(scratch); + } + + //- rjf: grab dynamically allocated process info + { + Temp scratch = scratch_begin(0, 0); + OS_ProcessInfo *info = &os_lnx_state.process_info; + + // grab binary path + { + // get self string + B32 got_final_result = 0; + U8 *buffer = 0; + int size = 0; + for(S64 cap = PATH_MAX, r = 0; r < 4; cap *= 2, r += 1) + { + scratch_end(scratch); + buffer = push_array_no_zero(scratch.arena, U8, cap); + size = readlink("/proc/self/exe", (char*)buffer, cap); + if(size < cap) + { + got_final_result = 1; + break; + } + } + + // save + if(got_final_result && size > 0) + { + String8 full_name = str8(buffer, size); + String8 name_chopped = str8_chop_last_slash(full_name); + info->binary_path = push_str8_copy(os_lnx_state.arena, name_chopped); + } + } + + // grab initial directory + { + info->initial_path = os_get_current_path(os_lnx_state.arena); + } + + // grab home directory + { + char *home = getenv("HOME"); + info->user_program_data_path = str8_cstring(home); + } + + // grab environment variables + { + extern char **environ; + if (environ != 0) + { + for (char **env_ptr = environ; *env_ptr != 0; env_ptr += 1) + { + String8 env_var = str8_cstring(*env_ptr); + str8_list_push(os_lnx_state.arena, &info->environment, env_var); + } + } + } + + scratch_end(scratch); + } + } + + //- rjf: call into "real" entry point + main_thread_base_entry_point(argc, argv); +} + diff --git a/src/os/core/linux/os_core_linux.h b/src/os/core/linux/os_core_linux.h index e7d2ba5ac..40fd43808 100644 --- a/src/os/core/linux/os_core_linux.h +++ b/src/os/core/linux/os_core_linux.h @@ -1,137 +1,212 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -#ifndef OS_CORE_LINUX_H -#define OS_CORE_LINUX_H - -//////////////////////////////// -//~ rjf: Includes - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -pid_t gettid(void); -int pthread_setname_np(pthread_t thread, const char *name); -int pthread_getname_np(pthread_t thread, char *name, size_t size); - -typedef struct tm tm; -typedef struct timespec timespec; - -//////////////////////////////// -//~ rjf: File Iterator - -typedef struct OS_LNX_FileIter OS_LNX_FileIter; -struct OS_LNX_FileIter -{ - DIR *dir; - struct dirent *dp; - String8 path; -}; -StaticAssert(sizeof(Member(OS_FileIter, memory)) >= sizeof(OS_LNX_FileIter), os_lnx_file_iter_size_check); - -//////////////////////////////// -//~ rjf: Safe Call Handler Chain - -typedef struct OS_LNX_SafeCallChain OS_LNX_SafeCallChain; -struct OS_LNX_SafeCallChain -{ - OS_LNX_SafeCallChain *next; - OS_ThreadFunctionType *fail_handler; - void *ptr; -}; - -//////////////////////////////// -//~ rjf: Entities - -typedef enum OS_LNX_EntityKind -{ - OS_LNX_EntityKind_Thread, - OS_LNX_EntityKind_Mutex, - OS_LNX_EntityKind_RWMutex, - OS_LNX_EntityKind_ConditionVariable, -} -OS_LNX_EntityKind; - -typedef struct OS_LNX_Entity OS_LNX_Entity; -struct OS_LNX_Entity -{ - OS_LNX_Entity *next; - OS_LNX_EntityKind kind; - union - { - struct - { - pthread_t handle; - OS_ThreadFunctionType *func; - void *ptr; - } thread; - pthread_mutex_t mutex_handle; - pthread_rwlock_t rwmutex_handle; - struct - { - pthread_cond_t cond_handle; - pthread_mutex_t rwlock_mutex_handle; - } cv; - }; -}; - -//////////////////////////////// -//~ rjf: State - -typedef struct OS_LNX_State OS_LNX_State; -struct OS_LNX_State -{ - Arena *arena; - OS_SystemInfo system_info; - OS_ProcessInfo process_info; - pthread_mutex_t entity_mutex; - Arena *entity_arena; - OS_LNX_Entity *entity_free; -}; - -//////////////////////////////// -//~ rjf: Globals - -global OS_LNX_State os_lnx_state = {0}; -thread_static OS_LNX_SafeCallChain *os_lnx_safe_call_chain = 0; - -//////////////////////////////// -//~ rjf: Helpers - -internal DateTime os_lnx_date_time_from_tm(tm in, U32 msec); -internal tm os_lnx_tm_from_date_time(DateTime dt); -internal timespec os_lnx_timespec_from_date_time(DateTime dt); -internal DenseTime os_lnx_dense_time_from_timespec(timespec in); -internal FileProperties os_lnx_file_properties_from_stat(struct stat *s); -internal void os_lnx_safe_call_sig_handler(int x); - -//////////////////////////////// -//~ rjf: Entities - -internal OS_LNX_Entity *os_lnx_entity_alloc(OS_LNX_EntityKind kind); -internal void os_lnx_entity_release(OS_LNX_Entity *entity); - -//////////////////////////////// -//~ rjf: Thread Entry Point - -internal void *os_lnx_thread_entry_point(void *ptr); - -#endif // OS_CORE_LINUX_H +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +#ifndef OS_CORE_LINUX_H +#define OS_CORE_LINUX_H + +//////////////////////////////// +//~ dan: Includes + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Explicit declarations for GNU extensions that might not be exposed +// by headers even when _GNU_SOURCE is defined. +// TODO this feels really stupid and not addressing the root cause of the problem: +extern char **environ; +int execvpe(const char *file, char *const argv[], char *const envp[]); +// From +int pthread_timedjoin_np(pthread_t thread, void **retval, const struct timespec *abstime); +// From +// Use _GNU_SOURCE as the guard since sem_clockwait is often tied to it +// and the specific POSIX macros might not be reliable everywhere. +int sem_clockwait(sem_t *sem, clockid_t clock_id, const struct timespec *abstime); + +#ifndef AT_EMPTY_PATH +#define AT_EMPTY_PATH 0x1000 +#endif + +pid_t gettid(void); +int pthread_setname_np(pthread_t thread, const char *name); +int pthread_getname_np(pthread_t thread, char *name, size_t size); + +typedef struct tm tm; +typedef struct timespec timespec; + +//////////////////////////////// +//~ dan: File Iterator + +typedef struct OS_LNX_FileIter OS_LNX_FileIter; +struct OS_LNX_FileIter +{ + DIR *dir; + struct dirent *dp; + String8 path; + B32 is_volume_iter; + FILE *mount_file_stream; + String8Array volume_mount_points; + U64 volume_iter_idx; +}; +StaticAssert(sizeof(Member(OS_FileIter, memory)) >= sizeof(OS_LNX_FileIter), os_lnx_file_iter_size_check); + +//////////////////////////////// +//~ dan: Safe Call Handler Chain + +typedef struct OS_LNX_SafeCallChain OS_LNX_SafeCallChain; +struct OS_LNX_SafeCallChain +{ + OS_LNX_SafeCallChain *next; + OS_ThreadFunctionType *fail_handler; + void *ptr; +}; + +//////////////////////////////// +//~ dan: Entities + +typedef enum OS_LNX_EntityKind +{ + OS_LNX_EntityKind_Thread, + OS_LNX_EntityKind_Mutex, + OS_LNX_EntityKind_RWMutex, + OS_LNX_EntityKind_ConditionVariable, + OS_LNX_EntityKind_Process, + OS_LNX_EntityKind_Semaphore, + OS_LNX_EntityKind_SafeCallChain, +} +OS_LNX_EntityKind; + +typedef struct OS_LNX_Entity OS_LNX_Entity; +struct OS_LNX_Entity +{ + OS_LNX_Entity *next; + OS_LNX_EntityKind kind; + union + { + struct + { + pthread_t handle; + OS_ThreadFunctionType *func; + void *ptr; + } thread; + struct // OS_LNX_EntityKind_Mutex + { + atomic_uint futex; // 0: unlocked, 1: locked uncontended, 2: locked contended + atomic_uint owner_tid; + atomic_uint recursion_depth; + } mutex; + struct // OS_LNX_EntityKind_RWMutex + { + atomic_uint futex; // Stores counts: lower 16 bits readers, upper 16 bits writers/waiters + // Special state for write lock held. +#define RW_MUTEX_READ_MASK 0x0000FFFFu // Lower 16 bits for reader count +#define RW_MUTEX_WRITE_MASK 0xFFFF0000u // Upper 16 bits used for writer/waiter state +#define RW_MUTEX_WRITE_HELD_MASK 0x80000000u // Top bit indicates writer holds the lock +#define RW_MUTEX_WRITE_WAIT_MASK 0x40000000u // Next bit indicates writers are waiting +#define RW_MUTEX_READ_WAIT_MASK 0x20000000u // Next bit indicates readers are waiting + } rw_mutex; + struct // OS_LNX_EntityKind_ConditionVariable + { + atomic_uint futex; // Sequence number or generation count for wakeups + // CVs need an associated mutex, handled externally by the API user + } cv; + struct + { + pid_t pid; + } process; + struct + { + sem_t *named_handle; // Handle for named semaphores from sem_open + sem_t unnamed_handle; // Embedded handle for unnamed semaphores + String8 name; // OS-internal name (e.g., with leading slash prepended) for named semaphores + B32 is_named; + } semaphore; + struct // OS_LNX_EntityKind_SafeCallChain + { + OS_ThreadFunctionType *fail_handler; + void *ptr; + sigjmp_buf jmp_buf; + struct sigaction original_actions[6]; // Max signals we handle + int signals_handled[6]; + int num_signals_handled; + OS_LNX_Entity *caller_chain_node; // Link to caller's node in the TLS chain + } safe_call_chain; + }; +}; + +//////////////////////////////// +//~ dan: State + +typedef struct OS_LNX_State OS_LNX_State; +struct OS_LNX_State +{ + Arena *arena; + OS_SystemInfo system_info; + OS_ProcessInfo process_info; + pthread_mutex_t entity_mutex; + Arena *entity_arena; + OS_LNX_Entity *entity_free; +}; + +//////////////////////////////// +//~ dan: Globals + +global OS_LNX_State os_lnx_state = {0}; +thread_static OS_LNX_Entity *os_lnx_safe_call_chain = 0; + +//////////////////////////////// +//~ dan: Helpers + +internal DateTime os_lnx_date_time_from_tm(tm in, U32 msec); +internal tm os_lnx_tm_from_date_time(DateTime dt); +internal timespec os_lnx_timespec_from_date_time(DateTime dt); +internal DenseTime os_lnx_dense_time_from_timespec(timespec in); +internal DenseTime os_lnx_dense_time_from_statx_timestamp(struct statx_timestamp in); +internal FileProperties os_lnx_file_properties_from_stat(struct stat *s); +internal void os_lnx_safe_call_sig_handler(int sig); + +//////////////////////////////// +//~ dan: Entities + +internal OS_LNX_Entity *os_lnx_entity_alloc(OS_LNX_EntityKind kind); +internal void os_lnx_entity_release(OS_LNX_Entity *entity); + +//////////////////////////////// +//~ dan: Futex Helpers + +internal int os_lnx_futex(atomic_uint *uaddr, int futex_op, unsigned int val, const struct timespec *timeout, unsigned int val3); +internal int os_lnx_futex_wait(atomic_uint *uaddr, unsigned int expected_val, const struct timespec *timeout); +internal int os_lnx_futex_wake(atomic_uint *uaddr, int num_to_wake); + +//////////////////////////////// +//~ dan: Thread Entry Point + +internal void *os_lnx_thread_entry_point(void *ptr); + +#endif // OS_CORE_LINUX_H diff --git a/src/os/core/win32/os_core_win32.c b/src/os/core/win32/os_core_win32.c index 6a0e02816..3cf517ef6 100644 --- a/src/os/core/win32/os_core_win32.c +++ b/src/os/core/win32/os_core_win32.c @@ -1,1739 +1,1739 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -//////////////////////////////// -//~ rjf: Modern Windows SDK Functions -// -// (We must dynamically link to them, since they can be missing in older SDKs) - -typedef HRESULT W32_SetThreadDescription_Type(HANDLE hThread, PCWSTR lpThreadDescription); -global W32_SetThreadDescription_Type *w32_SetThreadDescription_func = 0; - -//////////////////////////////// -//~ rjf: File Info Conversion Helpers - -internal FilePropertyFlags -os_w32_file_property_flags_from_dwFileAttributes(DWORD dwFileAttributes) -{ - FilePropertyFlags flags = 0; - if(dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - flags |= FilePropertyFlag_IsFolder; - } - return flags; -} - -internal void -os_w32_file_properties_from_attribute_data(FileProperties *properties, WIN32_FILE_ATTRIBUTE_DATA *attributes) -{ - properties->size = Compose64Bit(attributes->nFileSizeHigh, attributes->nFileSizeLow); - os_w32_dense_time_from_file_time(&properties->created, &attributes->ftCreationTime); - os_w32_dense_time_from_file_time(&properties->modified, &attributes->ftLastWriteTime); - properties->flags = os_w32_file_property_flags_from_dwFileAttributes(attributes->dwFileAttributes); -} - -//////////////////////////////// -//~ rjf: Time Conversion Helpers - -internal void -os_w32_date_time_from_system_time(DateTime *out, SYSTEMTIME *in) -{ - out->year = in->wYear; - out->mon = in->wMonth - 1; - out->wday = in->wDayOfWeek; - out->day = in->wDay; - out->hour = in->wHour; - out->min = in->wMinute; - out->sec = in->wSecond; - out->msec = in->wMilliseconds; -} - -internal void -os_w32_system_time_from_date_time(SYSTEMTIME *out, DateTime *in) -{ - out->wYear = (WORD)(in->year); - out->wMonth = in->mon + 1; - out->wDay = in->day; - out->wHour = in->hour; - out->wMinute = in->min; - out->wSecond = in->sec; - out->wMilliseconds = in->msec; -} - -internal void -os_w32_dense_time_from_file_time(DenseTime *out, FILETIME *in) -{ - SYSTEMTIME systime = {0}; - FileTimeToSystemTime(in, &systime); - DateTime date_time = {0}; - os_w32_date_time_from_system_time(&date_time, &systime); - *out = dense_time_from_date_time(date_time); -} - -internal U32 -os_w32_sleep_ms_from_endt_us(U64 endt_us) -{ - U32 sleep_ms = 0; - if(endt_us == max_U64) - { - sleep_ms = INFINITE; - } - else - { - U64 begint = os_now_microseconds(); - if(begint < endt_us) - { - U64 sleep_us = endt_us - begint; - sleep_ms = (U32)((sleep_us + 999)/1000); - } - } - return sleep_ms; -} - -internal U32 -os_w32_unix_time_from_file_time(FILETIME file_time) -{ - U64 win32_time = ((U64)file_time.dwHighDateTime << 32) | file_time.dwLowDateTime; - U64 unix_time64 = ((win32_time - 0x19DB1DED53E8000ULL) / 10000000); - - Assert(unix_time64 <= max_U32); - U32 unix_time32 = (U32)unix_time64; - - return unix_time32; -} - -//////////////////////////////// -//~ rjf: Entity Functions - -internal OS_W32_Entity * -os_w32_entity_alloc(OS_W32_EntityKind kind) -{ - OS_W32_Entity *result = 0; - EnterCriticalSection(&os_w32_state.entity_mutex); - { - result = os_w32_state.entity_free; - if(result) - { - SLLStackPop(os_w32_state.entity_free); - } - else - { - result = push_array_no_zero(os_w32_state.entity_arena, OS_W32_Entity, 1); - } - MemoryZeroStruct(result); - } - LeaveCriticalSection(&os_w32_state.entity_mutex); - result->kind = kind; - return result; -} - -internal void -os_w32_entity_release(OS_W32_Entity *entity) -{ - entity->kind = OS_W32_EntityKind_Null; - EnterCriticalSection(&os_w32_state.entity_mutex); - SLLStackPush(os_w32_state.entity_free, entity); - LeaveCriticalSection(&os_w32_state.entity_mutex); -} - -//////////////////////////////// -//~ rjf: Thread Entry Point - -internal DWORD -os_w32_thread_entry_point(void *ptr) -{ - OS_W32_Entity *entity = (OS_W32_Entity *)ptr; - OS_ThreadFunctionType *func = entity->thread.func; - void *thread_ptr = entity->thread.ptr; - TCTX tctx_; - tctx_init_and_equip(&tctx_); - func(thread_ptr); - tctx_release(); - return 0; -} - -//////////////////////////////// -//~ rjf: @os_hooks System/Process Info (Implemented Per-OS) - -internal OS_SystemInfo * -os_get_system_info(void) -{ - return &os_w32_state.system_info; -} - -internal OS_ProcessInfo * -os_get_process_info(void) -{ - return &os_w32_state.process_info; -} - -internal String8 -os_get_current_path(Arena *arena) -{ - Temp scratch = scratch_begin(&arena, 1); - DWORD length = GetCurrentDirectoryW(0, 0); - U16 *memory = push_array_no_zero(scratch.arena, U16, length + 1); - length = GetCurrentDirectoryW(length + 1, (WCHAR*)memory); - String8 name = str8_from_16(arena, str16(memory, length)); - scratch_end(scratch); - return name; -} - -internal U32 -os_get_process_start_time_unix(void) -{ - HANDLE handle = GetCurrentProcess(); - FILETIME start_time = {0}; - FILETIME exit_time; - FILETIME kernel_time; - FILETIME user_time; - if(GetProcessTimes(handle, &start_time, &exit_time, &kernel_time, &user_time)) - { - return os_w32_unix_time_from_file_time(start_time); - } - return 0; -} - -//////////////////////////////// -//~ rjf: @os_hooks Memory Allocation (Implemented Per-OS) - -//- rjf: basic - -internal void * -os_reserve(U64 size) -{ - void *result = VirtualAlloc(0, size, MEM_RESERVE, PAGE_READWRITE); - return result; -} - -internal B32 -os_commit(void *ptr, U64 size) -{ - B32 result = (VirtualAlloc(ptr, size, MEM_COMMIT, PAGE_READWRITE) != 0); - return result; -} - -internal void -os_decommit(void *ptr, U64 size) -{ - VirtualFree(ptr, size, MEM_DECOMMIT); -} - -internal void -os_release(void *ptr, U64 size) -{ - // NOTE(rjf): size not used - not necessary on Windows, but necessary for other OSes. - VirtualFree(ptr, 0, MEM_RELEASE); -} - -//- rjf: large pages - -internal void * -os_reserve_large(U64 size) -{ - // we commit on reserve because windows - void *result = VirtualAlloc(0, size, MEM_RESERVE|MEM_COMMIT|MEM_LARGE_PAGES, PAGE_READWRITE); - return result; -} - -internal B32 -os_commit_large(void *ptr, U64 size) -{ - return 1; -} - -//////////////////////////////// -//~ rjf: @os_hooks Thread Info (Implemented Per-OS) - -internal U32 -os_tid(void) -{ - DWORD id = GetCurrentThreadId(); - return (U32)id; -} - -internal void -os_set_thread_name(String8 name) -{ - Temp scratch = scratch_begin(0, 0); - - // rjf: windows 10 style - if(w32_SetThreadDescription_func) - { - String16 name16 = str16_from_8(scratch.arena, name); - HRESULT hr = w32_SetThreadDescription_func(GetCurrentThread(), (WCHAR*)name16.str); - } - - // rjf: raise-exception style - { - String8 name_copy = push_str8_copy(scratch.arena, name); -#pragma pack(push,8) - typedef struct THREADNAME_INFO THREADNAME_INFO; - struct THREADNAME_INFO - { - U32 dwType; // Must be 0x1000. - char *szName; // Pointer to name (in user addr space). - U32 dwThreadID; // Thread ID (-1=caller thread). - U32 dwFlags; // Reserved for future use, must be zero. - }; -#pragma pack(pop) - THREADNAME_INFO info; - info.dwType = 0x1000; - info.szName = (char *)name_copy.str; - info.dwThreadID = os_tid(); - info.dwFlags = 0; -#pragma warning(push) -#pragma warning(disable: 6320 6322) - __try - { - RaiseException(0x406D1388, 0, sizeof(info) / sizeof(void *), (const ULONG_PTR *)&info); - } - __except (EXCEPTION_EXECUTE_HANDLER) - { - } -#pragma warning(pop) - } - - scratch_end(scratch); -} - -//////////////////////////////// -//~ rjf: @os_hooks Aborting (Implemented Per-OS) - -internal void -os_abort(S32 exit_code) -{ - ExitProcess(exit_code); -} - -//////////////////////////////// -//~ rjf: @os_hooks File System (Implemented Per-OS) - -//- rjf: files - -internal OS_Handle -os_file_open(OS_AccessFlags flags, String8 path) -{ - OS_Handle result = {0}; - Temp scratch = scratch_begin(0, 0); - String16 path16 = str16_from_8(scratch.arena, path); - DWORD access_flags = 0; - DWORD share_mode = 0; - DWORD creation_disposition = OPEN_EXISTING; - SECURITY_ATTRIBUTES security_attributes = {sizeof(security_attributes), 0, 0}; - if(flags & OS_AccessFlag_Read) {access_flags |= GENERIC_READ;} - if(flags & OS_AccessFlag_Write) {access_flags |= GENERIC_WRITE;} - if(flags & OS_AccessFlag_Execute) {access_flags |= GENERIC_EXECUTE;} - if(flags & OS_AccessFlag_ShareRead) {share_mode |= FILE_SHARE_READ;} - if(flags & OS_AccessFlag_ShareWrite) {share_mode |= FILE_SHARE_WRITE|FILE_SHARE_DELETE;} - if(flags & OS_AccessFlag_Write) {creation_disposition = CREATE_ALWAYS;} - if(flags & OS_AccessFlag_Append) {creation_disposition = OPEN_ALWAYS; access_flags |= FILE_APPEND_DATA; } - if(flags & OS_AccessFlag_Inherited) - { - security_attributes.bInheritHandle = 1; - } - HANDLE file = CreateFileW((WCHAR *)path16.str, access_flags, share_mode, &security_attributes, creation_disposition, FILE_ATTRIBUTE_NORMAL, 0); - if(file != INVALID_HANDLE_VALUE) - { - result.u64[0] = (U64)file; - } - scratch_end(scratch); - return result; -} - -internal void -os_file_close(OS_Handle file) -{ - if(os_handle_match(file, os_handle_zero())) { return; } - HANDLE handle = (HANDLE)file.u64[0]; - BOOL result = CloseHandle(handle); - (void)result; -} - -internal U64 -os_file_read(OS_Handle file, Rng1U64 rng, void *out_data) -{ - if(os_handle_match(file, os_handle_zero())) { return 0; } - HANDLE handle = (HANDLE)file.u64[0]; - - // rjf: clamp range by file size - U64 size = 0; - GetFileSizeEx(handle, (LARGE_INTEGER *)&size); - Rng1U64 rng_clamped = r1u64(ClampTop(rng.min, size), ClampTop(rng.max, size)); - U64 total_read_size = 0; - - // rjf: read loop - { - U64 to_read = dim_1u64(rng_clamped); - for(U64 off = rng.min; total_read_size < to_read;) - { - U64 amt64 = to_read - total_read_size; - U32 amt32 = u32_from_u64_saturate(amt64); - DWORD read_size = 0; - OVERLAPPED overlapped = {0}; - overlapped.Offset = (off&0x00000000ffffffffull); - overlapped.OffsetHigh = (off&0xffffffff00000000ull) >> 32; - ReadFile(handle, (U8 *)out_data + total_read_size, amt32, &read_size, &overlapped); - off += read_size; - total_read_size += read_size; - if(read_size != amt32) - { - break; - } - } - } - - return total_read_size; -} - -internal U64 -os_file_write(OS_Handle file, Rng1U64 rng, void *data) -{ - if(os_handle_match(file, os_handle_zero())) { return 0; } - HANDLE win_handle = (HANDLE)file.u64[0]; - U64 src_off = 0; - U64 dst_off = rng.min; - U64 total_write_size = dim_1u64(rng); - for(;;) - { - void *bytes_src = (U8 *)data + src_off; - U64 bytes_left = total_write_size - src_off; - DWORD write_size = Min(MB(1), bytes_left); - DWORD bytes_written = 0; - OVERLAPPED overlapped = {0}; - overlapped.Offset = (dst_off&0x00000000ffffffffull); - overlapped.OffsetHigh = (dst_off&0xffffffff00000000ull) >> 32; - BOOL success = WriteFile(win_handle, bytes_src, write_size, &bytes_written, &overlapped); - if(success == 0) - { - break; - } - src_off += bytes_written; - dst_off += bytes_written; - if(bytes_left == 0) - { - break; - } - } - return src_off; -} - -internal B32 -os_file_set_time(OS_Handle file, DateTime time) -{ - if(os_handle_match(file, os_handle_zero())) { return 0; } - B32 result = 0; - HANDLE handle = (HANDLE)file.u64[0]; - SYSTEMTIME system_time = {0}; - os_w32_system_time_from_date_time(&system_time, &time); - FILETIME file_time = {0}; - result = (SystemTimeToFileTime(&system_time, &file_time) && - SetFileTime(handle, &file_time, &file_time, &file_time)); - return result; -} - -internal FileProperties -os_properties_from_file(OS_Handle file) -{ - if(os_handle_match(file, os_handle_zero())) { FileProperties r = {0}; return r; } - FileProperties props = {0}; - HANDLE handle = (HANDLE)file.u64[0]; - BY_HANDLE_FILE_INFORMATION info; - BOOL info_good = GetFileInformationByHandle(handle, &info); - if(info_good) - { - U32 size_lo = info.nFileSizeLow; - U32 size_hi = info.nFileSizeHigh; - props.size = (U64)size_lo | (((U64)size_hi)<<32); - os_w32_dense_time_from_file_time(&props.modified, &info.ftLastWriteTime); - os_w32_dense_time_from_file_time(&props.created, &info.ftCreationTime); - props.flags = os_w32_file_property_flags_from_dwFileAttributes(info.dwFileAttributes); - } - return props; -} - -internal OS_FileID -os_id_from_file(OS_Handle file) -{ - if(os_handle_match(file, os_handle_zero())) { OS_FileID r = {0}; return r; } - OS_FileID result = {0}; - HANDLE handle = (HANDLE)file.u64[0]; - BY_HANDLE_FILE_INFORMATION info; - BOOL is_ok = GetFileInformationByHandle(handle, &info); - if(is_ok) - { - result.v[0] = info.dwVolumeSerialNumber; - result.v[1] = info.nFileIndexLow; - result.v[2] = info.nFileIndexHigh; - } - return result; -} - -internal B32 -os_delete_file_at_path(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - String16 path16 = str16_from_8(scratch.arena, path); - B32 result = DeleteFileW((WCHAR*)path16.str); - scratch_end(scratch); - return result; -} - -internal B32 -os_copy_file_path(String8 dst, String8 src) -{ - Temp scratch = scratch_begin(0, 0); - String16 dst16 = str16_from_8(scratch.arena, dst); - String16 src16 = str16_from_8(scratch.arena, src); - B32 result = CopyFileW((WCHAR*)src16.str, (WCHAR*)dst16.str, 0); - scratch_end(scratch); - return result; -} - -internal String8 -os_full_path_from_path(Arena *arena, String8 path) -{ - Temp scratch = scratch_begin(&arena, 1); - DWORD buffer_size = Max(MAX_PATH, path.size * 2) + 1; - String16 path16 = str16_from_8(scratch.arena, path); - WCHAR *buffer = push_array_no_zero(scratch.arena, WCHAR, buffer_size); - DWORD path16_size = GetFullPathNameW((WCHAR*)path16.str, buffer_size, buffer, NULL); - if(path16_size > buffer_size) - { - arena_pop(scratch.arena, buffer_size); - buffer_size = path16_size + 1; - buffer = push_array_no_zero(scratch.arena, WCHAR, buffer_size); - path16_size = GetFullPathNameW((WCHAR*)path16.str, buffer_size, buffer, NULL); - } - String8 full_path = str8_from_16(arena, str16((U16*)buffer, path16_size)); - scratch_end(scratch); - return full_path; -} - -internal B32 -os_file_path_exists(String8 path) -{ - Temp scratch = scratch_begin(0,0); - String16 path16 = str16_from_8(scratch.arena, path); - DWORD attributes = GetFileAttributesW((WCHAR *)path16.str); - B32 exists = (attributes != INVALID_FILE_ATTRIBUTES) && !!(~attributes & FILE_ATTRIBUTE_DIRECTORY); - scratch_end(scratch); - return exists; -} - -internal B32 -os_folder_path_exists(String8 path) -{ - Temp scratch = scratch_begin(0,0); - String16 path16 = str16_from_8(scratch.arena, path); - DWORD attributes = GetFileAttributesW((WCHAR *)path16.str); - B32 exists = (attributes != INVALID_FILE_ATTRIBUTES) && (attributes & FILE_ATTRIBUTE_DIRECTORY); - scratch_end(scratch); - return exists; -} - -internal FileProperties -os_properties_from_file_path(String8 path) -{ - WIN32_FIND_DATAW find_data = {0}; - Temp scratch = scratch_begin(0, 0); - String16 path16 = str16_from_8(scratch.arena, path); - HANDLE handle = FindFirstFileW((WCHAR *)path16.str, &find_data); - FileProperties props = {0}; - if(handle != INVALID_HANDLE_VALUE) - { - props.size = Compose64Bit(find_data.nFileSizeHigh, find_data.nFileSizeLow); - os_w32_dense_time_from_file_time(&props.created, &find_data.ftCreationTime); - os_w32_dense_time_from_file_time(&props.modified, &find_data.ftLastWriteTime); - props.flags = os_w32_file_property_flags_from_dwFileAttributes(find_data.dwFileAttributes); - } - FindClose(handle); - scratch_end(scratch); - return props; -} - -//- rjf: file maps - -internal OS_Handle -os_file_map_open(OS_AccessFlags flags, OS_Handle file) -{ - OS_Handle map = {0}; - { - HANDLE file_handle = (HANDLE)file.u64[0]; - DWORD protect_flags = 0; - { - switch(flags) - { - default:{}break; - case OS_AccessFlag_Read: - {protect_flags = PAGE_READONLY;}break; - case OS_AccessFlag_Write: - case OS_AccessFlag_Read|OS_AccessFlag_Write: - {protect_flags = PAGE_READWRITE;}break; - case OS_AccessFlag_Execute: - case OS_AccessFlag_Read|OS_AccessFlag_Execute: - {protect_flags = PAGE_EXECUTE_READ;}break; - case OS_AccessFlag_Execute|OS_AccessFlag_Write|OS_AccessFlag_Read: - case OS_AccessFlag_Execute|OS_AccessFlag_Write: - {protect_flags = PAGE_EXECUTE_READWRITE;}break; - } - } - HANDLE map_handle = CreateFileMappingA(file_handle, 0, protect_flags, 0, 0, 0); - map.u64[0] = (U64)map_handle; - } - return map; -} - -internal void -os_file_map_close(OS_Handle map) -{ - HANDLE handle = (HANDLE)map.u64[0]; - BOOL result = CloseHandle(handle); - (void)result; -} - -internal void * -os_file_map_view_open(OS_Handle map, OS_AccessFlags flags, Rng1U64 range) -{ - HANDLE handle = (HANDLE)map.u64[0]; - U32 off_lo = (U32)((range.min&0x00000000ffffffffull)>>0); - U32 off_hi = (U32)((range.min&0xffffffff00000000ull)>>32); - U64 size = dim_1u64(range); - DWORD access_flags = 0; - { - switch(flags) - { - default:{}break; - case OS_AccessFlag_Read: - { - access_flags = FILE_MAP_READ; - }break; - case OS_AccessFlag_Write: - { - access_flags = FILE_MAP_WRITE; - }break; - case OS_AccessFlag_Read|OS_AccessFlag_Write: - { - access_flags = FILE_MAP_ALL_ACCESS; - }break; - case OS_AccessFlag_Execute: - case OS_AccessFlag_Read|OS_AccessFlag_Execute: - case OS_AccessFlag_Write|OS_AccessFlag_Execute: - case OS_AccessFlag_Read|OS_AccessFlag_Write|OS_AccessFlag_Execute: - { - access_flags = FILE_MAP_ALL_ACCESS|FILE_MAP_EXECUTE; - }break; - } - } - void *result = MapViewOfFile(handle, access_flags, off_hi, off_lo, size); - return result; -} - -internal void -os_file_map_view_close(OS_Handle map, void *ptr, Rng1U64 range) -{ - BOOL result = UnmapViewOfFile(ptr); - (void)result; -} - -//- rjf: directory iteration - -internal OS_FileIter * -os_file_iter_begin(Arena *arena, String8 path, OS_FileIterFlags flags) -{ - Temp scratch = scratch_begin(&arena, 1); - String8 path_with_wildcard = push_str8_cat(scratch.arena, path, str8_lit("\\*")); - String16 path16 = str16_from_8(scratch.arena, path_with_wildcard); - OS_FileIter *iter = push_array(arena, OS_FileIter, 1); - iter->flags = flags; - OS_W32_FileIter *w32_iter = (OS_W32_FileIter*)iter->memory; - if(path.size == 0) - { - w32_iter->is_volume_iter = 1; - WCHAR buffer[512] = {0}; - DWORD length = GetLogicalDriveStringsW(sizeof(buffer), buffer); - String8List drive_strings = {0}; - for(U64 off = 0; off < (U64)length;) - { - String16 next_drive_string_16 = str16_cstring((U16 *)buffer+off); - off += next_drive_string_16.size+1; - String8 next_drive_string = str8_from_16(arena, next_drive_string_16); - next_drive_string = str8_chop_last_slash(next_drive_string); - str8_list_push(scratch.arena, &drive_strings, next_drive_string); - } - w32_iter->drive_strings = str8_array_from_list(arena, &drive_strings); - w32_iter->drive_strings_iter_idx = 0; - } - else - { - w32_iter->handle = FindFirstFileW((WCHAR*)path16.str, &w32_iter->find_data); - } - scratch_end(scratch); - return iter; -} - -internal B32 -os_file_iter_next(Arena *arena, OS_FileIter *iter, OS_FileInfo *info_out) -{ - B32 result = 0; - OS_FileIterFlags flags = iter->flags; - OS_W32_FileIter *w32_iter = (OS_W32_FileIter*)iter->memory; - switch(w32_iter->is_volume_iter) - { - //- rjf: file iteration - default: - case 0: - { - if (!(flags & OS_FileIterFlag_Done) && w32_iter->handle != INVALID_HANDLE_VALUE) - { - do - { - // check is usable - B32 usable_file = 1; - - WCHAR *file_name = w32_iter->find_data.cFileName; - DWORD attributes = w32_iter->find_data.dwFileAttributes; - if (file_name[0] == '.'){ - if (flags & OS_FileIterFlag_SkipHiddenFiles){ - usable_file = 0; - } - else if (file_name[1] == 0){ - usable_file = 0; - } - else if (file_name[1] == '.' && file_name[2] == 0){ - usable_file = 0; - } - } - if (attributes & FILE_ATTRIBUTE_DIRECTORY){ - if (flags & OS_FileIterFlag_SkipFolders){ - usable_file = 0; - } - } - else{ - if (flags & OS_FileIterFlag_SkipFiles){ - usable_file = 0; - } - } - - // emit if usable - if (usable_file){ - info_out->name = str8_from_16(arena, str16_cstring((U16*)file_name)); - info_out->props.size = (U64)w32_iter->find_data.nFileSizeLow | (((U64)w32_iter->find_data.nFileSizeHigh)<<32); - os_w32_dense_time_from_file_time(&info_out->props.created, &w32_iter->find_data.ftCreationTime); - os_w32_dense_time_from_file_time(&info_out->props.modified, &w32_iter->find_data.ftLastWriteTime); - info_out->props.flags = os_w32_file_property_flags_from_dwFileAttributes(attributes); - result = 1; - if (!FindNextFileW(w32_iter->handle, &w32_iter->find_data)){ - iter->flags |= OS_FileIterFlag_Done; - } - break; - } - }while(FindNextFileW(w32_iter->handle, &w32_iter->find_data)); - } - }break; - - //- rjf: volume iteration - case 1: - { - result = w32_iter->drive_strings_iter_idx < w32_iter->drive_strings.count; - if(result != 0) - { - MemoryZeroStruct(info_out); - info_out->name = w32_iter->drive_strings.v[w32_iter->drive_strings_iter_idx]; - info_out->props.flags |= FilePropertyFlag_IsFolder; - w32_iter->drive_strings_iter_idx += 1; - } - }break; - } - if(!result) - { - iter->flags |= OS_FileIterFlag_Done; - } - return result; -} - -internal void -os_file_iter_end(OS_FileIter *iter) -{ - OS_W32_FileIter *w32_iter = (OS_W32_FileIter*)iter->memory; - HANDLE zero_handle; - MemoryZeroStruct(&zero_handle); - if(!MemoryMatchStruct(&zero_handle, &w32_iter->handle)) - { - FindClose(w32_iter->handle); - } -} - -//- rjf: directory creation - -internal B32 -os_make_directory(String8 path) -{ - B32 result = 0; - Temp scratch = scratch_begin(0, 0); - String16 name16 = str16_from_8(scratch.arena, path); - WIN32_FILE_ATTRIBUTE_DATA attributes = {0}; - GetFileAttributesExW((WCHAR*)name16.str, GetFileExInfoStandard, &attributes); - if(attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - result = 1; - } - else if(CreateDirectoryW((WCHAR*)name16.str, 0)) - { - result = 1; - } - scratch_end(scratch); - return(result); -} - -//////////////////////////////// -//~ rjf: @os_hooks Shared Memory (Implemented Per-OS) - -internal OS_Handle -os_shared_memory_alloc(U64 size, String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String16 name16 = str16_from_8(scratch.arena, name); - HANDLE file = CreateFileMappingW(INVALID_HANDLE_VALUE, - 0, - PAGE_READWRITE, - (U32)((size & 0xffffffff00000000) >> 32), - (U32)((size & 0x00000000ffffffff)), - (WCHAR *)name16.str); - OS_Handle result = {(U64)file}; - scratch_end(scratch); - return result; -} - -internal OS_Handle -os_shared_memory_open(String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String16 name16 = str16_from_8(scratch.arena, name); - HANDLE file = OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, (WCHAR *)name16.str); - OS_Handle result = {(U64)file}; - scratch_end(scratch); - return result; -} - -internal void -os_shared_memory_close(OS_Handle handle) -{ - HANDLE file = (HANDLE)(handle.u64[0]); - CloseHandle(file); -} - -internal void * -os_shared_memory_view_open(OS_Handle handle, Rng1U64 range) -{ - HANDLE file = (HANDLE)(handle.u64[0]); - U64 offset = range.min; - U64 size = range.max-range.min; - void *ptr = MapViewOfFile(file, FILE_MAP_ALL_ACCESS, - (U32)((offset & 0xffffffff00000000) >> 32), - (U32)((offset & 0x00000000ffffffff)), - size); - return ptr; -} - -internal void -os_shared_memory_view_close(OS_Handle handle, void *ptr, Rng1U64 range) -{ - UnmapViewOfFile(ptr); -} - -//////////////////////////////// -//~ rjf: @os_hooks Time (Implemented Per-OS) - -internal U64 -os_now_microseconds(void) -{ - U64 result = 0; - LARGE_INTEGER large_int_counter; - if(QueryPerformanceCounter(&large_int_counter)) - { - result = (large_int_counter.QuadPart*Million(1))/os_w32_state.microsecond_resolution; - } - return result; -} - -internal U32 -os_now_unix(void) -{ - FILETIME file_time; - GetSystemTimeAsFileTime(&file_time); - U32 unix_time = os_w32_unix_time_from_file_time(file_time); - return unix_time; -} - -internal DateTime -os_now_universal_time(void) -{ - SYSTEMTIME systime = {0}; - GetSystemTime(&systime); - DateTime result = {0}; - os_w32_date_time_from_system_time(&result, &systime); - return result; -} - -internal DateTime -os_universal_time_from_local(DateTime *date_time) -{ - SYSTEMTIME systime = {0}; - os_w32_system_time_from_date_time(&systime, date_time); - FILETIME ftime = {0}; - SystemTimeToFileTime(&systime, &ftime); - FILETIME ftime_local = {0}; - LocalFileTimeToFileTime(&ftime, &ftime_local); - FileTimeToSystemTime(&ftime_local, &systime); - DateTime result = {0}; - os_w32_date_time_from_system_time(&result, &systime); - return result; -} - -internal DateTime -os_local_time_from_universal(DateTime *date_time) -{ - SYSTEMTIME systime = {0}; - os_w32_system_time_from_date_time(&systime, date_time); - FILETIME ftime = {0}; - SystemTimeToFileTime(&systime, &ftime); - FILETIME ftime_local = {0}; - FileTimeToLocalFileTime(&ftime, &ftime_local); - FileTimeToSystemTime(&ftime_local, &systime); - DateTime result = {0}; - os_w32_date_time_from_system_time(&result, &systime); - return result; -} - -internal void -os_sleep_milliseconds(U32 msec) -{ - Sleep(msec); -} - -//////////////////////////////// -//~ rjf: @os_hooks Child Processes (Implemented Per-OS) - -internal OS_Handle -os_process_launch(OS_ProcessLaunchParams *params) -{ - OS_Handle result = {0}; - Temp scratch = scratch_begin(0, 0); - - //- rjf: form full command string - String8 cmd = {0}; - { - StringJoin join_params = {0}; - join_params.pre = str8_lit("\""); - join_params.sep = str8_lit("\" \""); - join_params.post = str8_lit("\""); - cmd = str8_list_join(scratch.arena, ¶ms->cmd_line, &join_params); - } - - //- rjf: form environment - B32 use_null_env_arg = 0; - String8 env = {0}; - { - StringJoin join_params2 = {0}; - join_params2.sep = str8_lit("\0"); - join_params2.post = str8_lit("\0"); - String8List all_opts = params->env; - if(params->inherit_env != 0) - { - if(all_opts.node_count != 0) - { - MemoryZeroStruct(&all_opts); - for(String8Node *n = params->env.first; n != 0; n = n->next) - { - str8_list_push(scratch.arena, &all_opts, n->string); - } - for(String8Node *n = os_w32_state.process_info.environment.first; n != 0; n = n->next) - { - str8_list_push(scratch.arena, &all_opts, n->string); - } - } - else - { - use_null_env_arg = 1; - } - } - if(use_null_env_arg == 0) - { - env = str8_list_join(scratch.arena, &all_opts, &join_params2); - } - } - - //- rjf: utf-8 -> utf-16 - String16 cmd16 = str16_from_8(scratch.arena, cmd); - String16 dir16 = str16_from_8(scratch.arena, params->path); - String16 env16 = {0}; - if(use_null_env_arg == 0) - { - env16 = str16_from_8(scratch.arena, env); - } - - //- rjf: determine creation flags - DWORD creation_flags = CREATE_UNICODE_ENVIRONMENT; - if(params->consoleless) - { - creation_flags |= CREATE_NO_WINDOW; - } - - //- rjf: launch - BOOL inherit_handles = 0; - STARTUPINFOW startup_info = {sizeof(startup_info)}; - if(!os_handle_match(params->stdout_file, os_handle_zero())) - { - HANDLE stdout_handle = (HANDLE)params->stdout_file.u64[0]; - startup_info.hStdOutput = stdout_handle; - startup_info.dwFlags |= STARTF_USESTDHANDLES; - inherit_handles = 1; - } - if(!os_handle_match(params->stderr_file, os_handle_zero())) - { - HANDLE stderr_handle = (HANDLE)params->stderr_file.u64[0]; - startup_info.hStdError = stderr_handle; - startup_info.dwFlags |= STARTF_USESTDHANDLES; - inherit_handles = 1; - } - if(!os_handle_match(params->stdin_file, os_handle_zero())) - { - HANDLE stdin_handle = (HANDLE)params->stdin_file.u64[0]; - startup_info.hStdInput = stdin_handle; - startup_info.dwFlags |= STARTF_USESTDHANDLES; - inherit_handles = 1; - } - PROCESS_INFORMATION process_info = {0}; - if(CreateProcessW(0, (WCHAR*)cmd16.str, 0, 0, inherit_handles, creation_flags, use_null_env_arg ? 0 : (WCHAR*)env16.str, (WCHAR*)dir16.str, &startup_info, &process_info)) - { - result.u64[0] = (U64)process_info.hProcess; - CloseHandle(process_info.hThread); - } - - scratch_end(scratch); - return result; -} - -internal B32 -os_process_join(OS_Handle handle, U64 endt_us) -{ - HANDLE process = (HANDLE)(handle.u64[0]); - DWORD sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); - DWORD result = WaitForSingleObject(process, sleep_ms); - return (result == WAIT_OBJECT_0); -} - -internal void -os_process_detach(OS_Handle handle) -{ - HANDLE process = (HANDLE)(handle.u64[0]); - CloseHandle(process); -} - -//////////////////////////////// -//~ rjf: @os_hooks Threads (Implemented Per-OS) - -internal OS_Handle -os_thread_launch(OS_ThreadFunctionType *func, void *ptr, void *params) -{ - OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_Thread); - entity->thread.func = func; - entity->thread.ptr = ptr; - entity->thread.handle = CreateThread(0, 0, os_w32_thread_entry_point, entity, 0, &entity->thread.tid); - OS_Handle result = {IntFromPtr(entity)}; - return result; -} - -internal B32 -os_thread_join(OS_Handle handle, U64 endt_us) -{ - DWORD sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); - OS_W32_Entity *entity = (OS_W32_Entity *)PtrFromInt(handle.u64[0]); - DWORD wait_result = WAIT_OBJECT_0; - if(entity != 0) - { - wait_result = WaitForSingleObject(entity->thread.handle, sleep_ms); - CloseHandle(entity->thread.handle); - os_w32_entity_release(entity); - } - return (wait_result == WAIT_OBJECT_0); -} - -internal void -os_thread_detach(OS_Handle thread) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(thread.u64[0]); - if(entity != 0) - { - CloseHandle(entity->thread.handle); - os_w32_entity_release(entity); - } -} - -//////////////////////////////// -//~ rjf: @os_hooks Synchronization Primitives (Implemented Per-OS) - -//- rjf: mutexes - -internal OS_Handle -os_mutex_alloc(void) -{ - OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_Mutex); - InitializeCriticalSection(&entity->mutex); - OS_Handle result = {IntFromPtr(entity)}; - return result; -} - -internal void -os_mutex_release(OS_Handle mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); - os_w32_entity_release(entity); -} - -internal void -os_mutex_take(OS_Handle mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); - EnterCriticalSection(&entity->mutex); -} - -internal void -os_mutex_drop(OS_Handle mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); - LeaveCriticalSection(&entity->mutex); -} - -//- rjf: reader/writer mutexes - -internal OS_Handle -os_rw_mutex_alloc(void) -{ - OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_RWMutex); - InitializeSRWLock(&entity->rw_mutex); - OS_Handle result = {IntFromPtr(entity)}; - return result; -} - -internal void -os_rw_mutex_release(OS_Handle rw_mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); - os_w32_entity_release(entity); -} - -internal void -os_rw_mutex_take_r(OS_Handle rw_mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); - AcquireSRWLockShared(&entity->rw_mutex); -} - -internal void -os_rw_mutex_drop_r(OS_Handle rw_mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); - ReleaseSRWLockShared(&entity->rw_mutex); -} - -internal void -os_rw_mutex_take_w(OS_Handle rw_mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); - AcquireSRWLockExclusive(&entity->rw_mutex); -} - -internal void -os_rw_mutex_drop_w(OS_Handle rw_mutex) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); - ReleaseSRWLockExclusive(&entity->rw_mutex); -} - -//- rjf: condition variables - -internal OS_Handle -os_condition_variable_alloc(void) -{ - OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_ConditionVariable); - InitializeConditionVariable(&entity->cv); - OS_Handle result = {IntFromPtr(entity)}; - return result; -} - -internal void -os_condition_variable_release(OS_Handle cv) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); - os_w32_entity_release(entity); -} - -internal B32 -os_condition_variable_wait(OS_Handle cv, OS_Handle mutex, U64 endt_us) -{ - U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); - BOOL result = 0; - if(sleep_ms > 0) - { - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); - OS_W32_Entity *mutex_entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); - result = SleepConditionVariableCS(&entity->cv, &mutex_entity->mutex, sleep_ms); - } - return result; -} - -internal B32 -os_condition_variable_wait_rw_r(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) -{ - U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); - BOOL result = 0; - if(sleep_ms > 0) - { - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); - OS_W32_Entity *mutex_entity = (OS_W32_Entity*)PtrFromInt(mutex_rw.u64[0]); - result = SleepConditionVariableSRW(&entity->cv, &mutex_entity->rw_mutex, sleep_ms, - CONDITION_VARIABLE_LOCKMODE_SHARED); - } - return result; -} - -internal B32 -os_condition_variable_wait_rw_w(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) -{ - U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); - BOOL result = 0; - if(sleep_ms > 0) - { - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); - OS_W32_Entity *mutex_entity = (OS_W32_Entity*)PtrFromInt(mutex_rw.u64[0]); - result = SleepConditionVariableSRW(&entity->cv, &mutex_entity->rw_mutex, sleep_ms, 0); - } - return result; -} - -internal void -os_condition_variable_signal(OS_Handle cv) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); - WakeConditionVariable(&entity->cv); -} - -internal void -os_condition_variable_broadcast(OS_Handle cv) -{ - OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); - WakeAllConditionVariable(&entity->cv); -} - -//- rjf: cross-process semaphores - -internal OS_Handle -os_semaphore_alloc(U32 initial_count, U32 max_count, String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String16 name16 = str16_from_8(scratch.arena, name); - HANDLE handle = CreateSemaphoreW(0, initial_count, max_count, (WCHAR *)name16.str); - OS_Handle result = {(U64)handle}; - scratch_end(scratch); - return result; -} - -internal void -os_semaphore_release(OS_Handle semaphore) -{ - HANDLE handle = (HANDLE)semaphore.u64[0]; - CloseHandle(handle); -} - -internal OS_Handle -os_semaphore_open(String8 name) -{ - Temp scratch = scratch_begin(0, 0); - String16 name16 = str16_from_8(scratch.arena, name); - HANDLE handle = OpenSemaphoreW(SEMAPHORE_ALL_ACCESS , 0, (WCHAR *)name16.str); - OS_Handle result = {(U64)handle}; - scratch_end(scratch); - return result; -} - -internal void -os_semaphore_close(OS_Handle semaphore) -{ - HANDLE handle = (HANDLE)semaphore.u64[0]; - CloseHandle(handle); -} - -internal B32 -os_semaphore_take(OS_Handle semaphore, U64 endt_us) -{ - U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); - HANDLE handle = (HANDLE)semaphore.u64[0]; - DWORD wait_result = WaitForSingleObject(handle, sleep_ms); - B32 result = (wait_result == WAIT_OBJECT_0); - return result; -} - -internal void -os_semaphore_drop(OS_Handle semaphore) -{ - HANDLE handle = (HANDLE)semaphore.u64[0]; - ReleaseSemaphore(handle, 1, 0); -} - -//////////////////////////////// -//~ rjf: @os_hooks Dynamically-Loaded Libraries (Implemented Per-OS) - -internal OS_Handle -os_library_open(String8 path) -{ - Temp scratch = scratch_begin(0, 0); - String16 path16 = str16_from_8(scratch.arena, path); - HMODULE mod = LoadLibraryW((LPCWSTR)path16.str); - OS_Handle result = { (U64)mod }; - scratch_end(scratch); - return result; -} - -internal VoidProc* -os_library_load_proc(OS_Handle lib, String8 name) -{ - Temp scratch = scratch_begin(0, 0); - HMODULE mod = (HMODULE)lib.u64[0]; - name = push_str8_copy(scratch.arena, name); - VoidProc *result = (VoidProc*)GetProcAddress(mod, (LPCSTR)name.str); - scratch_end(scratch); - return result; -} - -internal void -os_library_close(OS_Handle lib) -{ - HMODULE mod = (HMODULE)lib.u64[0]; - FreeLibrary(mod); -} - -//////////////////////////////// -//~ rjf: @os_hooks Safe Calls (Implemented Per-OS) - -internal void -os_safe_call(OS_ThreadFunctionType *func, OS_ThreadFunctionType *fail_handler, void *ptr) -{ - __try - { - func(ptr); - } - __except (EXCEPTION_EXECUTE_HANDLER) - { - if(fail_handler != 0) - { - fail_handler(ptr); - } - ExitProcess(1); - } -} - -//////////////////////////////// -//~ rjf: @os_hooks GUIDs (Implemented Per-OS) - -internal Guid -os_make_guid(void) -{ - Guid result; MemoryZeroStruct(&result); - UUID uuid; - RPC_STATUS rpc_status = UuidCreate(&uuid); - if(rpc_status == RPC_S_OK) - { - result.data1 = uuid.Data1; - result.data2 = uuid.Data2; - result.data3 = uuid.Data3; - MemoryCopyArray(result.data4, uuid.Data4); - } - return result; -} - -//////////////////////////////// -//~ rjf: @os_hooks Entry Points (Implemented Per-OS) - -#include -#undef OS_WINDOWS // shlwapi uses its own OS_WINDOWS include inside -#include - -internal B32 win32_g_is_quiet = 0; - -internal HRESULT WINAPI -win32_dialog_callback(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, LONG_PTR data) -{ - if(msg == TDN_HYPERLINK_CLICKED) - { - ShellExecuteW(NULL, L"open", (LPWSTR)lparam, NULL, NULL, SW_SHOWNORMAL); - } - return S_OK; -} - -internal LONG WINAPI -win32_exception_filter(EXCEPTION_POINTERS* exception_ptrs) -{ - if(win32_g_is_quiet) - { - ExitProcess(1); - } - - static volatile LONG first = 0; - if(InterlockedCompareExchange(&first, 1, 0) != 0) - { - // prevent failures in other threads to popup same message box - // this handler just shows first thread that crashes - // we are terminating afterwards anyway - for (;;) Sleep(1000); - } - - WCHAR buffer[4096] = {0}; - int buflen = 0; - - DWORD exception_code = exception_ptrs->ExceptionRecord->ExceptionCode; - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"A fatal exception (code 0x%x) occurred. The process is terminating.\n", exception_code); - - // load dbghelp dynamically just in case if it is missing - HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); - if(dbghelp) - { - DWORD (WINAPI *dbg_SymSetOptions)(DWORD SymOptions); - BOOL (WINAPI *dbg_SymInitializeW)(HANDLE hProcess, PCWSTR UserSearchPath, BOOL fInvadeProcess); - BOOL (WINAPI *dbg_StackWalk64)(DWORD MachineType, HANDLE hProcess, HANDLE hThread, - LPSTACKFRAME64 StackFrame, PVOID ContextRecord, PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, - PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, - PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress); - PVOID (WINAPI *dbg_SymFunctionTableAccess64)(HANDLE hProcess, DWORD64 AddrBase); - DWORD64 (WINAPI *dbg_SymGetModuleBase64)(HANDLE hProcess, DWORD64 qwAddr); - BOOL (WINAPI *dbg_SymFromAddrW)(HANDLE hProcess, DWORD64 Address, PDWORD64 Displacement, PSYMBOL_INFOW Symbol); - BOOL (WINAPI *dbg_SymGetLineFromAddrW64)(HANDLE hProcess, DWORD64 dwAddr, PDWORD pdwDisplacement, PIMAGEHLP_LINEW64 Line); - BOOL (WINAPI *dbg_SymGetModuleInfoW64)(HANDLE hProcess, DWORD64 qwAddr, PIMAGEHLP_MODULEW64 ModuleInfo); - - *(FARPROC*)&dbg_SymSetOptions = GetProcAddress(dbghelp, "SymSetOptions"); - *(FARPROC*)&dbg_SymInitializeW = GetProcAddress(dbghelp, "SymInitializeW"); - *(FARPROC*)&dbg_StackWalk64 = GetProcAddress(dbghelp, "StackWalk64"); - *(FARPROC*)&dbg_SymFunctionTableAccess64 = GetProcAddress(dbghelp, "SymFunctionTableAccess64"); - *(FARPROC*)&dbg_SymGetModuleBase64 = GetProcAddress(dbghelp, "SymGetModuleBase64"); - *(FARPROC*)&dbg_SymFromAddrW = GetProcAddress(dbghelp, "SymFromAddrW"); - *(FARPROC*)&dbg_SymGetLineFromAddrW64 = GetProcAddress(dbghelp, "SymGetLineFromAddrW64"); - *(FARPROC*)&dbg_SymGetModuleInfoW64 = GetProcAddress(dbghelp, "SymGetModuleInfoW64"); - - if(dbg_SymSetOptions && dbg_SymInitializeW && dbg_StackWalk64 && dbg_SymFunctionTableAccess64 && dbg_SymGetModuleBase64 && dbg_SymFromAddrW && dbg_SymGetLineFromAddrW64 && dbg_SymGetModuleInfoW64) - { - HANDLE process = GetCurrentProcess(); - HANDLE thread = GetCurrentThread(); - CONTEXT* context = exception_ptrs->ContextRecord; - - WCHAR module_path[MAX_PATH]; - GetModuleFileNameW(NULL, module_path, ArrayCount(module_path)); - PathRemoveFileSpecW(module_path); - - dbg_SymSetOptions(SYMOPT_EXACT_SYMBOLS | SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT_LOAD_LINES | SYMOPT_UNDNAME); - if(dbg_SymInitializeW(process, module_path, TRUE)) - { - // check that raddbg.pdb file is good - B32 raddbg_pdb_valid = 0; - { - IMAGEHLP_MODULEW64 module = {0}; - module.SizeOfStruct = sizeof(module); - if(dbg_SymGetModuleInfoW64(process, (DWORD64)&win32_exception_filter, &module)) - { - raddbg_pdb_valid = (module.SymType == SymPdb); - } - } - - if(!raddbg_pdb_valid) - { - buflen += wnsprintfW(buffer + buflen, sizeof(buffer) - buflen, - L"\nThe PDB debug information file for this executable is not valid or was not found. Please rebuild binary to get the call stack.\n"); - } - else - { - STACKFRAME64 frame = {0}; - DWORD image_type; -#if defined(_M_AMD64) - image_type = IMAGE_FILE_MACHINE_AMD64; - frame.AddrPC.Offset = context->Rip; - frame.AddrPC.Mode = AddrModeFlat; - frame.AddrFrame.Offset = context->Rbp; - frame.AddrFrame.Mode = AddrModeFlat; - frame.AddrStack.Offset = context->Rsp; - frame.AddrStack.Mode = AddrModeFlat; -#elif defined(_M_ARM64) - image_type = IMAGE_FILE_MACHINE_ARM64; - frame.AddrPC.Offset = context->Pc; - frame.AddrPC.Mode = AddrModeFlat; - frame.AddrFrame.Offset = context->Fp; - frame.AddrFrame.Mode = AddrModeFlat; - frame.AddrStack.Offset = context->Sp; - frame.AddrStack.Mode = AddrModeFlat; -#else -# error Arch not supported! -#endif - - for(U32 idx=0; ;idx++) - { - const U32 max_frames = 32; - if(idx == max_frames) - { - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"..."); - break; - } - - if(!dbg_StackWalk64(image_type, process, thread, &frame, context, 0, dbg_SymFunctionTableAccess64, dbg_SymGetModuleBase64, 0)) - { - break; - } - - U64 address = frame.AddrPC.Offset; - if(address == 0) - { - break; - } - - if(idx==0) - { -#if BUILD_CONSOLE_INTERFACE - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"\nCreate a new issue with this report at %S.\n\n", BUILD_ISSUES_LINK_STRING_LITERAL); -#else - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, - L"\nPress Ctrl+C to copy this text to clipboard, then create a new issue at\n" - L"%S\n\n", BUILD_ISSUES_LINK_STRING_LITERAL, BUILD_ISSUES_LINK_STRING_LITERAL); -#endif - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"Call stack:\n"); - } - - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"%u. [0x%I64x]", idx + 1, address); - - struct { - SYMBOL_INFOW info; - WCHAR name[MAX_SYM_NAME]; - } symbol = {0}; - - symbol.info.SizeOfStruct = sizeof(symbol.info); - symbol.info.MaxNameLen = MAX_SYM_NAME; - - DWORD64 displacement = 0; - if(dbg_SymFromAddrW(process, address, &displacement, &symbol.info)) - { - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L" %s +%u", symbol.info.Name, (DWORD)displacement); - - IMAGEHLP_LINEW64 line = {0}; - line.SizeOfStruct = sizeof(line); - - DWORD line_displacement = 0; - if(dbg_SymGetLineFromAddrW64(process, address, &line_displacement, &line)) - { - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L", %s line %u", PathFindFileNameW(line.FileName), line.LineNumber); - } - } - else - { - IMAGEHLP_MODULEW64 module = {0}; - module.SizeOfStruct = sizeof(module); - if(dbg_SymGetModuleInfoW64(process, address, &module)) - { - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L" %s", module.ModuleName); - } - } - - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"\n"); - } - } - } - } - } - - buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"\nVersion: %S%S", BUILD_VERSION_STRING_LITERAL, BUILD_GIT_HASH_STRING_LITERAL_APPEND); - -#if BUILD_CONSOLE_INTERFACE - fwprintf(stderr, L"\n--- Fatal Exception ---\n"); - fwprintf(stderr, L"%s\n\n", buffer); -#else - TASKDIALOGCONFIG dialog = {0}; - dialog.cbSize = sizeof(dialog); - dialog.dwFlags = TDF_SIZE_TO_CONTENT | TDF_ENABLE_HYPERLINKS | TDF_ALLOW_DIALOG_CANCELLATION; - dialog.pszMainIcon = TD_ERROR_ICON; - dialog.dwCommonButtons = TDCBF_CLOSE_BUTTON; - dialog.pszWindowTitle = L"Fatal Exception"; - dialog.pszContent = buffer; - dialog.pfCallback = &win32_dialog_callback; - TaskDialogIndirect(&dialog, 0, 0, 0); -#endif - - ExitProcess(1); -} - -#undef OS_WINDOWS // shlwapi uses its own OS_WINDOWS include inside -#define OS_WINDOWS 1 - -internal void -w32_entry_point_caller(int argc, WCHAR **wargv) -{ - SetUnhandledExceptionFilter(&win32_exception_filter); - - //- rjf: dynamically load windows functions which are not guaranteed - // in all SDKs - { - HMODULE module = LoadLibraryA("kernel32.dll"); - w32_SetThreadDescription_func = (W32_SetThreadDescription_Type *)GetProcAddress(module, "SetThreadDescription"); - FreeLibrary(module); - } - - //- rjf: try to allow large pages if we can - B32 large_pages_allowed = 0; - { - HANDLE token; - if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) - { - LUID luid; - if(LookupPrivilegeValue(0, SE_LOCK_MEMORY_NAME, &luid)) - { - TOKEN_PRIVILEGES priv; - priv.PrivilegeCount = 1; - priv.Privileges[0].Luid = luid; - priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - large_pages_allowed = !!AdjustTokenPrivileges(token, 0, &priv, sizeof(priv), 0, 0); - } - CloseHandle(token); - } - } - - //- rjf: get system info - SYSTEM_INFO sysinfo = {0}; - GetSystemInfo(&sysinfo); - - //- rjf: set up non-dynamically-alloc'd state - // - // (we need to set up some basics before this layer can supply - // memory allocation primitives) - { - os_w32_state.microsecond_resolution = 1; - LARGE_INTEGER large_int_resolution; - if(QueryPerformanceFrequency(&large_int_resolution)) - { - os_w32_state.microsecond_resolution = large_int_resolution.QuadPart; - } - } - { - OS_SystemInfo *info = &os_w32_state.system_info; - info->logical_processor_count = (U64)sysinfo.dwNumberOfProcessors; - info->page_size = sysinfo.dwPageSize; - info->large_page_size = GetLargePageMinimum(); - info->allocation_granularity = sysinfo.dwAllocationGranularity; - } - { - OS_ProcessInfo *info = &os_w32_state.process_info; - info->large_pages_allowed = large_pages_allowed; - info->pid = GetCurrentProcessId(); - } - - //- rjf: extract arguments - Arena *args_arena = arena_alloc(.reserve_size = MB(1), .commit_size = KB(32)); - char **argv = push_array(args_arena, char *, argc); - for(int i = 0; i < argc; i += 1) - { - String16 arg16 = str16_cstring((U16 *)wargv[i]); - String8 arg8 = str8_from_16(args_arena, arg16); - if(str8_match(arg8, str8_lit("--quiet"), StringMatchFlag_CaseInsensitive) || - str8_match(arg8, str8_lit("-quiet"), StringMatchFlag_CaseInsensitive)) - { - win32_g_is_quiet = 1; - } - if(str8_match(arg8, str8_lit("--large_pages"), StringMatchFlag_CaseInsensitive) || - str8_match(arg8, str8_lit("-large_pages"), StringMatchFlag_CaseInsensitive)) - { - arena_default_flags = ArenaFlag_LargePages; - arena_default_reserve_size = Max(MB(64), os_w32_state.system_info.large_page_size); - arena_default_commit_size = arena_default_reserve_size; - } - argv[i] = (char *)arg8.str; - } - - //- rjf: set up thread context - local_persist TCTX tctx; - tctx_init_and_equip(&tctx); - - //- rjf: set up dynamically-alloc'd state - Arena *arena = arena_alloc(); - { - os_w32_state.arena = arena; - { - OS_SystemInfo *info = &os_w32_state.system_info; - U8 buffer[MAX_COMPUTERNAME_LENGTH + 1] = {0}; - DWORD size = MAX_COMPUTERNAME_LENGTH + 1; - if(GetComputerNameA((char*)buffer, &size)) - { - info->machine_name = push_str8_copy(arena, str8(buffer, size)); - } - } - } - { - OS_ProcessInfo *info = &os_w32_state.process_info; - { - Temp scratch = scratch_begin(0, 0); - DWORD size = KB(32); - U16 *buffer = push_array_no_zero(scratch.arena, U16, size); - DWORD length = GetModuleFileNameW(0, (WCHAR*)buffer, size); - String8 name8 = str8_from_16(scratch.arena, str16(buffer, length)); - String8 name_chopped = str8_chop_last_slash(name8); - info->binary_path = push_str8_copy(arena, name_chopped); - scratch_end(scratch); - } - info->initial_path = os_get_current_path(arena); - { - Temp scratch = scratch_begin(0, 0); - U64 size = KB(32); - U16 *buffer = push_array_no_zero(scratch.arena, U16, size); - if(SUCCEEDED(SHGetFolderPathW(0, CSIDL_APPDATA, 0, 0, (WCHAR*)buffer))) - { - info->user_program_data_path = str8_from_16(arena, str16_cstring(buffer)); - } - scratch_end(scratch); - } - { - WCHAR *this_proc_env = GetEnvironmentStringsW(); - U64 start_idx = 0; - for(U64 idx = 0;; idx += 1) - { - if(this_proc_env[idx] == 0) - { - if(start_idx == idx) - { - break; - } - else - { - String16 string16 = str16((U16 *)this_proc_env + start_idx, idx - start_idx); - String8 string = str8_from_16(arena, string16); - str8_list_push(arena, &info->environment, string); - start_idx = idx+1; - } - } - } - } - } - - //- rjf: set up entity storage - InitializeCriticalSection(&os_w32_state.entity_mutex); - os_w32_state.entity_arena = arena_alloc(); - - //- rjf: call into "real" entry point - main_thread_base_entry_point(argc, argv); -} - -#if BUILD_CONSOLE_INTERFACE -int wmain(int argc, WCHAR **argv) -{ - w32_entry_point_caller(argc, argv); - return 0; -} -#else -int wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) -{ - w32_entry_point_caller(__argc, __wargv); - return 0; -} -#endif +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//////////////////////////////// +//~ dan: Modern Windows SDK Functions +// +// (We must dynamically link to them, since they can be missing in older SDKs) + +typedef HRESULT W32_SetThreadDescription_Type(HANDLE hThread, PCWSTR lpThreadDescription); +global W32_SetThreadDescription_Type *w32_SetThreadDescription_func = 0; + +//////////////////////////////// +//~ dan: File Info Conversion Helpers + +internal FilePropertyFlags +os_w32_file_property_flags_from_dwFileAttributes(DWORD dwFileAttributes) +{ + FilePropertyFlags flags = 0; + if(dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + flags |= FilePropertyFlag_IsFolder; + } + return flags; +} + +internal void +os_w32_file_properties_from_attribute_data(FileProperties *properties, WIN32_FILE_ATTRIBUTE_DATA *attributes) +{ + properties->size = Compose64Bit(attributes->nFileSizeHigh, attributes->nFileSizeLow); + os_w32_dense_time_from_file_time(&properties->created, &attributes->ftCreationTime); + os_w32_dense_time_from_file_time(&properties->modified, &attributes->ftLastWriteTime); + properties->flags = os_w32_file_property_flags_from_dwFileAttributes(attributes->dwFileAttributes); +} + +//////////////////////////////// +//~ dan: Time Conversion Helpers + +internal void +os_w32_date_time_from_system_time(DateTime *out, SYSTEMTIME *in) +{ + out->year = in->wYear; + out->mon = in->wMonth - 1; + out->wday = in->wDayOfWeek; + out->day = in->wDay; + out->hour = in->wHour; + out->min = in->wMinute; + out->sec = in->wSecond; + out->msec = in->wMilliseconds; +} + +internal void +os_w32_system_time_from_date_time(SYSTEMTIME *out, DateTime *in) +{ + out->wYear = (WORD)(in->year); + out->wMonth = in->mon + 1; + out->wDay = in->day; + out->wHour = in->hour; + out->wMinute = in->min; + out->wSecond = in->sec; + out->wMilliseconds = in->msec; +} + +internal void +os_w32_dense_time_from_file_time(DenseTime *out, FILETIME *in) +{ + SYSTEMTIME systime = {0}; + FileTimeToSystemTime(in, &systime); + DateTime date_time = {0}; + os_w32_date_time_from_system_time(&date_time, &systime); + *out = dense_time_from_date_time(date_time); +} + +internal U32 +os_w32_sleep_ms_from_endt_us(U64 endt_us) +{ + U32 sleep_ms = 0; + if(endt_us == max_U64) + { + sleep_ms = INFINITE; + } + else + { + U64 begint = os_now_microseconds(); + if(begint < endt_us) + { + U64 sleep_us = endt_us - begint; + sleep_ms = (U32)((sleep_us + 999)/1000); + } + } + return sleep_ms; +} + +internal U32 +os_w32_unix_time_from_file_time(FILETIME file_time) +{ + U64 win32_time = ((U64)file_time.dwHighDateTime << 32) | file_time.dwLowDateTime; + U64 unix_time64 = ((win32_time - 0x19DB1DED53E8000ULL) / 10000000); + + Assert(unix_time64 <= max_U32); + U32 unix_time32 = (U32)unix_time64; + + return unix_time32; +} + +//////////////////////////////// +//~ dan: Entity Functions + +internal OS_W32_Entity * +os_w32_entity_alloc(OS_W32_EntityKind kind) +{ + OS_W32_Entity *result = 0; + EnterCriticalSection(&os_w32_state.entity_mutex); + { + result = os_w32_state.entity_free; + if(result) + { + SLLStackPop(os_w32_state.entity_free); + } + else + { + result = push_array_no_zero(os_w32_state.entity_arena, OS_W32_Entity, 1); + } + MemoryZeroStruct(result); + } + LeaveCriticalSection(&os_w32_state.entity_mutex); + result->kind = kind; + return result; +} + +internal void +os_w32_entity_release(OS_W32_Entity *entity) +{ + entity->kind = OS_W32_EntityKind_Null; + EnterCriticalSection(&os_w32_state.entity_mutex); + SLLStackPush(os_w32_state.entity_free, entity); + LeaveCriticalSection(&os_w32_state.entity_mutex); +} + +//////////////////////////////// +//~ dan: Thread Entry Point + +internal DWORD +os_w32_thread_entry_point(void *ptr) +{ + OS_W32_Entity *entity = (OS_W32_Entity *)ptr; + OS_ThreadFunctionType *func = entity->thread.func; + void *thread_ptr = entity->thread.ptr; + TCTX tctx_; + tctx_init_and_equip(&tctx_); + func(thread_ptr); + tctx_release(); + return 0; +} + +//////////////////////////////// +//~ dan: @os_hooks System/Process Info (Implemented Per-OS) + +internal OS_SystemInfo * +os_get_system_info(void) +{ + return &os_w32_state.system_info; +} + +internal OS_ProcessInfo * +os_get_process_info(void) +{ + return &os_w32_state.process_info; +} + +internal String8 +os_get_current_path(Arena *arena) +{ + Temp scratch = scratch_begin(&arena, 1); + DWORD length = GetCurrentDirectoryW(0, 0); + U16 *memory = push_array_no_zero(scratch.arena, U16, length + 1); + length = GetCurrentDirectoryW(length + 1, (WCHAR*)memory); + String8 name = str8_from_16(arena, str16(memory, length)); + scratch_end(scratch); + return name; +} + +internal U32 +os_get_process_start_time_unix(void) +{ + HANDLE handle = GetCurrentProcess(); + FILETIME start_time = {0}; + FILETIME exit_time; + FILETIME kernel_time; + FILETIME user_time; + if(GetProcessTimes(handle, &start_time, &exit_time, &kernel_time, &user_time)) + { + return os_w32_unix_time_from_file_time(start_time); + } + return 0; +} + +//////////////////////////////// +//~ dan: @os_hooks Memory Allocation (Implemented Per-OS) + +//- rjf: basic + +internal void * +os_reserve(U64 size) +{ + void *result = VirtualAlloc(0, size, MEM_RESERVE, PAGE_READWRITE); + return result; +} + +internal B32 +os_commit(void *ptr, U64 size) +{ + B32 result = (VirtualAlloc(ptr, size, MEM_COMMIT, PAGE_READWRITE) != 0); + return result; +} + +internal void +os_decommit(void *ptr, U64 size) +{ + VirtualFree(ptr, size, MEM_DECOMMIT); +} + +internal void +os_release(void *ptr, U64 size) +{ + // NOTE(rjf): size not used - not necessary on Windows, but necessary for other OSes. + VirtualFree(ptr, 0, MEM_RELEASE); +} + +//- rjf: large pages + +internal void * +os_reserve_large(U64 size) +{ + // we commit on reserve because windows + void *result = VirtualAlloc(0, size, MEM_RESERVE|MEM_COMMIT|MEM_LARGE_PAGES, PAGE_READWRITE); + return result; +} + +internal B32 +os_commit_large(void *ptr, U64 size) +{ + return 1; +} + +//////////////////////////////// +//~ dan: @os_hooks Thread Info (Implemented Per-OS) + +internal U32 +os_tid(void) +{ + DWORD id = GetCurrentThreadId(); + return (U32)id; +} + +internal void +os_set_thread_name(String8 name) +{ + Temp scratch = scratch_begin(0, 0); + + // rjf: windows 10 style + if(w32_SetThreadDescription_func) + { + String16 name16 = str16_from_8(scratch.arena, name); + HRESULT hr = w32_SetThreadDescription_func(GetCurrentThread(), (WCHAR*)name16.str); + } + + // rjf: raise-exception style + { + String8 name_copy = push_str8_copy(scratch.arena, name); +#pragma pack(push,8) + typedef struct THREADNAME_INFO THREADNAME_INFO; + struct THREADNAME_INFO + { + U32 dwType; // Must be 0x1000. + char *szName; // Pointer to name (in user addr space). + U32 dwThreadID; // Thread ID (-1=caller thread). + U32 dwFlags; // Reserved for future use, must be zero. + }; +#pragma pack(pop) + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = (char *)name_copy.str; + info.dwThreadID = os_tid(); + info.dwFlags = 0; +#pragma warning(push) +#pragma warning(disable: 6320 6322) + __try + { + RaiseException(0x406D1388, 0, sizeof(info) / sizeof(void *), (const ULONG_PTR *)&info); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + } +#pragma warning(pop) + } + + scratch_end(scratch); +} + +//////////////////////////////// +//~ dan: @os_hooks Aborting (Implemented Per-OS) + +internal void +os_abort(S32 exit_code) +{ + ExitProcess(exit_code); +} + +//////////////////////////////// +//~ dan: @os_hooks File System (Implemented Per-OS) + +//- rjf: files + +internal OS_Handle +os_file_open(OS_AccessFlags flags, String8 path) +{ + OS_Handle result = {0}; + Temp scratch = scratch_begin(0, 0); + String16 path16 = str16_from_8(scratch.arena, path); + DWORD access_flags = 0; + DWORD share_mode = 0; + DWORD creation_disposition = OPEN_EXISTING; + SECURITY_ATTRIBUTES security_attributes = {sizeof(security_attributes), 0, 0}; + if(flags & OS_AccessFlag_Read) {access_flags |= GENERIC_READ;} + if(flags & OS_AccessFlag_Write) {access_flags |= GENERIC_WRITE;} + if(flags & OS_AccessFlag_Execute) {access_flags |= GENERIC_EXECUTE;} + if(flags & OS_AccessFlag_ShareRead) {share_mode |= FILE_SHARE_READ;} + if(flags & OS_AccessFlag_ShareWrite) {share_mode |= FILE_SHARE_WRITE|FILE_SHARE_DELETE;} + if(flags & OS_AccessFlag_Write) {creation_disposition = CREATE_ALWAYS;} + if(flags & OS_AccessFlag_Append) {creation_disposition = OPEN_ALWAYS; access_flags |= FILE_APPEND_DATA; } + if(flags & OS_AccessFlag_Inherited) + { + security_attributes.bInheritHandle = 1; + } + HANDLE file = CreateFileW((WCHAR *)path16.str, access_flags, share_mode, &security_attributes, creation_disposition, FILE_ATTRIBUTE_NORMAL, 0); + if(file != INVALID_HANDLE_VALUE) + { + result.u64[0] = (U64)file; + } + scratch_end(scratch); + return result; +} + +internal void +os_file_close(OS_Handle file) +{ + if(os_handle_match(file, os_handle_zero())) { return; } + HANDLE handle = (HANDLE)file.u64[0]; + BOOL result = CloseHandle(handle); + (void)result; +} + +internal U64 +os_file_read(OS_Handle file, Rng1U64 rng, void *out_data) +{ + if(os_handle_match(file, os_handle_zero())) { return 0; } + HANDLE handle = (HANDLE)file.u64[0]; + + // rjf: clamp range by file size + U64 size = 0; + GetFileSizeEx(handle, (LARGE_INTEGER *)&size); + Rng1U64 rng_clamped = r1u64(ClampTop(rng.min, size), ClampTop(rng.max, size)); + U64 total_read_size = 0; + + // rjf: read loop + { + U64 to_read = dim_1u64(rng_clamped); + for(U64 off = rng.min; total_read_size < to_read;) + { + U64 amt64 = to_read - total_read_size; + U32 amt32 = u32_from_u64_saturate(amt64); + DWORD read_size = 0; + OVERLAPPED overlapped = {0}; + overlapped.Offset = (off&0x00000000ffffffffull); + overlapped.OffsetHigh = (off&0xffffffff00000000ull) >> 32; + ReadFile(handle, (U8 *)out_data + total_read_size, amt32, &read_size, &overlapped); + off += read_size; + total_read_size += read_size; + if(read_size != amt32) + { + break; + } + } + } + + return total_read_size; +} + +internal U64 +os_file_write(OS_Handle file, Rng1U64 rng, void *data) +{ + if(os_handle_match(file, os_handle_zero())) { return 0; } + HANDLE win_handle = (HANDLE)file.u64[0]; + U64 src_off = 0; + U64 dst_off = rng.min; + U64 total_write_size = dim_1u64(rng); + for(;;) + { + void *bytes_src = (U8 *)data + src_off; + U64 bytes_left = total_write_size - src_off; + DWORD write_size = Min(MB(1), bytes_left); + DWORD bytes_written = 0; + OVERLAPPED overlapped = {0}; + overlapped.Offset = (dst_off&0x00000000ffffffffull); + overlapped.OffsetHigh = (dst_off&0xffffffff00000000ull) >> 32; + BOOL success = WriteFile(win_handle, bytes_src, write_size, &bytes_written, &overlapped); + if(success == 0) + { + break; + } + src_off += bytes_written; + dst_off += bytes_written; + if(bytes_left == 0) + { + break; + } + } + return src_off; +} + +internal B32 +os_file_set_time(OS_Handle file, DateTime time) +{ + if(os_handle_match(file, os_handle_zero())) { return 0; } + B32 result = 0; + HANDLE handle = (HANDLE)file.u64[0]; + SYSTEMTIME system_time = {0}; + os_w32_system_time_from_date_time(&system_time, &time); + FILETIME file_time = {0}; + result = (SystemTimeToFileTime(&system_time, &file_time) && + SetFileTime(handle, &file_time, &file_time, &file_time)); + return result; +} + +internal FileProperties +os_properties_from_file(OS_Handle file) +{ + if(os_handle_match(file, os_handle_zero())) { FileProperties r = {0}; return r; } + FileProperties props = {0}; + HANDLE handle = (HANDLE)file.u64[0]; + BY_HANDLE_FILE_INFORMATION info; + BOOL info_good = GetFileInformationByHandle(handle, &info); + if(info_good) + { + U32 size_lo = info.nFileSizeLow; + U32 size_hi = info.nFileSizeHigh; + props.size = (U64)size_lo | (((U64)size_hi)<<32); + os_w32_dense_time_from_file_time(&props.modified, &info.ftLastWriteTime); + os_w32_dense_time_from_file_time(&props.created, &info.ftCreationTime); + props.flags = os_w32_file_property_flags_from_dwFileAttributes(info.dwFileAttributes); + } + return props; +} + +internal OS_FileID +os_id_from_file(OS_Handle file) +{ + if(os_handle_match(file, os_handle_zero())) { OS_FileID r = {0}; return r; } + OS_FileID result = {0}; + HANDLE handle = (HANDLE)file.u64[0]; + BY_HANDLE_FILE_INFORMATION info; + BOOL is_ok = GetFileInformationByHandle(handle, &info); + if(is_ok) + { + result.v[0] = info.dwVolumeSerialNumber; + result.v[1] = info.nFileIndexLow; + result.v[2] = info.nFileIndexHigh; + } + return result; +} + +internal B32 +os_delete_file_at_path(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + String16 path16 = str16_from_8(scratch.arena, path); + B32 result = DeleteFileW((WCHAR*)path16.str); + scratch_end(scratch); + return result; +} + +internal B32 +os_copy_file_path(String8 dst, String8 src) +{ + Temp scratch = scratch_begin(0, 0); + String16 dst16 = str16_from_8(scratch.arena, dst); + String16 src16 = str16_from_8(scratch.arena, src); + B32 result = CopyFileW((WCHAR*)src16.str, (WCHAR*)dst16.str, 0); + scratch_end(scratch); + return result; +} + +internal String8 +os_full_path_from_path(Arena *arena, String8 path) +{ + Temp scratch = scratch_begin(&arena, 1); + DWORD buffer_size = Max(MAX_PATH, path.size * 2) + 1; + String16 path16 = str16_from_8(scratch.arena, path); + WCHAR *buffer = push_array_no_zero(scratch.arena, WCHAR, buffer_size); + DWORD path16_size = GetFullPathNameW((WCHAR*)path16.str, buffer_size, buffer, NULL); + if(path16_size > buffer_size) + { + arena_pop(scratch.arena, buffer_size); + buffer_size = path16_size + 1; + buffer = push_array_no_zero(scratch.arena, WCHAR, buffer_size); + path16_size = GetFullPathNameW((WCHAR*)path16.str, buffer_size, buffer, NULL); + } + String8 full_path = str8_from_16(arena, str16((U16*)buffer, path16_size)); + scratch_end(scratch); + return full_path; +} + +internal B32 +os_file_path_exists(String8 path) +{ + Temp scratch = scratch_begin(0,0); + String16 path16 = str16_from_8(scratch.arena, path); + DWORD attributes = GetFileAttributesW((WCHAR *)path16.str); + B32 exists = (attributes != INVALID_FILE_ATTRIBUTES) && !!(~attributes & FILE_ATTRIBUTE_DIRECTORY); + scratch_end(scratch); + return exists; +} + +internal B32 +os_folder_path_exists(String8 path) +{ + Temp scratch = scratch_begin(0,0); + String16 path16 = str16_from_8(scratch.arena, path); + DWORD attributes = GetFileAttributesW((WCHAR *)path16.str); + B32 exists = (attributes != INVALID_FILE_ATTRIBUTES) && (attributes & FILE_ATTRIBUTE_DIRECTORY); + scratch_end(scratch); + return exists; +} + +internal FileProperties +os_properties_from_file_path(String8 path) +{ + WIN32_FIND_DATAW find_data = {0}; + Temp scratch = scratch_begin(0, 0); + String16 path16 = str16_from_8(scratch.arena, path); + HANDLE handle = FindFirstFileW((WCHAR *)path16.str, &find_data); + FileProperties props = {0}; + if(handle != INVALID_HANDLE_VALUE) + { + props.size = Compose64Bit(find_data.nFileSizeHigh, find_data.nFileSizeLow); + os_w32_dense_time_from_file_time(&props.created, &find_data.ftCreationTime); + os_w32_dense_time_from_file_time(&props.modified, &find_data.ftLastWriteTime); + props.flags = os_w32_file_property_flags_from_dwFileAttributes(find_data.dwFileAttributes); + } + FindClose(handle); + scratch_end(scratch); + return props; +} + +//- rjf: file maps + +internal OS_Handle +os_file_map_open(OS_AccessFlags flags, OS_Handle file) +{ + OS_Handle map = {0}; + { + HANDLE file_handle = (HANDLE)file.u64[0]; + DWORD protect_flags = 0; + { + switch(flags) + { + default:{}break; + case OS_AccessFlag_Read: + {protect_flags = PAGE_READONLY;}break; + case OS_AccessFlag_Write: + case OS_AccessFlag_Read|OS_AccessFlag_Write: + {protect_flags = PAGE_READWRITE;}break; + case OS_AccessFlag_Execute: + case OS_AccessFlag_Read|OS_AccessFlag_Execute: + {protect_flags = PAGE_EXECUTE_READ;}break; + case OS_AccessFlag_Execute|OS_AccessFlag_Write|OS_AccessFlag_Read: + case OS_AccessFlag_Execute|OS_AccessFlag_Write: + {protect_flags = PAGE_EXECUTE_READWRITE;}break; + } + } + HANDLE map_handle = CreateFileMappingA(file_handle, 0, protect_flags, 0, 0, 0); + map.u64[0] = (U64)map_handle; + } + return map; +} + +internal void +os_file_map_close(OS_Handle map) +{ + HANDLE handle = (HANDLE)map.u64[0]; + BOOL result = CloseHandle(handle); + (void)result; +} + +internal void * +os_file_map_view_open(OS_Handle map, OS_AccessFlags flags, Rng1U64 range) +{ + HANDLE handle = (HANDLE)map.u64[0]; + U32 off_lo = (U32)((range.min&0x00000000ffffffffull)>>0); + U32 off_hi = (U32)((range.min&0xffffffff00000000ull)>>32); + U64 size = dim_1u64(range); + DWORD access_flags = 0; + { + switch(flags) + { + default:{}break; + case OS_AccessFlag_Read: + { + access_flags = FILE_MAP_READ; + }break; + case OS_AccessFlag_Write: + { + access_flags = FILE_MAP_WRITE; + }break; + case OS_AccessFlag_Read|OS_AccessFlag_Write: + { + access_flags = FILE_MAP_ALL_ACCESS; + }break; + case OS_AccessFlag_Execute: + case OS_AccessFlag_Read|OS_AccessFlag_Execute: + case OS_AccessFlag_Write|OS_AccessFlag_Execute: + case OS_AccessFlag_Read|OS_AccessFlag_Write|OS_AccessFlag_Execute: + { + access_flags = FILE_MAP_ALL_ACCESS|FILE_MAP_EXECUTE; + }break; + } + } + void *result = MapViewOfFile(handle, access_flags, off_hi, off_lo, size); + return result; +} + +internal void +os_file_map_view_close(OS_Handle map, void *ptr, Rng1U64 range) +{ + BOOL result = UnmapViewOfFile(ptr); + (void)result; +} + +//- rjf: directory iteration + +internal OS_FileIter * +os_file_iter_begin(Arena *arena, String8 path, OS_FileIterFlags flags) +{ + Temp scratch = scratch_begin(&arena, 1); + String8 path_with_wildcard = push_str8_cat(scratch.arena, path, str8_lit("\\*")); + String16 path16 = str16_from_8(scratch.arena, path_with_wildcard); + OS_FileIter *iter = push_array(arena, OS_FileIter, 1); + iter->flags = flags; + OS_W32_FileIter *w32_iter = (OS_W32_FileIter*)iter->memory; + if(path.size == 0) + { + w32_iter->is_volume_iter = 1; + WCHAR buffer[512] = {0}; + DWORD length = GetLogicalDriveStringsW(sizeof(buffer), buffer); + String8List drive_strings = {0}; + for(U64 off = 0; off < (U64)length;) + { + String16 next_drive_string_16 = str16_cstring((U16 *)buffer+off); + off += next_drive_string_16.size+1; + String8 next_drive_string = str8_from_16(arena, next_drive_string_16); + next_drive_string = str8_chop_last_slash(next_drive_string); + str8_list_push(scratch.arena, &drive_strings, next_drive_string); + } + w32_iter->drive_strings = str8_array_from_list(arena, &drive_strings); + w32_iter->drive_strings_iter_idx = 0; + } + else + { + w32_iter->handle = FindFirstFileW((WCHAR*)path16.str, &w32_iter->find_data); + } + scratch_end(scratch); + return iter; +} + +internal B32 +os_file_iter_next(Arena *arena, OS_FileIter *iter, OS_FileInfo *info_out) +{ + B32 result = 0; + OS_FileIterFlags flags = iter->flags; + OS_W32_FileIter *w32_iter = (OS_W32_FileIter*)iter->memory; + switch(w32_iter->is_volume_iter) + { + //- rjf: file iteration + default: + case 0: + { + if (!(flags & OS_FileIterFlag_Done) && w32_iter->handle != INVALID_HANDLE_VALUE) + { + do + { + // check is usable + B32 usable_file = 1; + + WCHAR *file_name = w32_iter->find_data.cFileName; + DWORD attributes = w32_iter->find_data.dwFileAttributes; + if (file_name[0] == '.'){ + if (flags & OS_FileIterFlag_SkipHiddenFiles){ + usable_file = 0; + } + else if (file_name[1] == 0){ + usable_file = 0; + } + else if (file_name[1] == '.' && file_name[2] == 0){ + usable_file = 0; + } + } + if (attributes & FILE_ATTRIBUTE_DIRECTORY){ + if (flags & OS_FileIterFlag_SkipFolders){ + usable_file = 0; + } + } + else{ + if (flags & OS_FileIterFlag_SkipFiles){ + usable_file = 0; + } + } + + // emit if usable + if (usable_file){ + info_out->name = str8_from_16(arena, str16_cstring((U16*)file_name)); + info_out->props.size = (U64)w32_iter->find_data.nFileSizeLow | (((U64)w32_iter->find_data.nFileSizeHigh)<<32); + os_w32_dense_time_from_file_time(&info_out->props.created, &w32_iter->find_data.ftCreationTime); + os_w32_dense_time_from_file_time(&info_out->props.modified, &w32_iter->find_data.ftLastWriteTime); + info_out->props.flags = os_w32_file_property_flags_from_dwFileAttributes(attributes); + result = 1; + if (!FindNextFileW(w32_iter->handle, &w32_iter->find_data)){ + iter->flags |= OS_FileIterFlag_Done; + } + break; + } + }while(FindNextFileW(w32_iter->handle, &w32_iter->find_data)); + } + }break; + + //- rjf: volume iteration + case 1: + { + result = w32_iter->drive_strings_iter_idx < w32_iter->drive_strings.count; + if(result != 0) + { + MemoryZeroStruct(info_out); + info_out->name = w32_iter->drive_strings.v[w32_iter->drive_strings_iter_idx]; + info_out->props.flags |= FilePropertyFlag_IsFolder; + w32_iter->drive_strings_iter_idx += 1; + } + }break; + } + if(!result) + { + iter->flags |= OS_FileIterFlag_Done; + } + return result; +} + +internal void +os_file_iter_end(OS_FileIter *iter) +{ + OS_W32_FileIter *w32_iter = (OS_W32_FileIter*)iter->memory; + HANDLE zero_handle; + MemoryZeroStruct(&zero_handle); + if(!MemoryMatchStruct(&zero_handle, &w32_iter->handle)) + { + FindClose(w32_iter->handle); + } +} + +//- rjf: directory creation + +internal B32 +os_make_directory(String8 path) +{ + B32 result = 0; + Temp scratch = scratch_begin(0, 0); + String16 name16 = str16_from_8(scratch.arena, path); + WIN32_FILE_ATTRIBUTE_DATA attributes = {0}; + GetFileAttributesExW((WCHAR*)name16.str, GetFileExInfoStandard, &attributes); + if(attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + result = 1; + } + else if(CreateDirectoryW((WCHAR*)name16.str, 0)) + { + result = 1; + } + scratch_end(scratch); + return(result); +} + +//////////////////////////////// +//~ dan: @os_hooks Shared Memory (Implemented Per-OS) + +internal OS_Handle +os_shared_memory_alloc(U64 size, String8 name) +{ + Temp scratch = scratch_begin(0, 0); + String16 name16 = str16_from_8(scratch.arena, name); + HANDLE file = CreateFileMappingW(INVALID_HANDLE_VALUE, + 0, + PAGE_READWRITE, + (U32)((size & 0xffffffff00000000) >> 32), + (U32)((size & 0x00000000ffffffff)), + (WCHAR *)name16.str); + OS_Handle result = {(U64)file}; + scratch_end(scratch); + return result; +} + +internal OS_Handle +os_shared_memory_open(String8 name) +{ + Temp scratch = scratch_begin(0, 0); + String16 name16 = str16_from_8(scratch.arena, name); + HANDLE file = OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, (WCHAR *)name16.str); + OS_Handle result = {(U64)file}; + scratch_end(scratch); + return result; +} + +internal void +os_shared_memory_close(OS_Handle handle) +{ + HANDLE file = (HANDLE)(handle.u64[0]); + CloseHandle(file); +} + +internal void * +os_shared_memory_view_open(OS_Handle handle, Rng1U64 range) +{ + HANDLE file = (HANDLE)(handle.u64[0]); + U64 offset = range.min; + U64 size = range.max-range.min; + void *ptr = MapViewOfFile(file, FILE_MAP_ALL_ACCESS, + (U32)((offset & 0xffffffff00000000) >> 32), + (U32)((offset & 0x00000000ffffffff)), + size); + return ptr; +} + +internal void +os_shared_memory_view_close(OS_Handle handle, void *ptr, Rng1U64 range) +{ + UnmapViewOfFile(ptr); +} + +//////////////////////////////// +//~ dan: @os_hooks Time (Implemented Per-OS) + +internal U64 +os_now_microseconds(void) +{ + U64 result = 0; + LARGE_INTEGER large_int_counter; + if(QueryPerformanceCounter(&large_int_counter)) + { + result = (large_int_counter.QuadPart*Million(1))/os_w32_state.microsecond_resolution; + } + return result; +} + +internal U32 +os_now_unix(void) +{ + FILETIME file_time; + GetSystemTimeAsFileTime(&file_time); + U32 unix_time = os_w32_unix_time_from_file_time(file_time); + return unix_time; +} + +internal DateTime +os_now_universal_time(void) +{ + SYSTEMTIME systime = {0}; + GetSystemTime(&systime); + DateTime result = {0}; + os_w32_date_time_from_system_time(&result, &systime); + return result; +} + +internal DateTime +os_universal_time_from_local(DateTime *date_time) +{ + SYSTEMTIME systime = {0}; + os_w32_system_time_from_date_time(&systime, date_time); + FILETIME ftime = {0}; + SystemTimeToFileTime(&systime, &ftime); + FILETIME ftime_local = {0}; + LocalFileTimeToFileTime(&ftime, &ftime_local); + FileTimeToSystemTime(&ftime_local, &systime); + DateTime result = {0}; + os_w32_date_time_from_system_time(&result, &systime); + return result; +} + +internal DateTime +os_local_time_from_universal(DateTime *date_time) +{ + SYSTEMTIME systime = {0}; + os_w32_system_time_from_date_time(&systime, date_time); + FILETIME ftime = {0}; + SystemTimeToFileTime(&systime, &ftime); + FILETIME ftime_local = {0}; + FileTimeToLocalFileTime(&ftime, &ftime_local); + FileTimeToSystemTime(&ftime_local, &systime); + DateTime result = {0}; + os_w32_date_time_from_system_time(&result, &systime); + return result; +} + +internal void +os_sleep_milliseconds(U32 msec) +{ + Sleep(msec); +} + +//////////////////////////////// +//~ dan: @os_hooks Child Processes (Implemented Per-OS) + +internal OS_Handle +os_process_launch(OS_ProcessLaunchParams *params) +{ + OS_Handle result = {0}; + Temp scratch = scratch_begin(0, 0); + + //- rjf: form full command string + String8 cmd = {0}; + { + StringJoin join_params = {0}; + join_params.pre = str8_lit("\""); + join_params.sep = str8_lit("\" \""); + join_params.post = str8_lit("\""); + cmd = str8_list_join(scratch.arena, ¶ms->cmd_line, &join_params); + } + + //- rjf: form environment + B32 use_null_env_arg = 0; + String8 env = {0}; + { + StringJoin join_params2 = {0}; + join_params2.sep = str8_lit("\0"); + join_params2.post = str8_lit("\0"); + String8List all_opts = params->env; + if(params->inherit_env != 0) + { + if(all_opts.node_count != 0) + { + MemoryZeroStruct(&all_opts); + for(String8Node *n = params->env.first; n != 0; n = n->next) + { + str8_list_push(scratch.arena, &all_opts, n->string); + } + for(String8Node *n = os_w32_state.process_info.environment.first; n != 0; n = n->next) + { + str8_list_push(scratch.arena, &all_opts, n->string); + } + } + else + { + use_null_env_arg = 1; + } + } + if(use_null_env_arg == 0) + { + env = str8_list_join(scratch.arena, &all_opts, &join_params2); + } + } + + //- rjf: utf-8 -> utf-16 + String16 cmd16 = str16_from_8(scratch.arena, cmd); + String16 dir16 = str16_from_8(scratch.arena, params->path); + String16 env16 = {0}; + if(use_null_env_arg == 0) + { + env16 = str16_from_8(scratch.arena, env); + } + + //- rjf: determine creation flags + DWORD creation_flags = CREATE_UNICODE_ENVIRONMENT; + if(params->consoleless) + { + creation_flags |= CREATE_NO_WINDOW; + } + + //- rjf: launch + BOOL inherit_handles = 0; + STARTUPINFOW startup_info = {sizeof(startup_info)}; + if(!os_handle_match(params->stdout_file, os_handle_zero())) + { + HANDLE stdout_handle = (HANDLE)params->stdout_file.u64[0]; + startup_info.hStdOutput = stdout_handle; + startup_info.dwFlags |= STARTF_USESTDHANDLES; + inherit_handles = 1; + } + if(!os_handle_match(params->stderr_file, os_handle_zero())) + { + HANDLE stderr_handle = (HANDLE)params->stderr_file.u64[0]; + startup_info.hStdError = stderr_handle; + startup_info.dwFlags |= STARTF_USESTDHANDLES; + inherit_handles = 1; + } + if(!os_handle_match(params->stdin_file, os_handle_zero())) + { + HANDLE stdin_handle = (HANDLE)params->stdin_file.u64[0]; + startup_info.hStdInput = stdin_handle; + startup_info.dwFlags |= STARTF_USESTDHANDLES; + inherit_handles = 1; + } + PROCESS_INFORMATION process_info = {0}; + if(CreateProcessW(0, (WCHAR*)cmd16.str, 0, 0, inherit_handles, creation_flags, use_null_env_arg ? 0 : (WCHAR*)env16.str, (WCHAR*)dir16.str, &startup_info, &process_info)) + { + result.u64[0] = (U64)process_info.hProcess; + CloseHandle(process_info.hThread); + } + + scratch_end(scratch); + return result; +} + +internal B32 +os_process_join(OS_Handle handle, U64 endt_us) +{ + HANDLE process = (HANDLE)(handle.u64[0]); + DWORD sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); + DWORD result = WaitForSingleObject(process, sleep_ms); + return (result == WAIT_OBJECT_0); +} + +internal void +os_process_detach(OS_Handle handle) +{ + HANDLE process = (HANDLE)(handle.u64[0]); + CloseHandle(process); +} + +//////////////////////////////// +//~ dan: @os_hooks Threads (Implemented Per-OS) + +internal OS_Handle +os_thread_launch(OS_ThreadFunctionType *func, void *ptr, void *params) +{ + OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_Thread); + entity->thread.func = func; + entity->thread.ptr = ptr; + entity->thread.handle = CreateThread(0, 0, os_w32_thread_entry_point, entity, 0, &entity->thread.tid); + OS_Handle result = {IntFromPtr(entity)}; + return result; +} + +internal B32 +os_thread_join(OS_Handle handle, U64 endt_us) +{ + DWORD sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); + OS_W32_Entity *entity = (OS_W32_Entity *)PtrFromInt(handle.u64[0]); + DWORD wait_result = WAIT_OBJECT_0; + if(entity != 0) + { + wait_result = WaitForSingleObject(entity->thread.handle, sleep_ms); + CloseHandle(entity->thread.handle); + os_w32_entity_release(entity); + } + return (wait_result == WAIT_OBJECT_0); +} + +internal void +os_thread_detach(OS_Handle thread) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(thread.u64[0]); + if(entity != 0) + { + CloseHandle(entity->thread.handle); + os_w32_entity_release(entity); + } +} + +//////////////////////////////// +//~ dan: @os_hooks Synchronization Primitives (Implemented Per-OS) + +//- rjf: mutexes + +internal OS_Handle +os_mutex_alloc(void) +{ + OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_Mutex); + InitializeCriticalSection(&entity->mutex); + OS_Handle result = {IntFromPtr(entity)}; + return result; +} + +internal void +os_mutex_release(OS_Handle mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); + os_w32_entity_release(entity); +} + +internal void +os_mutex_take(OS_Handle mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); + EnterCriticalSection(&entity->mutex); +} + +internal void +os_mutex_drop(OS_Handle mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); + LeaveCriticalSection(&entity->mutex); +} + +//- rjf: reader/writer mutexes + +internal OS_Handle +os_rw_mutex_alloc(void) +{ + OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_RWMutex); + InitializeSRWLock(&entity->rw_mutex); + OS_Handle result = {IntFromPtr(entity)}; + return result; +} + +internal void +os_rw_mutex_release(OS_Handle rw_mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); + os_w32_entity_release(entity); +} + +internal void +os_rw_mutex_take_r(OS_Handle rw_mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); + AcquireSRWLockShared(&entity->rw_mutex); +} + +internal void +os_rw_mutex_drop_r(OS_Handle rw_mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); + ReleaseSRWLockShared(&entity->rw_mutex); +} + +internal void +os_rw_mutex_take_w(OS_Handle rw_mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); + AcquireSRWLockExclusive(&entity->rw_mutex); +} + +internal void +os_rw_mutex_drop_w(OS_Handle rw_mutex) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(rw_mutex.u64[0]); + ReleaseSRWLockExclusive(&entity->rw_mutex); +} + +//- rjf: condition variables + +internal OS_Handle +os_condition_variable_alloc(void) +{ + OS_W32_Entity *entity = os_w32_entity_alloc(OS_W32_EntityKind_ConditionVariable); + InitializeConditionVariable(&entity->cv); + OS_Handle result = {IntFromPtr(entity)}; + return result; +} + +internal void +os_condition_variable_release(OS_Handle cv) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); + os_w32_entity_release(entity); +} + +internal B32 +os_condition_variable_wait(OS_Handle cv, OS_Handle mutex, U64 endt_us) +{ + U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); + BOOL result = 0; + if(sleep_ms > 0) + { + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); + OS_W32_Entity *mutex_entity = (OS_W32_Entity*)PtrFromInt(mutex.u64[0]); + result = SleepConditionVariableCS(&entity->cv, &mutex_entity->mutex, sleep_ms); + } + return result; +} + +internal B32 +os_condition_variable_wait_rw_r(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) +{ + U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); + BOOL result = 0; + if(sleep_ms > 0) + { + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); + OS_W32_Entity *mutex_entity = (OS_W32_Entity*)PtrFromInt(mutex_rw.u64[0]); + result = SleepConditionVariableSRW(&entity->cv, &mutex_entity->rw_mutex, sleep_ms, + CONDITION_VARIABLE_LOCKMODE_SHARED); + } + return result; +} + +internal B32 +os_condition_variable_wait_rw_w(OS_Handle cv, OS_Handle mutex_rw, U64 endt_us) +{ + U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); + BOOL result = 0; + if(sleep_ms > 0) + { + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); + OS_W32_Entity *mutex_entity = (OS_W32_Entity*)PtrFromInt(mutex_rw.u64[0]); + result = SleepConditionVariableSRW(&entity->cv, &mutex_entity->rw_mutex, sleep_ms, 0); + } + return result; +} + +internal void +os_condition_variable_signal(OS_Handle cv) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); + WakeConditionVariable(&entity->cv); +} + +internal void +os_condition_variable_broadcast(OS_Handle cv) +{ + OS_W32_Entity *entity = (OS_W32_Entity*)PtrFromInt(cv.u64[0]); + WakeAllConditionVariable(&entity->cv); +} + +//- rjf: cross-process semaphores + +internal OS_Handle +os_semaphore_alloc(U32 initial_count, U32 max_count, String8 name) +{ + Temp scratch = scratch_begin(0, 0); + String16 name16 = str16_from_8(scratch.arena, name); + HANDLE handle = CreateSemaphoreW(0, initial_count, max_count, (WCHAR *)name16.str); + OS_Handle result = {(U64)handle}; + scratch_end(scratch); + return result; +} + +internal void +os_semaphore_release(OS_Handle semaphore) +{ + HANDLE handle = (HANDLE)semaphore.u64[0]; + CloseHandle(handle); +} + +internal OS_Handle +os_semaphore_open(String8 name) +{ + Temp scratch = scratch_begin(0, 0); + String16 name16 = str16_from_8(scratch.arena, name); + HANDLE handle = OpenSemaphoreW(SEMAPHORE_ALL_ACCESS , 0, (WCHAR *)name16.str); + OS_Handle result = {(U64)handle}; + scratch_end(scratch); + return result; +} + +internal void +os_semaphore_close(OS_Handle semaphore) +{ + HANDLE handle = (HANDLE)semaphore.u64[0]; + CloseHandle(handle); +} + +internal B32 +os_semaphore_take(OS_Handle semaphore, U64 endt_us) +{ + U32 sleep_ms = os_w32_sleep_ms_from_endt_us(endt_us); + HANDLE handle = (HANDLE)semaphore.u64[0]; + DWORD wait_result = WaitForSingleObject(handle, sleep_ms); + B32 result = (wait_result == WAIT_OBJECT_0); + return result; +} + +internal void +os_semaphore_drop(OS_Handle semaphore) +{ + HANDLE handle = (HANDLE)semaphore.u64[0]; + ReleaseSemaphore(handle, 1, 0); +} + +//////////////////////////////// +//~ dan: @os_hooks Dynamically-Loaded Libraries (Implemented Per-OS) + +internal OS_Handle +os_library_open(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + String16 path16 = str16_from_8(scratch.arena, path); + HMODULE mod = LoadLibraryW((LPCWSTR)path16.str); + OS_Handle result = { (U64)mod }; + scratch_end(scratch); + return result; +} + +internal VoidProc* +os_library_load_proc(OS_Handle lib, String8 name) +{ + Temp scratch = scratch_begin(0, 0); + HMODULE mod = (HMODULE)lib.u64[0]; + name = push_str8_copy(scratch.arena, name); + VoidProc *result = (VoidProc*)GetProcAddress(mod, (LPCSTR)name.str); + scratch_end(scratch); + return result; +} + +internal void +os_library_close(OS_Handle lib) +{ + HMODULE mod = (HMODULE)lib.u64[0]; + FreeLibrary(mod); +} + +//////////////////////////////// +//~ dan: @os_hooks Safe Calls (Implemented Per-OS) + +internal void +os_safe_call(OS_ThreadFunctionType *func, OS_ThreadFunctionType *fail_handler, void *ptr) +{ + __try + { + func(ptr); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + if(fail_handler != 0) + { + fail_handler(ptr); + } + ExitProcess(1); + } +} + +//////////////////////////////// +//~ dan: @os_hooks GUIDs (Implemented Per-OS) + +internal Guid +os_make_guid(void) +{ + Guid result; MemoryZeroStruct(&result); + UUID uuid; + RPC_STATUS rpc_status = UuidCreate(&uuid); + if(rpc_status == RPC_S_OK) + { + result.data1 = uuid.Data1; + result.data2 = uuid.Data2; + result.data3 = uuid.Data3; + MemoryCopyArray(result.data4, uuid.Data4); + } + return result; +} + +//////////////////////////////// +//~ dan: @os_hooks Entry Points (Implemented Per-OS) + +#include +#undef OS_WINDOWS // shlwapi uses its own OS_WINDOWS include inside +#include + +internal B32 win32_g_is_quiet = 0; + +internal HRESULT WINAPI +win32_dialog_callback(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, LONG_PTR data) +{ + if(msg == TDN_HYPERLINK_CLICKED) + { + ShellExecuteW(NULL, L"open", (LPWSTR)lparam, NULL, NULL, SW_SHOWNORMAL); + } + return S_OK; +} + +internal LONG WINAPI +win32_exception_filter(EXCEPTION_POINTERS* exception_ptrs) +{ + if(win32_g_is_quiet) + { + ExitProcess(1); + } + + static volatile LONG first = 0; + if(InterlockedCompareExchange(&first, 1, 0) != 0) + { + // prevent failures in other threads to popup same message box + // this handler just shows first thread that crashes + // we are terminating afterwards anyway + for (;;) Sleep(1000); + } + + WCHAR buffer[4096] = {0}; + int buflen = 0; + + DWORD exception_code = exception_ptrs->ExceptionRecord->ExceptionCode; + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"A fatal exception (code 0x%x) occurred. The process is terminating.\n", exception_code); + + // load dbghelp dynamically just in case if it is missing + HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); + if(dbghelp) + { + DWORD (WINAPI *dbg_SymSetOptions)(DWORD SymOptions); + BOOL (WINAPI *dbg_SymInitializeW)(HANDLE hProcess, PCWSTR UserSearchPath, BOOL fInvadeProcess); + BOOL (WINAPI *dbg_StackWalk64)(DWORD MachineType, HANDLE hProcess, HANDLE hThread, + LPSTACKFRAME64 StackFrame, PVOID ContextRecord, PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, + PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, + PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress); + PVOID (WINAPI *dbg_SymFunctionTableAccess64)(HANDLE hProcess, DWORD64 AddrBase); + DWORD64 (WINAPI *dbg_SymGetModuleBase64)(HANDLE hProcess, DWORD64 qwAddr); + BOOL (WINAPI *dbg_SymFromAddrW)(HANDLE hProcess, DWORD64 Address, PDWORD64 Displacement, PSYMBOL_INFOW Symbol); + BOOL (WINAPI *dbg_SymGetLineFromAddrW64)(HANDLE hProcess, DWORD64 dwAddr, PDWORD pdwDisplacement, PIMAGEHLP_LINEW64 Line); + BOOL (WINAPI *dbg_SymGetModuleInfoW64)(HANDLE hProcess, DWORD64 qwAddr, PIMAGEHLP_MODULEW64 ModuleInfo); + + *(FARPROC*)&dbg_SymSetOptions = GetProcAddress(dbghelp, "SymSetOptions"); + *(FARPROC*)&dbg_SymInitializeW = GetProcAddress(dbghelp, "SymInitializeW"); + *(FARPROC*)&dbg_StackWalk64 = GetProcAddress(dbghelp, "StackWalk64"); + *(FARPROC*)&dbg_SymFunctionTableAccess64 = GetProcAddress(dbghelp, "SymFunctionTableAccess64"); + *(FARPROC*)&dbg_SymGetModuleBase64 = GetProcAddress(dbghelp, "SymGetModuleBase64"); + *(FARPROC*)&dbg_SymFromAddrW = GetProcAddress(dbghelp, "SymFromAddrW"); + *(FARPROC*)&dbg_SymGetLineFromAddrW64 = GetProcAddress(dbghelp, "SymGetLineFromAddrW64"); + *(FARPROC*)&dbg_SymGetModuleInfoW64 = GetProcAddress(dbghelp, "SymGetModuleInfoW64"); + + if(dbg_SymSetOptions && dbg_SymInitializeW && dbg_StackWalk64 && dbg_SymFunctionTableAccess64 && dbg_SymGetModuleBase64 && dbg_SymFromAddrW && dbg_SymGetLineFromAddrW64 && dbg_SymGetModuleInfoW64) + { + HANDLE process = GetCurrentProcess(); + HANDLE thread = GetCurrentThread(); + CONTEXT* context = exception_ptrs->ContextRecord; + + WCHAR module_path[MAX_PATH]; + GetModuleFileNameW(NULL, module_path, ArrayCount(module_path)); + PathRemoveFileSpecW(module_path); + + dbg_SymSetOptions(SYMOPT_EXACT_SYMBOLS | SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT_LOAD_LINES | SYMOPT_UNDNAME); + if(dbg_SymInitializeW(process, module_path, TRUE)) + { + // check that raddbg.pdb file is good + B32 raddbg_pdb_valid = 0; + { + IMAGEHLP_MODULEW64 module = {0}; + module.SizeOfStruct = sizeof(module); + if(dbg_SymGetModuleInfoW64(process, (DWORD64)&win32_exception_filter, &module)) + { + raddbg_pdb_valid = (module.SymType == SymPdb); + } + } + + if(!raddbg_pdb_valid) + { + buflen += wnsprintfW(buffer + buflen, sizeof(buffer) - buflen, + L"\nThe PDB debug information file for this executable is not valid or was not found. Please rebuild binary to get the call stack.\n"); + } + else + { + STACKFRAME64 frame = {0}; + DWORD image_type; +#if defined(_M_AMD64) + image_type = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = context->Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context->Rbp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context->Rsp; + frame.AddrStack.Mode = AddrModeFlat; +#elif defined(_M_ARM64) + image_type = IMAGE_FILE_MACHINE_ARM64; + frame.AddrPC.Offset = context->Pc; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context->Fp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context->Sp; + frame.AddrStack.Mode = AddrModeFlat; +#else +# error Arch not supported! +#endif + + for(U32 idx=0; ;idx++) + { + const U32 max_frames = 32; + if(idx == max_frames) + { + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"..."); + break; + } + + if(!dbg_StackWalk64(image_type, process, thread, &frame, context, 0, dbg_SymFunctionTableAccess64, dbg_SymGetModuleBase64, 0)) + { + break; + } + + U64 address = frame.AddrPC.Offset; + if(address == 0) + { + break; + } + + if(idx==0) + { +#if BUILD_CONSOLE_INTERFACE + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"\nCreate a new issue with this report at %S.\n\n", BUILD_ISSUES_LINK_STRING_LITERAL); +#else + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, + L"\nPress Ctrl+C to copy this text to clipboard, then create a new issue at\n" + L"%S\n\n", BUILD_ISSUES_LINK_STRING_LITERAL, BUILD_ISSUES_LINK_STRING_LITERAL); +#endif + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"Call stack:\n"); + } + + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"%u. [0x%I64x]", idx + 1, address); + + struct { + SYMBOL_INFOW info; + WCHAR name[MAX_SYM_NAME]; + } symbol = {0}; + + symbol.info.SizeOfStruct = sizeof(symbol.info); + symbol.info.MaxNameLen = MAX_SYM_NAME; + + DWORD64 displacement = 0; + if(dbg_SymFromAddrW(process, address, &displacement, &symbol.info)) + { + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L" %s +%u", symbol.info.Name, (DWORD)displacement); + + IMAGEHLP_LINEW64 line = {0}; + line.SizeOfStruct = sizeof(line); + + DWORD line_displacement = 0; + if(dbg_SymGetLineFromAddrW64(process, address, &line_displacement, &line)) + { + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L", %s line %u", PathFindFileNameW(line.FileName), line.LineNumber); + } + } + else + { + IMAGEHLP_MODULEW64 module = {0}; + module.SizeOfStruct = sizeof(module); + if(dbg_SymGetModuleInfoW64(process, address, &module)) + { + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L" %s", module.ModuleName); + } + } + + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"\n"); + } + } + } + } + } + + buflen += wnsprintfW(buffer + buflen, ArrayCount(buffer) - buflen, L"\nVersion: %S%S", BUILD_VERSION_STRING_LITERAL, BUILD_GIT_HASH_STRING_LITERAL_APPEND); + +#if BUILD_CONSOLE_INTERFACE + fwprintf(stderr, L"\n--- Fatal Exception ---\n"); + fwprintf(stderr, L"%s\n\n", buffer); +#else + TASKDIALOGCONFIG dialog = {0}; + dialog.cbSize = sizeof(dialog); + dialog.dwFlags = TDF_SIZE_TO_CONTENT | TDF_ENABLE_HYPERLINKS | TDF_ALLOW_DIALOG_CANCELLATION; + dialog.pszMainIcon = TD_ERROR_ICON; + dialog.dwCommonButtons = TDCBF_CLOSE_BUTTON; + dialog.pszWindowTitle = L"Fatal Exception"; + dialog.pszContent = buffer; + dialog.pfCallback = &win32_dialog_callback; + TaskDialogIndirect(&dialog, 0, 0, 0); +#endif + + ExitProcess(1); +} + +#undef OS_WINDOWS // shlwapi uses its own OS_WINDOWS include inside +#define OS_WINDOWS 1 + +internal void +w32_entry_point_caller(int argc, WCHAR **wargv) +{ + SetUnhandledExceptionFilter(&win32_exception_filter); + + //- rjf: dynamically load windows functions which are not guaranteed + // in all SDKs + { + HMODULE module = LoadLibraryA("kernel32.dll"); + w32_SetThreadDescription_func = (W32_SetThreadDescription_Type *)GetProcAddress(module, "SetThreadDescription"); + FreeLibrary(module); + } + + //- rjf: try to allow large pages if we can + B32 large_pages_allowed = 0; + { + HANDLE token; + if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) + { + LUID luid; + if(LookupPrivilegeValue(0, SE_LOCK_MEMORY_NAME, &luid)) + { + TOKEN_PRIVILEGES priv; + priv.PrivilegeCount = 1; + priv.Privileges[0].Luid = luid; + priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + large_pages_allowed = !!AdjustTokenPrivileges(token, 0, &priv, sizeof(priv), 0, 0); + } + CloseHandle(token); + } + } + + //- rjf: get system info + SYSTEM_INFO sysinfo = {0}; + GetSystemInfo(&sysinfo); + + //- rjf: set up non-dynamically-alloc'd state + // + // (we need to set up some basics before this layer can supply + // memory allocation primitives) + { + os_w32_state.microsecond_resolution = 1; + LARGE_INTEGER large_int_resolution; + if(QueryPerformanceFrequency(&large_int_resolution)) + { + os_w32_state.microsecond_resolution = large_int_resolution.QuadPart; + } + } + { + OS_SystemInfo *info = &os_w32_state.system_info; + info->logical_processor_count = (U64)sysinfo.dwNumberOfProcessors; + info->page_size = sysinfo.dwPageSize; + info->large_page_size = GetLargePageMinimum(); + info->allocation_granularity = sysinfo.dwAllocationGranularity; + } + { + OS_ProcessInfo *info = &os_w32_state.process_info; + info->large_pages_allowed = large_pages_allowed; + info->pid = GetCurrentProcessId(); + } + + //- rjf: extract arguments + Arena *args_arena = arena_alloc(.reserve_size = MB(1), .commit_size = KB(32)); + char **argv = push_array(args_arena, char *, argc); + for(int i = 0; i < argc; i += 1) + { + String16 arg16 = str16_cstring((U16 *)wargv[i]); + String8 arg8 = str8_from_16(args_arena, arg16); + if(str8_match(arg8, str8_lit("--quiet"), StringMatchFlag_CaseInsensitive) || + str8_match(arg8, str8_lit("-quiet"), StringMatchFlag_CaseInsensitive)) + { + win32_g_is_quiet = 1; + } + if(str8_match(arg8, str8_lit("--large_pages"), StringMatchFlag_CaseInsensitive) || + str8_match(arg8, str8_lit("-large_pages"), StringMatchFlag_CaseInsensitive)) + { + arena_default_flags = ArenaFlag_LargePages; + arena_default_reserve_size = Max(MB(64), os_w32_state.system_info.large_page_size); + arena_default_commit_size = arena_default_reserve_size; + } + argv[i] = (char *)arg8.str; + } + + //- rjf: set up thread context + local_persist TCTX tctx; + tctx_init_and_equip(&tctx); + + //- rjf: set up dynamically-alloc'd state + Arena *arena = arena_alloc(); + { + os_w32_state.arena = arena; + { + OS_SystemInfo *info = &os_w32_state.system_info; + U8 buffer[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD size = MAX_COMPUTERNAME_LENGTH + 1; + if(GetComputerNameA((char*)buffer, &size)) + { + info->machine_name = push_str8_copy(arena, str8(buffer, size)); + } + } + } + { + OS_ProcessInfo *info = &os_w32_state.process_info; + { + Temp scratch = scratch_begin(0, 0); + DWORD size = KB(32); + U16 *buffer = push_array_no_zero(scratch.arena, U16, size); + DWORD length = GetModuleFileNameW(0, (WCHAR*)buffer, size); + String8 name8 = str8_from_16(scratch.arena, str16(buffer, length)); + String8 name_chopped = str8_chop_last_slash(name8); + info->binary_path = push_str8_copy(arena, name_chopped); + scratch_end(scratch); + } + info->initial_path = os_get_current_path(arena); + { + Temp scratch = scratch_begin(0, 0); + U64 size = KB(32); + U16 *buffer = push_array_no_zero(scratch.arena, U16, size); + if(SUCCEEDED(SHGetFolderPathW(0, CSIDL_APPDATA, 0, 0, (WCHAR*)buffer))) + { + info->user_program_data_path = str8_from_16(arena, str16_cstring(buffer)); + } + scratch_end(scratch); + } + { + WCHAR *this_proc_env = GetEnvironmentStringsW(); + U64 start_idx = 0; + for(U64 idx = 0;; idx += 1) + { + if(this_proc_env[idx] == 0) + { + if(start_idx == idx) + { + break; + } + else + { + String16 string16 = str16((U16 *)this_proc_env + start_idx, idx - start_idx); + String8 string = str8_from_16(arena, string16); + str8_list_push(arena, &info->environment, string); + start_idx = idx+1; + } + } + } + } + } + + //- rjf: set up entity storage + InitializeCriticalSection(&os_w32_state.entity_mutex); + os_w32_state.entity_arena = arena_alloc(); + + //- rjf: call into "real" entry point + main_thread_base_entry_point(argc, argv); +} + +#if BUILD_CONSOLE_INTERFACE +int wmain(int argc, WCHAR **argv) +{ + w32_entry_point_caller(argc, argv); + return 0; +} +#else +int wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) +{ + w32_entry_point_caller(__argc, __wargv); + return 0; +} +#endif diff --git a/src/os/gfx/linux/os_gfx_linux.c b/src/os/gfx/linux/os_gfx_linux.c index 11da04b8a..b79f06d6e 100644 --- a/src/os/gfx/linux/os_gfx_linux.c +++ b/src/os/gfx/linux/os_gfx_linux.c @@ -1,500 +1,1811 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -//////////////////////////////// -//~ rjf: Helpers - -internal OS_LNX_Window * -os_lnx_window_from_x11window(Window window) -{ - OS_LNX_Window *result = 0; - for(OS_LNX_Window *w = os_lnx_gfx_state->first_window; w != 0; w = w->next) - { - if(w->window == window) - { - result = w; - break; - } - } - return result; -} - -//////////////////////////////// -//~ rjf: @os_hooks Main Initialization API (Implemented Per-OS) - -internal void -os_gfx_init(void) -{ - //- rjf: initialize basics - Arena *arena = arena_alloc(); - os_lnx_gfx_state = push_array(arena, OS_LNX_GfxState, 1); - os_lnx_gfx_state->arena = arena; - os_lnx_gfx_state->display = XOpenDisplay(0); - - //- rjf: calculate atoms - os_lnx_gfx_state->wm_delete_window_atom = XInternAtom(os_lnx_gfx_state->display, "WM_DELETE_WINDOW", 0); - os_lnx_gfx_state->wm_sync_request_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_SYNC_REQUEST", 0); - os_lnx_gfx_state->wm_sync_request_counter_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_SYNC_REQUEST_COUNTER", 0); - - //- rjf: fill out gfx info - os_lnx_gfx_state->gfx_info.double_click_time = 0.5f; - os_lnx_gfx_state->gfx_info.caret_blink_time = 0.5f; - os_lnx_gfx_state->gfx_info.default_refresh_rate = 60.f; -} - -//////////////////////////////// -//~ rjf: @os_hooks Graphics System Info (Implemented Per-OS) - -internal OS_GfxInfo * -os_get_gfx_info(void) -{ - return &os_lnx_gfx_state->gfx_info; -} - -//////////////////////////////// -//~ rjf: @os_hooks Clipboards (Implemented Per-OS) - -internal void -os_set_clipboard_text(String8 string) -{ - -} - -internal String8 -os_get_clipboard_text(Arena *arena) -{ - String8 result = {0}; - return result; -} - -//////////////////////////////// -//~ rjf: @os_hooks Windows (Implemented Per-OS) - -internal OS_Handle -os_window_open(Vec2F32 resolution, OS_WindowFlags flags, String8 title) -{ - //- rjf: allocate window - OS_LNX_Window *w = os_lnx_gfx_state->free_window; - if(w) - { - SLLStackPop(os_lnx_gfx_state->free_window); - } - else - { - w = push_array_no_zero(os_lnx_gfx_state->arena, OS_LNX_Window, 1); - } - MemoryZeroStruct(w); - DLLPushBack(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); - - //- rjf: create window & equip with x11 info - w->window = XCreateWindow(os_lnx_gfx_state->display, - XDefaultRootWindow(os_lnx_gfx_state->display), - 0, 0, resolution.x, resolution.y, - 0, - CopyFromParent, - InputOutput, - CopyFromParent, - 0, - 0); - XSelectInput(os_lnx_gfx_state->display, w->window, - ExposureMask| - PointerMotionMask| - ButtonPressMask| - ButtonReleaseMask| - KeyPressMask| - KeyReleaseMask| - FocusChangeMask); - Atom protocols[] = - { - os_lnx_gfx_state->wm_delete_window_atom, - os_lnx_gfx_state->wm_sync_request_atom, - }; - XSetWMProtocols(os_lnx_gfx_state->display, w->window, protocols, ArrayCount(protocols)); - { - XSyncValue initial_value; - XSyncIntToValue(&initial_value, 0); - w->counter_xid = XSyncCreateCounter(os_lnx_gfx_state->display, initial_value); - } - XChangeProperty(os_lnx_gfx_state->display, w->window, os_lnx_gfx_state->wm_sync_request_counter_atom, XA_CARDINAL, 32, PropModeReplace, (U8 *)&w->counter_xid, 1); - - //- rjf: attach name - Temp scratch = scratch_begin(0, 0); - String8 title_copy = push_str8_copy(scratch.arena, title); - XStoreName(os_lnx_gfx_state->display, w->window, (char *)title_copy.str); - scratch_end(scratch); - - //- rjf: convert to handle & return - OS_Handle handle = {(U64)w}; - return handle; -} - -internal void -os_window_close(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_first_paint(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} - OS_LNX_Window *w = (OS_LNX_Window *)handle.u64[0]; - XMapWindow(os_lnx_gfx_state->display, w->window); -} - -internal void -os_window_focus(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal B32 -os_window_is_focused(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return 0;} - return 0; -} - -internal B32 -os_window_is_fullscreen(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return 0;} - return 0; -} - -internal void -os_window_set_fullscreen(OS_Handle handle, B32 fullscreen) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal B32 -os_window_is_maximized(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return 0;} - return 0; -} - -internal void -os_window_set_maximized(OS_Handle handle, B32 maximized) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal B32 -os_window_is_minimized(OS_Handle window) -{ - if(os_handle_match(handle, os_handle_zero())) {return 0;} -} - -internal void -os_window_set_minimized(OS_Handle window, B32 minimized) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_bring_to_front(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_set_monitor(OS_Handle handle, OS_Handle monitor) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_clear_custom_border_data(OS_Handle handle) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_push_custom_title_bar(OS_Handle handle, F32 thickness) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_push_custom_edges(OS_Handle handle, F32 thickness) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal void -os_window_push_custom_title_bar_client_area(OS_Handle handle, Rng2F32 rect) -{ - if(os_handle_match(handle, os_handle_zero())) {return;} -} - -internal Rng2F32 -os_rect_from_window(OS_Handle handle) -{ - return r2f32p(0, 0, 0, 0); -} - -internal Rng2F32 -os_client_rect_from_window(OS_Handle handle) -{ - return r2f32p(0, 0, 0, 0); -} - -internal F32 -os_dpi_from_window(OS_Handle handle) -{ - return 0; -} - -//////////////////////////////// -//~ rjf: @os_hooks Monitors (Implemented Per-OS) - -internal OS_HandleArray -os_push_monitors_array(Arena *arena) -{ - OS_HandleArray result = {0}; - return result; -} - -internal OS_Handle -os_primary_monitor(void) -{ - OS_Handle result = {0}; - return result; -} - -internal OS_Handle -os_monitor_from_window(OS_Handle window) -{ - OS_Handle result = {0}; - return result; -} - -internal String8 -os_name_from_monitor(Arena *arena, OS_Handle monitor) -{ - return str8_zero(); -} - -internal Vec2F32 -os_dim_from_monitor(OS_Handle monitor) -{ - return v2f32(0, 0); -} - -//////////////////////////////// -//~ rjf: @os_hooks Events (Implemented Per-OS) - -internal void -os_send_wakeup_event(void) -{ - -} - -internal OS_EventList -os_get_events(Arena *arena, B32 wait) -{ - OS_EventList evts = {0}; - for(;XPending(os_lnx_gfx_state->display) > 0 || (wait && evts.count == 0);) - { - XEvent evt = {0}; - XNextEvent(os_lnx_gfx_state->display, &evt); - switch(evt.type) - { - default:{}break; - - //- rjf: key presses/releases - case KeyPress: - case KeyRelease: - { - // rjf: determine flags - OS_Modifiers flags = 0; - if(evt.xkey.state & ShiftMask) { flags |= OS_Modifier_Shift; } - if(evt.xkey.state & ControlMask) { flags |= OS_Modifier_Ctrl; } - if(evt.xkey.state & Mod1Mask) { flags |= OS_Modifier_Alt; } - - // rjf: map keycode -> keysym - U32 keysym = XLookupKeysym(&evt.xkey, 0); - - // rjf: map keysym -> OS_Key - OS_Key key = OS_Key_Null; - switch(keysym) - { - default: - { - if(0){} - else if(XK_F1 <= keysym && keysym <= XK_F24) { key = (OS_Key)(OS_Key_F1 + (keysym - XK_F1)); } - else if('0' <= keysym && keysym <= '9') { key = OS_Key_0 + (keysym-'0'); } - }break; - case XK_Escape:{key = OS_Key_Esc;};break; - case '-':{key = OS_Key_Minus;}break; - case '=':{key = OS_Key_Equal;}break; - case '[':{key = OS_Key_LeftBracket;}break; - case ']':{key = OS_Key_RightBracket;}break; - case ';':{key = OS_Key_Semicolon;}break; - case '\'':{key = OS_Key_Quote;}break; - case '.':{key = OS_Key_Period;}break; - case ',':{key = OS_Key_Comma;}break; - case '/':{key = OS_Key_Slash;}break; - case '\\':{key = OS_Key_BackSlash;}break; - case 'a':case 'A':{key = OS_Key_A;}break; - case 'b':case 'B':{key = OS_Key_B;}break; - case 'c':case 'C':{key = OS_Key_C;}break; - case 'd':case 'D':{key = OS_Key_D;}break; - case 'e':case 'E':{key = OS_Key_E;}break; - case 'f':case 'F':{key = OS_Key_F;}break; - case 'g':case 'G':{key = OS_Key_G;}break; - case 'h':case 'H':{key = OS_Key_H;}break; - case 'i':case 'I':{key = OS_Key_I;}break; - case 'j':case 'J':{key = OS_Key_J;}break; - case 'k':case 'K':{key = OS_Key_K;}break; - case 'l':case 'L':{key = OS_Key_L;}break; - case 'm':case 'M':{key = OS_Key_M;}break; - case 'n':case 'N':{key = OS_Key_N;}break; - case 'o':case 'O':{key = OS_Key_O;}break; - case 'p':case 'P':{key = OS_Key_P;}break; - case 'q':case 'Q':{key = OS_Key_Q;}break; - case 'r':case 'R':{key = OS_Key_R;}break; - case 's':case 'S':{key = OS_Key_S;}break; - case 't':case 'T':{key = OS_Key_T;}break; - case 'u':case 'U':{key = OS_Key_U;}break; - case 'v':case 'V':{key = OS_Key_V;}break; - case 'w':case 'W':{key = OS_Key_W;}break; - case 'x':case 'X':{key = OS_Key_X;}break; - case 'y':case 'Y':{key = OS_Key_Y;}break; - case 'z':case 'Z':{key = OS_Key_Z;}break; - case ' ':{key = OS_Key_Space;}break; - } - - // rjf: push event - OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); - OS_Event *e = os_event_list_push_new(arena, &evts, evt.type == KeyPress ? OS_EventKind_Press : OS_EventKind_Release); - e->window.u64[0] = (U64)window; - e->flags = flags; - e->key = key; - }break; - - //- rjf: mouse button presses/releases - case ButtonPress: - case ButtonRelease: - { - // rjf: determine flags - OS_Modifiers flags = 0; - if(evt.xbutton.state & ShiftMask) { flags |= OS_Modifier_Shift; } - if(evt.xbutton.state & ControlMask) { flags |= OS_Modifier_Ctrl; } - if(evt.xbutton.state & Mod1Mask) { flags |= OS_Modifier_Alt; } - - // rjf: map button -> OS_Key - OS_Key key = OS_Key_Null; - switch(evt.xbutton.button) - { - default:{}break; - case Button1:{key = OS_Key_LeftMouseButton;}break; - case Button2:{key = OS_Key_MiddleMouseButton;}break; - case Button3:{key = OS_Key_RightMouseButton;}break; - } - - // rjf: push event - OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); - OS_Event *e = os_event_list_push_new(arena, &evts, evt.type == ButtonPress ? OS_EventKind_Press : OS_EventKind_Release); - e->window.u64[0] = (U64)window; - e->flags = flags; - e->key = key; - }break; - - //- rjf: mouse motion - case MotionNotify: - { - OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); - OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_MouseMove); - e->window.u64[0] = (U64)window; - e->pos.x = (F32)evt.xmotion.x; - e->pos.y = (F32)evt.xmotion.y; - }break; - - //- rjf: window focus/unfocus - case FocusIn: - case FocusOut: - { - - }break; - - //- rjf: client messages - case ClientMessage: - { - if((Atom)evt.xclient.data.l[0] == os_lnx_gfx_state->wm_delete_window_atom) - { - OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); - OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_WindowClose); - e->window.u64[0] = (U64)window; - } - else if((Atom)evt.xclient.data.l[0] == os_lnx_gfx_state->wm_sync_request_atom) - { - OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); - if(window != 0) - { - window->counter_value = 0; - window->counter_value |= evt.xclient.data.l[2]; - window->counter_value |= (evt.xclient.data.l[3] << 32); - XSyncValue value; - XSyncIntToValue(&value, window->counter_value); - XSyncSetCounter(os_lnx_gfx_state->display, window->counter_xid, value); - } - } - }break; - } - } - return evts; -} - -internal OS_Modifiers -os_get_modifiers(void) -{ - return 0; -} - -internal B32 -os_key_is_down(OS_Key key) -{ - return 0; -} - -internal Vec2F32 -os_mouse_from_window(OS_Handle handle) -{ - return v2f32(0, 0); -} - -//////////////////////////////// -//~ rjf: @os_hooks Cursors (Implemented Per-OS) - -internal void -os_set_cursor(OS_Cursor cursor) -{ - -} - -//////////////////////////////// -//~ rjf: @os_hooks Native User-Facing Graphical Messages (Implemented Per-OS) - -internal void -os_graphical_message(B32 error, String8 title, String8 message) -{ - -} - -//////////////////////////////// -//~ rjf: @os_hooks Shell Operations - -internal void -os_show_in_filesystem_ui(String8 path) -{ - -} - -internal void -os_open_in_browser(String8 url) -{ - -} +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//////////////////////////////// +//~ dan: Helpers + + +internal OS_Key +os_lnx_keysym_to_oskey(KeySym keysym) +{ + OS_Key key = OS_Key_Null; + switch(keysym) + { + default: + { + if(0){} + else if(XK_F1 <= keysym && keysym <= XK_F24) { key = (OS_Key)(OS_Key_F1 + (keysym - XK_F1)); } + else if('0' <= keysym && keysym <= '9') { key = OS_Key_0 + (keysym-'0'); } + else if(XK_KP_0 <= keysym && keysym <= XK_KP_9) { key = (OS_Key)(OS_Key_Num0 + (keysym - XK_KP_0)); } + }break; + case XK_Escape:{key = OS_Key_Esc;};break; + case XK_minus:{key = OS_Key_Minus;}break; + case XK_equal:{key = OS_Key_Equal;}break; + case XK_bracketleft:{key = OS_Key_LeftBracket;}break; + case XK_bracketright:{key = OS_Key_RightBracket;}break; + case XK_semicolon:{key = OS_Key_Semicolon;}break; + case XK_apostrophe:{key = OS_Key_Quote;}break; + case XK_period:{key = OS_Key_Period;}break; + case XK_comma:{key = OS_Key_Comma;}break; + case XK_slash:{key = OS_Key_Slash;}break; + case XK_backslash:{key = OS_Key_BackSlash;}break; + case XK_grave:{key = OS_Key_Tick;}break; // Grave accent often maps to Tick + case XK_a:case XK_A:{key = OS_Key_A;}break; + case XK_b:case XK_B:{key = OS_Key_B;}break; + case XK_c:case XK_C:{key = OS_Key_C;}break; + case XK_d:case XK_D:{key = OS_Key_D;}break; + case XK_e:case XK_E:{key = OS_Key_E;}break; + case XK_f:case XK_F:{key = OS_Key_F;}break; + case XK_g:case XK_G:{key = OS_Key_G;}break; + case XK_h:case XK_H:{key = OS_Key_H;}break; + case XK_i:case XK_I:{key = OS_Key_I;}break; + case XK_j:case XK_J:{key = OS_Key_J;}break; + case XK_k:case XK_K:{key = OS_Key_K;}break; + case XK_l:case XK_L:{key = OS_Key_L;}break; + case XK_m:case XK_M:{key = OS_Key_M;}break; + case XK_n:case XK_N:{key = OS_Key_N;}break; + case XK_o:case XK_O:{key = OS_Key_O;}break; + case XK_p:case XK_P:{key = OS_Key_P;}break; + case XK_q:case XK_Q:{key = OS_Key_Q;}break; + case XK_r:case XK_R:{key = OS_Key_R;}break; + case XK_s:case XK_S:{key = OS_Key_S;}break; + case XK_t:case XK_T:{key = OS_Key_T;}break; + case XK_u:case XK_U:{key = OS_Key_U;}break; + case XK_v:case XK_V:{key = OS_Key_V;}break; + case XK_w:case XK_W:{key = OS_Key_W;}break; + case XK_x:case XK_X:{key = OS_Key_X;}break; + case XK_y:case XK_Y:{key = OS_Key_Y;}break; + case XK_z:case XK_Z:{key = OS_Key_Z;}break; + case XK_space:{key = OS_Key_Space;}break; + case XK_BackSpace:{key = OS_Key_Backspace;}break; + case XK_Tab: case XK_ISO_Left_Tab: {key = OS_Key_Tab;}break; // Handle shift+tab + case XK_Return: case XK_KP_Enter: {key = OS_Key_Return;}break; + case XK_Shift_L: case XK_Shift_R: {key = OS_Key_Shift;}break; + case XK_Control_L: case XK_Control_R: {key = OS_Key_Ctrl;}break; + case XK_Alt_L: case XK_Alt_R: {key = OS_Key_Alt;}break; + case XK_Menu: case XK_Super_L: case XK_Super_R: {key = OS_Key_Menu;}break; // Map Super/Windows key to Menu + case XK_Scroll_Lock: {key = OS_Key_ScrollLock;}break; + case XK_Pause: {key = OS_Key_Pause;}break; + case XK_Insert: case XK_KP_Insert: {key = OS_Key_Insert;}break; + case XK_Home: case XK_KP_Home: {key = OS_Key_Home;}break; + case XK_Page_Up: case XK_KP_Page_Up: {key = OS_Key_PageUp;}break; + case XK_Delete: case XK_KP_Delete: {key = OS_Key_Delete;}break; + case XK_End: case XK_KP_End: {key = OS_Key_End;}break; + case XK_Page_Down: case XK_KP_Page_Down: {key = OS_Key_PageDown;}break; + case XK_Up: case XK_KP_Up: {key = OS_Key_Up;}break; + case XK_Left: case XK_KP_Left: {key = OS_Key_Left;}break; + case XK_Down: case XK_KP_Down: {key = OS_Key_Down;}break; + case XK_Right: case XK_KP_Right: {key = OS_Key_Right;}break; + case XK_Num_Lock: {key = OS_Key_NumLock;}break; + case XK_KP_Divide: {key = OS_Key_NumSlash;}break; + case XK_KP_Multiply: {key = OS_Key_NumStar;}break; + case XK_KP_Subtract: {key = OS_Key_NumMinus;}break; + case XK_KP_Add: {key = OS_Key_NumPlus;}break; + case XK_KP_Decimal: {key = OS_Key_NumPeriod;}break; + case XK_Caps_Lock: {key = OS_Key_CapsLock;} break; + } + return key; +} + +internal OS_LNX_Window * +os_lnx_window_from_x11window(Window window) +{ + OS_LNX_Window *result = 0; + for(OS_LNX_Window *w = os_lnx_gfx_state->first_window; w != 0; w = w->next) + { + if(w->window == window) + { + result = w; + break; + } + } + return result; +} + +//////////////////////////////// +//~ dan: @os_hooks Main Initialization API (Implemented Per-OS) + +internal void +os_gfx_init(void) +{ + //- dan: initialize basics + Arena *arena = arena_alloc(); + os_lnx_gfx_state = push_array(arena, OS_LNX_GfxState, 1); + os_lnx_gfx_state->arena = arena; + + // Attempt to open a connection to the X server + Display *display = XOpenDisplay(NULL); + if (!display) { + // Handle the error properly - log it and terminate + const char *display_env = getenv("DISPLAY"); + fprintf(stderr, "ERROR: Failed to connect to X11 server. DISPLAY=%s\n", + display_env ? display_env : "(not set)"); + fprintf(stderr, "Make sure an X server is running and DISPLAY is set correctly.\n"); + exit(1); // Exit with error code - X11 is required + } + os_lnx_gfx_state->display = display; + os_lnx_gfx_state->clipboard_arena = arena_alloc(); // Initialize clipboard arena + + //- dan: calculate atoms + os_lnx_gfx_state->wm_delete_window_atom = XInternAtom(os_lnx_gfx_state->display, "WM_DELETE_WINDOW", 0); + os_lnx_gfx_state->wm_sync_request_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_SYNC_REQUEST", 0); + os_lnx_gfx_state->wm_sync_request_counter_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_SYNC_REQUEST_COUNTER", 0); + os_lnx_gfx_state->clipboard_atom = XInternAtom(os_lnx_gfx_state->display, "CLIPBOARD", 0); + os_lnx_gfx_state->targets_atom = XInternAtom(os_lnx_gfx_state->display, "TARGETS", 0); + os_lnx_gfx_state->utf8_string_atom = XInternAtom(os_lnx_gfx_state->display, "UTF8_STRING", 0); + os_lnx_gfx_state->wakeup_atom = XInternAtom(os_lnx_gfx_state->display, "_RADDBG_WAKEUP", 0); + os_lnx_gfx_state->clipboard_target_prop_atom = XInternAtom(os_lnx_gfx_state->display, "_RADDBG_CLIPBOARD_TARGET", 0); + + //- dan: calculate EWMH atoms for window state + os_lnx_gfx_state->net_wm_state_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_STATE", 0); + os_lnx_gfx_state->net_wm_state_maximized_vert_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_STATE_MAXIMIZED_VERT", 0); + os_lnx_gfx_state->net_wm_state_maximized_horz_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_STATE_MAXIMIZED_HORZ", 0); + os_lnx_gfx_state->net_wm_state_hidden_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_STATE_HIDDEN", 0); + os_lnx_gfx_state->net_wm_state_fullscreen_atom = XInternAtom(os_lnx_gfx_state->display, "_NET_WM_STATE_FULLSCREEN", 0); + + //- dan: create dedicated hidden window for clipboard operations + { + XSetWindowAttributes attributes; + attributes.override_redirect = True; // Prevent window manager interference + os_lnx_gfx_state->clipboard_window = XCreateWindow( + os_lnx_gfx_state->display, + XDefaultRootWindow(os_lnx_gfx_state->display), + -1, -1, 1, 1, // Position off-screen, minimal size + 0, CopyFromParent, InputOnly, CopyFromParent, + CWOverrideRedirect, + &attributes); + // We need to select for PropertyChangeMask to receive SelectionNotify events + // when the property we specified in XConvertSelection is updated. + XSelectInput(os_lnx_gfx_state->display, os_lnx_gfx_state->clipboard_window, PropertyChangeMask); + } + + //- dan: initialize clipboard cache + os_lnx_gfx_state->last_retrieved_clipboard_text = str8_lit(""); // Initial empty cache + + //- dan: fill out gfx info + os_lnx_gfx_state->gfx_info.double_click_time = 0.5f; + os_lnx_gfx_state->gfx_info.caret_blink_time = 0.5f; + os_lnx_gfx_state->gfx_info.default_refresh_rate = 60.f; + + //- dan: Populate OS_Key -> KeyCode mapping + MemoryZeroArray(os_lnx_gfx_state->keycode_from_oskey); + Display *d = os_lnx_gfx_state->display; + int min_keycode, max_keycode; + XDisplayKeycodes(d, &min_keycode, &max_keycode); + int keysyms_per_keycode = 0; + KeySym *keysyms = XGetKeyboardMapping(d, min_keycode, (max_keycode - min_keycode + 1), &keysyms_per_keycode); + if (keysyms != NULL) + { + for (int kc = min_keycode; kc <= max_keycode; ++kc) + { + // Check standard keysym (index 0) + KeySym ks = XKeycodeToKeysym(d, kc, 0); + if (ks == XK_Shift_L) { os_lnx_gfx_state->keycode_lshift = kc; } + if (ks == XK_Shift_R) { os_lnx_gfx_state->keycode_rshift = kc; } + if (ks == XK_Control_L) { os_lnx_gfx_state->keycode_lctrl = kc; } + if (ks == XK_Control_R) { os_lnx_gfx_state->keycode_rctrl = kc; } + if (ks == XK_Alt_L) { os_lnx_gfx_state->keycode_lalt = kc; } + if (ks == XK_Alt_R) { os_lnx_gfx_state->keycode_ralt = kc; } + if (ks != NoSymbol) + { + OS_Key oskey = os_lnx_keysym_to_oskey(ks); + if (oskey != OS_Key_Null && os_lnx_gfx_state->keycode_from_oskey[oskey] == 0) + { + // Prioritize non-modifier keys or left-side modifiers for the main table + if (oskey != OS_Key_Shift && oskey != OS_Key_Ctrl && oskey != OS_Key_Alt) { + os_lnx_gfx_state->keycode_from_oskey[oskey] = kc; + } else if (ks == XK_Shift_L || ks == XK_Control_L || ks == XK_Alt_L) { + os_lnx_gfx_state->keycode_from_oskey[oskey] = kc; + } + } + } + } + XFree(keysyms); + } + + //- dan: load cursors + os_lnx_gfx_state->cursors[OS_Cursor_Pointer] = XCreateFontCursor(os_lnx_gfx_state->display, XC_left_ptr); + os_lnx_gfx_state->cursors[OS_Cursor_IBar] = XCreateFontCursor(os_lnx_gfx_state->display, XC_xterm); + os_lnx_gfx_state->cursors[OS_Cursor_LeftRight] = XCreateFontCursor(os_lnx_gfx_state->display, XC_sb_h_double_arrow); + os_lnx_gfx_state->cursors[OS_Cursor_UpDown] = XCreateFontCursor(os_lnx_gfx_state->display, XC_sb_v_double_arrow); + os_lnx_gfx_state->cursors[OS_Cursor_DownRight] = XCreateFontCursor(os_lnx_gfx_state->display, XC_bottom_right_corner); + os_lnx_gfx_state->cursors[OS_Cursor_UpRight] = XCreateFontCursor(os_lnx_gfx_state->display, XC_top_right_corner); + os_lnx_gfx_state->cursors[OS_Cursor_UpDownLeftRight]= XCreateFontCursor(os_lnx_gfx_state->display, XC_fleur); + os_lnx_gfx_state->cursors[OS_Cursor_HandPoint] = XCreateFontCursor(os_lnx_gfx_state->display, XC_hand2); + os_lnx_gfx_state->cursors[OS_Cursor_Disabled] = XCreateFontCursor(os_lnx_gfx_state->display, XC_pirate); + os_set_cursor(OS_Cursor_Pointer); +} + +//////////////////////////////// +//~ dan: @os_hooks Graphics System Info (Implemented Per-OS) + +internal OS_GfxInfo * +os_get_gfx_info(void) +{ + return &os_lnx_gfx_state->gfx_info; +} + +//////////////////////////////// +//~ dan: @os_hooks Clipboards (Implemented Per-OS) + +internal void +os_set_clipboard_text(String8 string) +{ + Display *d = os_lnx_gfx_state->display; + // Arena clear/re-push might be needed if clipboard_buffer is reused often + arena_clear(os_lnx_gfx_state->clipboard_arena); // Use clipboard_arena + os_lnx_gfx_state->clipboard_buffer = push_str8_copy(os_lnx_gfx_state->clipboard_arena, string); + XSetSelectionOwner(d, os_lnx_gfx_state->clipboard_atom, os_lnx_gfx_state->clipboard_window, CurrentTime); + // Ensure the server processes the request + XFlush(d); +} + +internal String8 +os_get_clipboard_text(Arena *arena) +{ + Display *d = os_lnx_gfx_state->display; + Window owner = XGetSelectionOwner(d, os_lnx_gfx_state->clipboard_atom); + String8 result = {0}; // Initialize result + + if (owner == None) + { + // No owner, clear our cache if it wasn't already empty + if (os_lnx_gfx_state->last_retrieved_clipboard_text.size > 0) { + // Clear or manage the arena holding the cache if necessary + arena_clear(os_lnx_gfx_state->clipboard_arena); // Use clipboard_arena + os_lnx_gfx_state->last_retrieved_clipboard_text = str8_lit(""); + } + // Return empty string as there is no owner + result = str8_lit(""); + } + else if (owner == os_lnx_gfx_state->clipboard_window) + { + // We are the owner, return the buffer we set. + result = push_str8_copy(arena, os_lnx_gfx_state->clipboard_buffer); + } + else + { + // Request the selection data from the current owner. + // The result will be placed in the property 'clipboard_target_prop_atom' + // on our dedicated 'clipboard_window'. + // The 'os_get_events' loop will handle the PropertyNotify event when the data arrives. + XConvertSelection(d, + os_lnx_gfx_state->clipboard_atom, // selection atom (CLIPBOARD) + os_lnx_gfx_state->utf8_string_atom, // target format (UTF8_STRING) + os_lnx_gfx_state->clipboard_target_prop_atom, // property to store result on requestor + os_lnx_gfx_state->clipboard_window, // requestor window + CurrentTime); + // NOTE(perf): Cannot block here waiting for clipboard data. Must return cached value + // and handle PropertyNotify event asynchronously. + XFlush(d); + + // Immediately return the last successfully retrieved text from our cache. + // The cache will be updated asynchronously later in the event loop. + result = push_str8_copy(arena, os_lnx_gfx_state->last_retrieved_clipboard_text); + } + + return result; +} + +//////////////////////////////// +//~ dan: @os_hooks Windows (Implemented Per-OS) + +internal OS_Handle +os_window_open(Vec2F32 resolution, OS_WindowFlags flags, String8 title) +{ + //- dan: allocate window + OS_LNX_Window *w = os_lnx_gfx_state->free_window; + if(w) + { + SLLStackPop(os_lnx_gfx_state->free_window); + } + else + { + w = push_array_no_zero(os_lnx_gfx_state->arena, OS_LNX_Window, 1); + } + MemoryZeroStruct(w); + DLLPushBack(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + + //- dan: create window & equip with x11 info + w->window = XCreateWindow(os_lnx_gfx_state->display, + XDefaultRootWindow(os_lnx_gfx_state->display), + 0, 0, resolution.x, resolution.y, + 0, + CopyFromParent, + InputOutput, + CopyFromParent, + 0, + 0); + XSelectInput(os_lnx_gfx_state->display, w->window, + ExposureMask| + PointerMotionMask| + ButtonPressMask| + ButtonReleaseMask| + KeyPressMask| + KeyReleaseMask| + FocusChangeMask); + + //- dan: attach name + Temp scratch = scratch_begin(0, 0); + String8 title_copy = push_str8_copy(scratch.arena, title); + // XStoreName(os_lnx_gfx_state->display, w->window, (char *)title_copy.str); // MOVED + // scratch_end(scratch); // MOVED + + //--- START NEW GLX/GLEW INIT CODE --- + Display *d = os_lnx_gfx_state->display; + int default_screen = XDefaultScreen(d); + + // Define desired framebuffer attributes + static int visual_attribs[] = { + GLX_X_RENDERABLE, True, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, + GLX_ALPHA_SIZE, 8, + GLX_DEPTH_SIZE, 24, + GLX_STENCIL_SIZE, 8, + GLX_DOUBLEBUFFER, True, + //GLX_SAMPLE_BUFFERS , 1, // For MSAA + //GLX_SAMPLES , 4, // MSAA samples + None // Null terminate the list + }; + + // Choose FBConfig + int fbcount; + GLXFBConfig *fbc = glXChooseFBConfig(d, default_screen, visual_attribs, &fbcount); + if (!fbc || fbcount <= 0) { + fprintf(stderr, "Failed to retrieve a framebuffer config\n"); + XDestroyWindow(d, w->window); // Destroy the initial dummy window + // Handle error: maybe release 'w' back to free list? + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + SLLStackPush(os_lnx_gfx_state->free_window, w); + return os_handle_zero(); + } + // Select the best FBConfig (take the first one for simplicity) + GLXFBConfig best_fbc = fbc[0]; + XFree(fbc); // Free the list returned by glXChooseFBConfig + os_lnx_gfx_state->fb_config = best_fbc; // Store if needed globally + + // Get visual info from FBConfig + XVisualInfo *vi = glXGetVisualFromFBConfig(d, best_fbc); + if (!vi) { + fprintf(stderr, "Could not create visual window info\n"); + XDestroyWindow(d, w->window); // Destroy the initial dummy window + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + SLLStackPush(os_lnx_gfx_state->free_window, w); + return os_handle_zero(); + } + + // Destroy the initially created window (it had the wrong visual) + XDestroyWindow(d, w->window); + + // Create a colormap and window attributes + Colormap cmap = XCreateColormap(d, XRootWindow(d, vi->screen), vi->visual, AllocNone); + XSetWindowAttributes swa; + swa.colormap = cmap; + swa.background_pixmap = None; + swa.border_pixel = 0; + swa.event_mask = StructureNotifyMask | ExposureMask | KeyPressMask | KeyReleaseMask | + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask; + + // Re-create the window with the correct visual and attributes + w->window = XCreateWindow(d, + XRootWindow(d, vi->screen), + 0, 0, resolution.x, resolution.y, + 0, // border_width + vi->depth, + InputOutput, + vi->visual, + CWBorderPixel | CWColormap | CWEventMask, // Value mask + &swa); + if (!w->window) { + fprintf(stderr, "Failed to create window with chosen visual\n"); + XFreeColormap(d, cmap); + XFree(vi); + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + SLLStackPush(os_lnx_gfx_state->free_window, w); + return os_handle_zero(); + } + // Free the visual info structure + XFree(vi); + XFreeColormap(d, cmap); // Colormap is associated with window now + + // Set window title AFTER final window creation + XStoreName(os_lnx_gfx_state->display, w->window, (char *)title_copy.str); + scratch_end(scratch); + + // Create temporary legacy OpenGL context first for GLEW initialization + // We still need a temporary context to initialize GLEW and get the glXGetProcAddressARB function + GLXContext temp_ctx = glXCreateNewContext(d, best_fbc, GLX_RGBA_TYPE, 0, True); + if (!temp_ctx) { + fprintf(stderr, "Failed to create temporary GL context\\n"); + XDestroyWindow(d, w->window); + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + SLLStackPush(os_lnx_gfx_state->free_window, w); + return os_handle_zero(); + } + + // Make temporary context current + if (!glXMakeCurrent(d, w->window, temp_ctx)) { + fprintf(stderr, "Failed to make temporary context current\\n"); + glXDestroyContext(d, temp_ctx); + XDestroyWindow(d, w->window); + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + SLLStackPush(os_lnx_gfx_state->free_window, w); + return os_handle_zero(); + } + + fprintf(stderr, "Temporary OpenGL context created and made current\n"); + + // We now have a valid temporary context, but don't set has_valid_context yet + // as we want to initialize GLEW first + + // Initialize GLEW AFTER making the temporary context current + r_gl_init_glew_if_needed(); // Only initialize GLEW here, not resources! + + // Attempt to get the modern context creation function pointer + PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB = NULL; + glXCreateContextAttribsARB = (PFNGLXCREATECONTEXTATTRIBSARBPROC) + glXGetProcAddressARB((const GLubyte*)"glXCreateContextAttribsARB"); + + GLXContext final_ctx = NULL; + // Use the flag set by r_gl_init_extensions_if_needed + if(glXCreateContextAttribsARB) + { + int context_attribs[] = { + GLX_CONTEXT_MAJOR_VERSION_ARB, 4, // Request OpenGL 4.6 + GLX_CONTEXT_MINOR_VERSION_ARB, 6, + GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + // GLX_CONTEXT_FLAGS_ARB , GLX_CONTEXT_DEBUG_BIT_ARB, // Optional debug context + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, + None // Null terminate + }; + + // glXCreateContextAttribsARB is defined by glew.h if GLEW_ARB_create_context is defined + final_ctx = glXCreateContextAttribsARB(d, best_fbc, temp_ctx, True, context_attribs); + + if (!final_ctx) { + fprintf(stderr, "Failed to create GL 3.3 context using ARB (glXGetProcAddressARB succeeded), falling back to legacy context.\\n"); + final_ctx = temp_ctx; // Keep the temporary context + temp_ctx = NULL; // Don't destroy it later + } else { + // Successfully created 3.3 context, destroy the temporary one + glXMakeCurrent(d, None, NULL); // Make no context current before destroying temp + // glXDestroyContext(d, temp_ctx); // TEMP: Comment out to test resource sharing + glXDestroyContext(d, temp_ctx); // Destroy temporary context now that we have a working final context + temp_ctx = NULL; // Mark temp as destroyed + + // Make the new 3.3 context current + if (!glXMakeCurrent(d, w->window, final_ctx)) { + fprintf(stderr, "Failed to make final GL 3.3 context current\\n"); + glXDestroyContext(d, final_ctx); + XDestroyWindow(d, w->window); + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + SLLStackPush(os_lnx_gfx_state->free_window, w); + return os_handle_zero(); + } + + // Set the context as valid - THIS IS NEW + r_gl_set_has_valid_context(1); + + // Now that the final context is current, create all OpenGL resources + r_gl_create_global_resources(); + } + } + else + { + fprintf(stderr, "glXCreateContextAttribsARB not available. Using legacy context.\n"); + final_ctx = temp_ctx; // Keep the temporary context + temp_ctx = NULL; // Don't destroy it later + // The temporary context is already current + + // Set the context as valid - THIS IS NEW for legacy path + r_gl_set_has_valid_context(1); + + // REMOVED: Create OpenGL resources in the legacy context + // r_gl_create_global_resources(); + } + + // Store the final context (either the new 3.3 or the fallback legacy one) + w->gl_context = final_ctx; + + // ADDED: Log the final OpenGL version *after* the context is made current + if (glXGetCurrentContext() == final_ctx) { + const GLubyte* version = glGetString(GL_VERSION); + fprintf(stderr, ">>> Final OpenGL Context Version: %s\n", version ? (const char*)version : "NULL (Error getting version?)"); + } else { + fprintf(stderr, ">>> ERROR: Final context not current after creation!\n"); + } + + // Resume existing window setup logic + XSelectInput(os_lnx_gfx_state->display, w->window, + ExposureMask| + PointerMotionMask| + ButtonPressMask| + ButtonReleaseMask| + KeyPressMask| + KeyReleaseMask| + FocusChangeMask); + Atom protocols[] = + { + os_lnx_gfx_state->wm_delete_window_atom, + os_lnx_gfx_state->wm_sync_request_atom, + }; + XSetWMProtocols(os_lnx_gfx_state->display, w->window, protocols, ArrayCount(protocols)); + { + XSyncValue initial_value; + XSyncIntToValue(&initial_value, 0); + w->counter_xid = XSyncCreateCounter(os_lnx_gfx_state->display, initial_value); + } + XChangeProperty(os_lnx_gfx_state->display, w->window, os_lnx_gfx_state->wm_sync_request_counter_atom, XA_CARDINAL, 32, PropModeReplace, (U8 *)&w->counter_xid, 1); + + //- dan: convert to handle & return + OS_Handle handle = {(U64)w}; + return handle; +} + +internal void +os_window_close(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window *)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + + if (w != 0 && d != 0) + { + // Destroy the XSync counter associated with the window + if (w->counter_xid != None) // Check if counter was created + { + XSyncDestroyCounter(d, w->counter_xid); + if (XSyncDestroyCounter(d, w->counter_xid) == BadValue) { // Check return value + fprintf(stderr, "Warning: Failed to destroy XSync counter %lu.\n", (unsigned long)w->counter_xid); + } + w->counter_xid = None; // Mark as destroyed + } + + // Make context not current and destroy it + if (w->gl_context) { + // Check if this context is current before making null current + if (glXGetCurrentContext() == w->gl_context) { + glXMakeCurrent(d, None, NULL); + } + glXDestroyContext(d, w->gl_context); + w->gl_context = NULL; // Mark as destroyed + } + + // Destroy the X11 window + XDestroyWindow(d, w->window); + + // Remove from the active window list + DLLRemove(os_lnx_gfx_state->first_window, os_lnx_gfx_state->last_window, w); + + // Add to the free list + SLLStackPush(os_lnx_gfx_state->free_window, w); + + // Maybe flush needed? Depends if caller expects immediate effect. + // XFlush(d); + } +} + +internal void +os_window_first_paint(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window *)handle.u64[0]; + XMapWindow(os_lnx_gfx_state->display, w->window); +} + +internal void +os_window_focus(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + if (w != 0 && d != 0) + { + // Raise the window to the top + XRaiseWindow(d, w->window); + + // Set the input focus + // RevertToParent means focus goes to parent if window becomes unviewable. + XSetInputFocus(d, w->window, RevertToParent, CurrentTime); + + // Ensure the server processes the requests + XFlush(d); + } +} + +internal B32 +os_window_is_focused(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return 0;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + return w->has_focus; +} + +internal B32 +os_window_is_fullscreen(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return 0;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + Atom actual_type; + int actual_format; + unsigned long nitems; + unsigned long bytes_after; + Atom *props = 0; + B32 is_fullscreen = 0; + + // Check _NET_WM_STATE property for the _NET_WM_STATE_FULLSCREEN atom + int result = XGetWindowProperty(d, + w->window, + os_lnx_gfx_state->net_wm_state_atom, + 0, (~0L), // Read the whole property + False, // Don't delete + XA_ATOM, // Expected type is Atom + &actual_type, &actual_format, + &nitems, &bytes_after, (unsigned char **)&props); + + if (result == Success && props != 0) + { + for (unsigned long i = 0; i < nitems; i++) + { + if (props[i] == os_lnx_gfx_state->net_wm_state_fullscreen_atom) + { + is_fullscreen = 1; + break; + } + } + XFree(props); + } + return is_fullscreen; +} + +internal void +os_window_set_fullscreen(OS_Handle handle, B32 fullscreen) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + + // EWMH action: _NET_WM_STATE_REMOVE=0, _NET_WM_STATE_ADD=1 + long action = fullscreen ? 1 : 0; + + XEvent e = {0}; + e.type = ClientMessage; + e.xclient.window = w->window; + e.xclient.message_type = os_lnx_gfx_state->net_wm_state_atom; + e.xclient.format = 32; + e.xclient.data.l[0] = action; // Add or Remove state + e.xclient.data.l[1] = os_lnx_gfx_state->net_wm_state_fullscreen_atom; + e.xclient.data.l[2] = 0; // No second property + e.xclient.data.l[3] = 1; // Source indication: Normal Application + e.xclient.data.l[4] = 0; + + // Send the event to the root window, requesting the WM change the state + XSendEvent(d, root, False, SubstructureRedirectMask | SubstructureNotifyMask, &e); + XFlush(d); // Ensure the request is sent immediately +} + +internal B32 +os_window_is_maximized(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return 0;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + Atom actual_type; + int actual_format; + unsigned long nitems; + unsigned long bytes_after; + Atom *props = 0; + B32 is_maximized = 0; + + int result = XGetWindowProperty(d, + w->window, + os_lnx_gfx_state->net_wm_state_atom, + 0, (~0L), // Read the whole property + False, // Don't delete + XA_ATOM, // Expected type is Atom + &actual_type, &actual_format, + &nitems, &bytes_after, (unsigned char **)&props); + + if (result == Success && props != 0) + { + B32 found_vert = 0; + B32 found_horz = 0; + for (unsigned long i = 0; i < nitems; i++) + { + if (props[i] == os_lnx_gfx_state->net_wm_state_maximized_vert_atom) + { + found_vert = 1; + } + else if (props[i] == os_lnx_gfx_state->net_wm_state_maximized_horz_atom) + { + found_horz = 1; + } + } + is_maximized = found_vert && found_horz; + XFree(props); + } + + return is_maximized; +} + +internal void +os_window_set_maximized(OS_Handle handle, B32 maximized) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + + // Action: _NET_WM_STATE_REMOVE=0, _NET_WM_STATE_ADD=1, _NET_WM_STATE_TOGGLE=2 + long action = maximized ? 1 : 0; + + XEvent e = {0}; + e.type = ClientMessage; + e.xclient.window = w->window; + e.xclient.message_type = os_lnx_gfx_state->net_wm_state_atom; + e.xclient.format = 32; + e.xclient.data.l[0] = action; // Add or Remove state + e.xclient.data.l[1] = os_lnx_gfx_state->net_wm_state_maximized_horz_atom; + e.xclient.data.l[2] = os_lnx_gfx_state->net_wm_state_maximized_vert_atom; + e.xclient.data.l[3] = 0; // Source indication (0 for normal apps) + e.xclient.data.l[4] = 0; + + XSendEvent(d, root, False, SubstructureRedirectMask | SubstructureNotifyMask, &e); + XFlush(d); +} + +internal B32 +os_window_is_minimized(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return 0;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + Atom actual_type; + int actual_format; + unsigned long nitems; + unsigned long bytes_after; + Atom *props = 0; + B32 is_hidden = 0; + + int result = XGetWindowProperty(d, + w->window, + os_lnx_gfx_state->net_wm_state_atom, + 0, (~0L), + False, + XA_ATOM, + &actual_type, &actual_format, + &nitems, &bytes_after, (unsigned char **)&props); + + if (result == Success && props != 0) + { + for (unsigned long i = 0; i < nitems; i++) + { + if (props[i] == os_lnx_gfx_state->net_wm_state_hidden_atom) + { + is_hidden = 1; + break; + } + } + XFree(props); + } + // Additionally, check the classic WM_STATE IconicState for compatibility? + // Some older WMs might not fully support EWMH _NET_WM_STATE_HIDDEN. + // Atom wm_state = XInternAtom(d, "WM_STATE", False); + // ... query WM_STATE property ... check for IconicState ... + // For now, just rely on EWMH. + + return is_hidden; +} + +internal void +os_window_set_minimized(OS_Handle handle, B32 minimized) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + + if (minimized) + { + // Request the window manager to iconify (minimize) the window + XIconifyWindow(d, w->window, DefaultScreen(d)); + } + else + { + // Request the window manager to remove the hidden state (un-minimize) + // using EWMH _NET_WM_STATE message. + Window root = XDefaultRootWindow(d); + XEvent e = {0}; + e.type = ClientMessage; + e.xclient.window = w->window; + e.xclient.message_type = os_lnx_gfx_state->net_wm_state_atom; + e.xclient.format = 32; + e.xclient.data.l[0] = 0; // _NET_WM_STATE_REMOVE + e.xclient.data.l[1] = os_lnx_gfx_state->net_wm_state_hidden_atom; + e.xclient.data.l[2] = 0; // No second property + e.xclient.data.l[3] = 1; // Source indication: Normal Application + e.xclient.data.l[4] = 0; + + XSendEvent(d, root, False, SubstructureRedirectMask | SubstructureNotifyMask, &e); + + // Ensure the window is mapped and raised + XMapRaised(d, w->window); + } + XFlush(d); +} + +internal void +os_window_bring_to_front(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + if (w != 0 && d != 0) + { + XRaiseWindow(d, w->window); + XFlush(d); + } +} + +internal void +os_window_set_monitor(OS_Handle handle, OS_Handle monitor) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + RROutput output_id = (RROutput)monitor.u64[0]; + + if (w == 0 || d == 0 || output_id == None) { + return; + } + + // Get target monitor geometry + Rng2S32 monitor_rect = {0}; + int monitor_width = 0; + int monitor_height = 0; + XRRScreenResources *res = XRRGetScreenResourcesCurrent(d, root); + if (res) { + XRROutputInfo *output_info = XRRGetOutputInfo(d, res, output_id); + if (output_info && output_info->crtc != None) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(d, res, output_info->crtc); + if (crtc_info) { + monitor_rect = r2s32p(crtc_info->x, crtc_info->y, + crtc_info->x + crtc_info->width, + crtc_info->y + crtc_info->height); + monitor_width = crtc_info->width; + monitor_height = crtc_info->height; + XRRFreeCrtcInfo(crtc_info); + } + } + if (output_info) XRRFreeOutputInfo(output_info); + XRRFreeScreenResources(res); + } + + // Check if monitor info was successfully obtained + if (monitor_width <= 0 || monitor_height <= 0) { + return; // Failed to get valid monitor geometry + } + + // Get current window geometry (size only needed) + Window root_return; + int win_x_ignored, win_y_ignored; + unsigned int win_width, win_height, win_border_ignored, win_depth_ignored; + if (!XGetGeometry(d, w->window, &root_return, &win_x_ignored, &win_y_ignored, &win_width, &win_height, &win_border_ignored, &win_depth_ignored)) { + return; // Failed to get window geometry + } + + // Calculate centered position on the target monitor + S32 target_x = monitor_rect.x0 + (monitor_width / 2) - ((S32)win_width / 2); + S32 target_y = monitor_rect.y0 + (monitor_height / 2) - ((S32)win_height / 2); + + // Move the window + XMoveWindow(d, w->window, target_x, target_y); + XFlush(d); // Ensure the request is sent +} + +internal void +os_window_clear_custom_border_data(OS_Handle handle) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} +} + +internal void +os_window_push_custom_title_bar(OS_Handle handle, F32 thickness) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} +} + +internal void +os_window_push_custom_edges(OS_Handle handle, F32 thickness) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} +} + +internal void +os_window_push_custom_title_bar_client_area(OS_Handle handle, Rng2F32 rect) +{ + if(os_handle_match(handle, os_handle_zero())) {return;} +} + +internal Rng2F32 +os_rect_from_window(OS_Handle handle) +{ + Rng2F32 result = r2f32p(0, 0, 0, 0); + OS_LNX_Window *w = (OS_LNX_Window *)handle.u64[0]; + if (w != 0) + { + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + int x, y; + unsigned int width, height, border_width, depth; + Window child_return; + + // Get window geometry (width, height) + if (XGetGeometry(d, w->window, &root, &x, &y, &width, &height, &border_width, &depth)) + { + // Get window position relative to root + if (XTranslateCoordinates(d, w->window, root, 0, 0, &x, &y, &child_return)) + { + result.x0 = (F32)x; + result.y0 = (F32)y; + result.x1 = (F32)(x + width); + result.y1 = (F32)(y + height); + } + } + } + return result; +} + +internal Rng2F32 +os_client_rect_from_window(OS_Handle handle) +{ + Rng2F32 result = r2f32p(0, 0, 0, 0); + OS_LNX_Window *w = (OS_LNX_Window *)handle.u64[0]; + if (w != 0) + { + Display *d = os_lnx_gfx_state->display; + Window root_return; + int x_return, y_return; + unsigned int width, height, border_width_return, depth_return; + + if (XGetGeometry(d, w->window, &root_return, &x_return, &y_return, &width, &height, &border_width_return, &depth_return)) + { + // Client rect is always 0,0 to width, height relative to the window itself + result.x0 = 0; + result.y0 = 0; + result.x1 = (F32)width; + result.y1 = (F32)height; + } + } + return result; +} + +internal F32 +os_dpi_from_window(OS_Handle handle) +{ + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + if (w != 0 && w->dpi > 0.0f) // Check if DPI is already cached + { + return w->dpi; + } + + // Default DPI + F32 dpi = 96.0f; + Display *d = os_lnx_gfx_state->display; + char *resource_manager = XResourceManagerString(d); + Temp scratch = scratch_begin(0, 0); // Begin scratch arena + + if (resource_manager) + { + XrmDatabase db = XrmGetStringDatabase(resource_manager); + if (db) + { + char *type = NULL; + XrmValue value; + // Try to get Xft.dpi + if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value) == True) + { + if (value.addr && value.size > 0) + { + // Convert string value to float + // Use scratch arena for temporary allocation + char *str_dpi = push_array(scratch.arena, char, value.size + 1); + if(str_dpi) + { + MemoryCopy(str_dpi, value.addr, value.size); // Use MemoryCopy for safety + str_dpi[value.size] = '\0'; + dpi = atof(str_dpi); + // No MemoryFree needed, scratch arena handles cleanup + // Basic validation: DPI shouldn't be zero or negative + if (dpi <= 0) { dpi = 96.0f; } + } + } + } + else + { + // Fallback: Calculate DPI from screen dimensions if Xft.dpi is not found + int screen_num = DefaultScreen(d); + int width_px = DisplayWidth(d, screen_num); + int width_mm = DisplayWidthMM(d, screen_num); + if (width_mm > 0) + { + dpi = (F32)(width_px * 25.4 / width_mm); + if (dpi <= 0) { dpi = 96.0f; } // Sanity check + } + } + XrmDestroyDatabase(db); + } + } + else + { + // Fallback if no resource manager string: Calculate DPI from screen dimensions + int screen_num = DefaultScreen(d); + int width_px = DisplayWidth(d, screen_num); + int width_mm = DisplayWidthMM(d, screen_num); + if (width_mm > 0) + { + dpi = (F32)(width_px * 25.4 / width_mm); + if (dpi <= 0) { dpi = 96.0f; } // Sanity check + } + } + + scratch_end(scratch); // End scratch arena scope + + if (w != 0) // Cache the calculated DPI if window is valid + { + w->dpi = dpi; + } + + return dpi; +} + +//////////////////////////////// +//~ dan: @os_hooks Monitors (Implemented Per-OS) + +internal OS_HandleArray +os_push_monitors_array(Arena *arena) +{ + OS_HandleArray array = {0}; + OS_HandleList list = {0}; + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + + XRRScreenResources *res = XRRGetScreenResourcesCurrent(d, root); + if (res == NULL) + { + return array; // Failed to get resources + } + + for (int i = 0; i < res->noutput; i++) + { + XRROutputInfo *output_info = XRRGetOutputInfo(d, res, res->outputs[i]); + if (output_info == NULL) + { + continue; + } + + // Check if the output is connected and has an active CRTC (is an active monitor) + if (output_info->connection == RR_Connected && output_info->crtc != None) + { + // Consider this an active monitor + // Use the RROutput ID as the handle value + OS_Handle monitor_handle = {(U64)res->outputs[i]}; + os_handle_list_push(arena, &list, monitor_handle); + } + + XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(res); + + array = os_handle_array_from_list(arena, &list); + return array; +} + +internal OS_Handle +os_primary_monitor(void) +{ + OS_Handle result = {0}; + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + + // Use Xrandr to get the primary output + RROutput primary_output = XRRGetOutputPrimary(d, root); + + if (primary_output != None) + { + // Assign the RROutput ID to the handle + result.u64[0] = (U64)primary_output; + } + // else: primary_output is None, meaning no primary output is set or Xrandr failed. + // Return the zero handle in this case. + + return result; +} + +internal OS_Handle +os_monitor_from_window(OS_Handle window) +{ + OS_Handle result = {0}; + OS_LNX_Window *w = (OS_LNX_Window*)window.u64[0]; + Display *d = os_lnx_gfx_state->display; + + if (w == 0 || d == 0) { + return result; // Return zero handle if window or display is invalid + } + + Window root = XDefaultRootWindow(d); + int win_x, win_y; + unsigned int win_width, win_height, win_border, win_depth; + Window child_return; + + // 1. Get window geometry + if (!XGetGeometry(d, w->window, &root, &win_x, &win_y, &win_width, &win_height, &win_border, &win_depth)) { + return result; // Failed to get window geometry + } + // Translate coordinates relative to root + if (!XTranslateCoordinates(d, w->window, root, 0, 0, &win_x, &win_y, &child_return)) { + return result; // Failed to translate coordinates + } + Rng2S32 window_rect = r2s32p(win_x, win_y, win_x + win_width, win_y + win_height); + + // 2. Get monitor resources + XRRScreenResources *res = XRRGetScreenResourcesCurrent(d, root); + if (res == NULL) { + return result; // Failed to get screen resources + } + + RROutput best_output = None; + S64 max_intersection_area = -1; + + // 3. Iterate through monitors + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(d, res, res->outputs[i]); + if (output_info == NULL) { + continue; + } + + // Consider only connected outputs with an active CRTC + if (output_info->connection == RR_Connected && output_info->crtc != None) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(d, res, output_info->crtc); + if (crtc_info != NULL) { + // 4. Get monitor geometry + Rng2S32 monitor_rect = r2s32p(crtc_info->x, crtc_info->y, + crtc_info->x + crtc_info->width, + crtc_info->y + crtc_info->height); + + // 5. Calculate intersection area + Rng2S32 intersection = intersect_2s32(window_rect, monitor_rect); + S64 area = (S64)dim_2s32(intersection).x * (S64)dim_2s32(intersection).y; + + if (area > max_intersection_area) { + max_intersection_area = area; + best_output = res->outputs[i]; + } + XRRFreeCrtcInfo(crtc_info); + } + } + XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(res); + + // 6. Return handle of the best monitor + if (best_output != None) { + result.u64[0] = (U64)best_output; + } + // If no intersection found, or only disconnected monitors, result remains zero handle. + + return result; +} + +internal String8 +os_name_from_monitor(Arena *arena, OS_Handle monitor) +{ + String8 result = {0}; + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + RROutput output_id = (RROutput)monitor.u64[0]; + + if (output_id == None || d == NULL) { + return result; // Return zero string if handle or display is invalid + } + + XRRScreenResources *res = XRRGetScreenResourcesCurrent(d, root); + if (res == NULL) { + return result; // Failed to get screen resources + } + + XRROutputInfo *output_info = XRRGetOutputInfo(d, res, output_id); + if (output_info != NULL) + { + // Copy the name string into the arena + if (output_info->name != NULL && output_info->nameLen > 0) + { + result = push_str8_copy(arena, str8((U8*)output_info->name, output_info->nameLen)); + } + XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(res); + + return result; +} + +internal Vec2F32 +os_dim_from_monitor(OS_Handle monitor) +{ + Vec2F32 result = v2f32(0, 0); + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + RROutput output_id = (RROutput)monitor.u64[0]; + + if (output_id == None || d == NULL) { + return result; + } + + XRRScreenResources *res = XRRGetScreenResourcesCurrent(d, root); + if (res == NULL) { + return result; + } + + XRROutputInfo *output_info = XRRGetOutputInfo(d, res, output_id); + if (output_info == NULL || output_info->crtc == None) { + if (output_info) XRRFreeOutputInfo(output_info); + XRRFreeScreenResources(res); + return result; + } + + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(d, res, output_info->crtc); + if (crtc_info != NULL) + { + result.x = (F32)crtc_info->width; + result.y = (F32)crtc_info->height; + XRRFreeCrtcInfo(crtc_info); + } + + XRRFreeOutputInfo(output_info); + XRRFreeScreenResources(res); + + return result; +} + +//////////////////////////////// +//~ dan: @os_hooks Events (Implemented Per-OS) + +internal void +os_send_wakeup_event(void) +{ + Display *d = os_lnx_gfx_state->display; + Window root = XDefaultRootWindow(d); + XEvent event = {0}; + event.type = ClientMessage; + event.xclient.display = d; + event.xclient.window = root; // Sending to root, could send to specific window + event.xclient.message_type = os_lnx_gfx_state->wakeup_atom; + event.xclient.format = 32; // size of data items (l[0]...l[4]) in bits + // event.xclient.data.l[0..4] can contain custom data if needed + XSendEvent(d, root, False, NoEventMask, &event); + XFlush(d); +} + +internal OS_EventList +os_get_events(Arena *arena, B32 wait) +{ + OS_EventList evts = {0}; + Display *d = os_lnx_gfx_state->display; // Cache display pointer + + // Check for pending events or wait if requested + if(XPending(d) == 0 && wait) + { + // No events pending, and we need to wait. Use select() on the X connection FD. + int x11_fd = ConnectionNumber(d); + fd_set fds; + FD_ZERO(&fds); + FD_SET(x11_fd, &fds); + + // Select blocks until the FD is readable (meaning X event is available) + // No timeout specified, so it waits indefinitely like GetMessage. + int select_result = select(x11_fd + 1, &fds, NULL, NULL, NULL); + + if (select_result < 0 && errno != EINTR) { + // Handle select error (e.g., log it), but avoid busy-looping. + // For now, just return empty list on error. + perror("select"); + return evts; + } + // If select_result > 0 or == 0 (EINTR), we proceed to check XPending again below. + } + + // Process all currently pending events + while(XPending(d) > 0) + { + XEvent evt = {0}; + XNextEvent(d, &evt); + + // Add global modifier update here if needed, based on event state + // Example: os_lnx_gfx_state->modifiers = os_lnx_get_modifiers_from_state(evt.xkey.state or evt.xbutton.state); + + switch(evt.type) + { + default:{}break; + + //- dan: key presses/releases + case KeyPress: + case KeyRelease: + { + // dan: determine flags + OS_Modifiers flags = 0; + if(evt.xkey.state & ShiftMask) { flags |= OS_Modifier_Shift; } + if(evt.xkey.state & ControlMask) { flags |= OS_Modifier_Ctrl; } + if(evt.xkey.state & Mod1Mask) { flags |= OS_Modifier_Alt; } + + // dan: map keycode -> keysym & possibly text + KeySym keysym = NoSymbol; + char text_buffer[16]; // Buffer for translated characters + int nbytes = XLookupString(&evt.xkey, text_buffer, sizeof(text_buffer), &keysym, NULL); + + // dan: map keysym -> OS_Key + OS_Key key = os_lnx_keysym_to_oskey(keysym); + + // dan: push Press/Release event + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xkey.window); + if (key != OS_Key_Null) // Only push press/release for keys we map + { + OS_Event *press_release_evt = os_event_list_push_new(arena, &evts, evt.type == KeyPress ? OS_EventKind_Press : OS_EventKind_Release); + press_release_evt->window.u64[0] = (U64)window; + press_release_evt->flags = flags; + press_release_evt->key = key; + os_lnx_gfx_state->key_is_down[key] = (evt.type == KeyPress); + } + + // dan: push Text event if applicable (only on KeyPress) + if (evt.type == KeyPress && nbytes > 0) + { + // Filter out control characters except for common ones like tab, enter, backspace if desired + // For simplicity, let's push most things XLookupString gives us. + // A more robust implementation might check iscntrl() but allow specific control codes. + if (!(flags & OS_Modifier_Ctrl) && !(flags & OS_Modifier_Alt)) // Don't push text for Ctrl/Alt combinations usually + { + String8 text_str = str8((U8*)text_buffer, nbytes); + // Decode UTF-8 potentially received from XLookupString + UnicodeDecode decoded = utf8_decode(text_str.str, text_str.size); + U32 codepoint = decoded.codepoint; // Get codepoint directly + if (codepoint != 0 && codepoint != 127) // Skip null and delete + { + OS_Event *text_evt = os_event_list_push_new(arena, &evts, OS_EventKind_Text); + text_evt->window.u64[0] = (U64)window; + text_evt->flags = flags; // Modifiers might be relevant for text sometimes? + text_evt->character = codepoint; + } + } + } + }break; + + //- dan: mouse button presses/releases + case ButtonPress: + case ButtonRelease: + { + // dan: determine flags + OS_Modifiers flags = 0; + if(evt.xbutton.state & ShiftMask) { flags |= OS_Modifier_Shift; } + if(evt.xbutton.state & ControlMask) { flags |= OS_Modifier_Ctrl; } + if(evt.xbutton.state & Mod1Mask) { flags |= OS_Modifier_Alt; } + + // dan: map button -> OS_Key + OS_Key key = OS_Key_Null; + switch(evt.xbutton.button) + { + default:{}break; + case Button1:{key = OS_Key_LeftMouseButton;}break; + case Button2:{key = OS_Key_MiddleMouseButton;}break; + case Button3:{key = OS_Key_RightMouseButton;}break; + case Button4: // Scroll up + case Button5: // Scroll down + { + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xbutton.window); + OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_Scroll); + e->window.u64[0] = (U64)window; + e->flags = flags; + e->pos.x = (F32)evt.xbutton.x; + e->pos.y = (F32)evt.xbutton.y; + e->delta.y = (evt.xbutton.button == Button4) ? 120.f : -120.f; // Simulate standard wheel delta + } break; + } + + if (key != OS_Key_Null) + { + // dan: push event + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); + OS_Event *e = os_event_list_push_new(arena, &evts, evt.type == ButtonPress ? OS_EventKind_Press : OS_EventKind_Release); + e->window.u64[0] = (U64)window; + e->flags = flags; + e->key = key; + os_lnx_gfx_state->key_is_down[key] = (evt.type == ButtonPress); + } + }break; + + //- dan: mouse motion + case MotionNotify: + { + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); + OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_MouseMove); + e->window.u64[0] = (U64)window; + e->pos.x = (F32)evt.xmotion.x; + e->pos.y = (F32)evt.xmotion.y; + }break; + + //- dan: window focus/unfocus + case FocusIn: + case FocusOut: + { + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xfocus.window); + if (window) { + window->has_focus = (evt.type == FocusIn); + if (evt.type == FocusOut) { + OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_WindowLoseFocus); + e->window.u64[0] = (U64)window; + // Clear key state on focus lost? Maybe not necessary if we query directly. + } + } + }break; + + //- dan: client messages + case ClientMessage: + { + if((Atom)evt.xclient.data.l[0] == os_lnx_gfx_state->wm_delete_window_atom) + { + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); + OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_WindowClose); + e->window.u64[0] = (U64)window; + } + else if((Atom)evt.xclient.data.l[0] == os_lnx_gfx_state->wm_sync_request_atom) + { + OS_LNX_Window *window = os_lnx_window_from_x11window(evt.xclient.window); + if(window != 0) + { + window->counter_value = 0; + window->counter_value |= evt.xclient.data.l[2]; + window->counter_value |= (evt.xclient.data.l[3] << 32); + XSyncValue value; + XSyncIntToValue(&value, window->counter_value); + XSyncSetCounter(os_lnx_gfx_state->display, window->counter_xid, value); + } + } + else if (evt.xclient.message_type == os_lnx_gfx_state->wakeup_atom) + { + OS_Event *e = os_event_list_push_new(arena, &evts, OS_EventKind_Wakeup); + // Optional: copy data from event.xclient.data.l if needed + } + }break; + + //- dan: clipboard handling (SelectionRequest / PropertyNotify for getting data) + case SelectionRequest: + { + XSelectionRequestEvent *req = &evt.xselectionrequest; + XEvent response = {0}; + response.type = SelectionNotify; + response.xselection.display = req->display; + response.xselection.requestor = req->requestor; + response.xselection.selection = req->selection; + response.xselection.target = req->target; + response.xselection.property = None; // Default to None (failure) + response.xselection.time = req->time; + + // Only handle requests if we are the owner (using clipboard_window) + if (req->owner == os_lnx_gfx_state->clipboard_window && req->selection == os_lnx_gfx_state->clipboard_atom) + { + if (req->target == os_lnx_gfx_state->targets_atom) + { + // Respond with the list of supported targets + Atom supported_targets[] = { os_lnx_gfx_state->targets_atom, os_lnx_gfx_state->utf8_string_atom }; + XChangeProperty(d, req->requestor, req->property, XA_ATOM, 32, PropModeReplace, (unsigned char*)supported_targets, ArrayCount(supported_targets)); + response.xselection.property = req->property; // Indicate success + } + else if (req->target == os_lnx_gfx_state->utf8_string_atom) + { + // Respond with the actual clipboard data + XChangeProperty(d, req->requestor, req->property, os_lnx_gfx_state->utf8_string_atom, 8, // 8 bits per char for UTF8 + PropModeReplace, os_lnx_gfx_state->clipboard_buffer.str, os_lnx_gfx_state->clipboard_buffer.size); + response.xselection.property = req->property; // Indicate success + } + // Add other target formats here if needed (e.g., TARGETS) + } + // If we are not the owner, or target is unsupported, property remains None (failure) + + XSendEvent(d, req->requestor, False, NoEventMask, &response); + XFlush(d); + } break; + + // This handles the notification that clipboard data is ready after we called XConvertSelection + case PropertyNotify: + { + if (evt.xproperty.state == PropertyNewValue && + evt.xproperty.window == os_lnx_gfx_state->clipboard_window && + evt.xproperty.atom == os_lnx_gfx_state->clipboard_target_prop_atom) + { + Atom actual_type; + int actual_format; + unsigned long nitems; + unsigned long bytes_after; + unsigned char *data = 0; + int read_result = XGetWindowProperty(d, + os_lnx_gfx_state->clipboard_window, // Read from our dedicated window + os_lnx_gfx_state->clipboard_target_prop_atom, // The property we specified + 0, // Offset 0 + (~0L), // Read the whole property (~0L is common practice for large size) + True, // Delete the property after reading + AnyPropertyType, // Accept any type, but check actual_type later + &actual_type, &actual_format, &nitems, &bytes_after, &data); + + if (read_result == Success && data != 0) + { + if (actual_type == os_lnx_gfx_state->utf8_string_atom && actual_format == 8) + { + arena_clear(os_lnx_gfx_state->clipboard_arena); // Use clipboard_arena + os_lnx_gfx_state->last_retrieved_clipboard_text = push_str8_copy(os_lnx_gfx_state->clipboard_arena, str8(data, nitems)); // Use clipboard_arena + } + XFree(data); + } + } + } break; + } + } + return evts; +} + +internal OS_Modifiers +os_get_modifiers(void) +{ + OS_Modifiers mods = 0; + Display *d = os_lnx_gfx_state->display; + if (d == 0) { return mods; } // Return 0 if display is invalid + + char keys_return[32]; // XQueryKeymap returns 32 bytes + XQueryKeymap(d, keys_return); + + // Check Shift + KeyCode kc_l = os_lnx_gfx_state->keycode_lshift; + KeyCode kc_r = os_lnx_gfx_state->keycode_rshift; + if ((kc_l != 0 && (keys_return[kc_l / 8] & (1 << (kc_l % 8)))) || + (kc_r != 0 && (keys_return[kc_r / 8] & (1 << (kc_r % 8))))) + { + mods |= OS_Modifier_Shift; + } + + // Check Ctrl + kc_l = os_lnx_gfx_state->keycode_lctrl; + kc_r = os_lnx_gfx_state->keycode_rctrl; + if ((kc_l != 0 && (keys_return[kc_l / 8] & (1 << (kc_l % 8)))) || + (kc_r != 0 && (keys_return[kc_r / 8] & (1 << (kc_r % 8))))) + { + mods |= OS_Modifier_Ctrl; + } + + // Check Alt + kc_l = os_lnx_gfx_state->keycode_lalt; + kc_r = os_lnx_gfx_state->keycode_ralt; + if ((kc_l != 0 && (keys_return[kc_l / 8] & (1 << (kc_l % 8)))) || + (kc_r != 0 && (keys_return[kc_r / 8] & (1 << (kc_r % 8))))) + { + mods |= OS_Modifier_Alt; + } + + return mods; +} + +internal B32 +os_key_is_down(OS_Key key) +{ + if (key >= OS_Key_COUNT) + { + return 0; + } + + // dan: query real-time key state instead of relying on cached event state + Display *d = os_lnx_gfx_state->display; + if (d == 0) { // Check if display is valid + return 0; + } + KeyCode kc_left = 0, kc_right = 0; + + // Check if it's a modifier key and get specific left/right codes + if (key == OS_Key_Shift) + { + kc_left = os_lnx_gfx_state->keycode_lshift; + kc_right = os_lnx_gfx_state->keycode_rshift; + } + else if (key == OS_Key_Ctrl) + { + kc_left = os_lnx_gfx_state->keycode_lctrl; + kc_right = os_lnx_gfx_state->keycode_rctrl; + } + else if (key == OS_Key_Alt) + { + kc_left = os_lnx_gfx_state->keycode_lalt; + kc_right = os_lnx_gfx_state->keycode_ralt; + } + else + { + // Regular key, use the main table + kc_left = os_lnx_gfx_state->keycode_from_oskey[key]; + // kc_right remains 0 + } + + // Check if at least one keycode is valid + if (kc_left == 0 && kc_right == 0) + { + return 0; // Unmapped key + } + + char keys_return[32]; // XQueryKeymap returns 32 bytes + XQueryKeymap(d, keys_return); + + B32 is_down = 0; + // Check the bit corresponding to the left KeyCode + if (kc_left != 0 && (keys_return[kc_left / 8] & (1 << (kc_left % 8)))) + { + is_down = 1; + } + // Check the bit corresponding to the right KeyCode if applicable + if (!is_down && kc_right != 0 && (keys_return[kc_right / 8] & (1 << (kc_right % 8)))) + { + is_down = 1; + } + + return is_down; +} + +internal Vec2F32 +os_mouse_from_window(OS_Handle handle) +{ + Vec2F32 result = v2f32(0, 0); + OS_LNX_Window *w = (OS_LNX_Window*)handle.u64[0]; + if (w) + { + Window root_return, child_return; + int root_x_return, root_y_return, win_x_return, win_y_return; + unsigned int mask_return; + if (XQueryPointer(os_lnx_gfx_state->display, w->window, &root_return, &child_return, + &root_x_return, &root_y_return, &win_x_return, &win_y_return, &mask_return)) + { + result.x = (F32)win_x_return; + result.y = (F32)win_y_return; + } + } + return result; +} + +//////////////////////////////// +//~ dan: @os_hooks Cursors (Implemented Per-OS) + +internal void +os_set_cursor(OS_Cursor cursor) +{ + if (cursor < OS_Cursor_COUNT) + { + Cursor xcursor = os_lnx_gfx_state->cursors[cursor]; + if (xcursor != os_lnx_gfx_state->current_cursor) + { + os_lnx_gfx_state->current_cursor = xcursor; + // Apply to all windows, or just the focused one? Apply to all for now. + for(OS_LNX_Window *w = os_lnx_gfx_state->first_window; w != 0; w = w->next) + { + XDefineCursor(os_lnx_gfx_state->display, w->window, xcursor); + } + XFlush(os_lnx_gfx_state->display); + } + } +} + +//////////////////////////////// +//~ dan: @os_hooks Native User-Facing Graphical Messages (Implemented Per-OS) + +internal void +os_graphical_message(B32 error, String8 title, String8 message) +{ + Temp scratch = scratch_begin(0, 0); + String8List cmd_line = {0}; + + str8_list_push(scratch.arena, &cmd_line, str8_lit("zenity")); + if(error) + { + str8_list_push(scratch.arena, &cmd_line, str8_lit("--error")); + } + else + { + str8_list_push(scratch.arena, &cmd_line, str8_lit("--info")); + } + + // Format title and message arguments safely for the command line + String8 title_arg = push_str8f(scratch.arena, "--title=\"%S\"", title); + String8 text_arg = push_str8f(scratch.arena, "--text=\"%S\"", message); + + str8_list_push(scratch.arena, &cmd_line, title_arg); + str8_list_push(scratch.arena, &cmd_line, text_arg); + + // Launch the process + OS_ProcessLaunchParams params = {0}; + params.cmd_line = cmd_line; + params.inherit_env = 1; + params.consoleless = 1; // Don't need a console for zenity + + OS_Handle proc_handle = os_process_launch(¶ms); + + // We don't need to wait for the dialog, detach immediately. + if (!os_handle_match(proc_handle, os_handle_zero())) + { + os_process_detach(proc_handle); + } + scratch_end(scratch); +} + +//////////////////////////////// +//~ dan: @os_hooks Shell Operations + +internal void +os_show_in_filesystem_ui(String8 path) +{ + Temp scratch = scratch_begin(0, 0); + String8 path_copy = push_str8_copy(scratch.arena, path); + + // Get the parent directory of the path + String8 dir_path = str8_chop_last_slash(path_copy); + if (dir_path.size == 0) // Handle root or relative path in cwd + { + dir_path = str8_lit("."); + } + + // Construct the command line: xdg-open + String8List cmd_line = {0}; + str8_list_push(scratch.arena, &cmd_line, str8_lit("xdg-open")); + str8_list_push(scratch.arena, &cmd_line, dir_path); + + // Launch the process + OS_ProcessLaunchParams params = {0}; + params.cmd_line = cmd_line; + params.inherit_env = 1; + params.consoleless = 1; // Don't need a console for xdg-open + + OS_Handle proc_handle = os_process_launch(¶ms); + + // We don't need to wait for xdg-open, detach immediately. + if (!os_handle_match(proc_handle, os_handle_zero())) + { + os_process_detach(proc_handle); + } + scratch_end(scratch); +} + +internal void +os_open_in_browser(String8 url) +{ + Temp scratch = scratch_begin(0, 0); + // Construct the command line: xdg-open + String8List cmd_line = {0}; + str8_list_push(scratch.arena, &cmd_line, str8_lit("xdg-open")); + str8_list_push(scratch.arena, &cmd_line, url); // Pass URL directly + + // Launch the process + OS_ProcessLaunchParams params = {0}; + params.cmd_line = cmd_line; + params.inherit_env = 1; + params.consoleless = 1; // Don't need a console for xdg-open + + OS_Handle proc_handle = os_process_launch(¶ms); + + // We don't need to wait for xdg-open, detach immediately. + if (!os_handle_match(proc_handle, os_handle_zero())) + { + os_process_detach(proc_handle); + } + scratch_end(scratch); +} + diff --git a/src/os/gfx/linux/os_gfx_linux.h b/src/os/gfx/linux/os_gfx_linux.h index 4ac97ab30..7a6b28e98 100644 --- a/src/os/gfx/linux/os_gfx_linux.h +++ b/src/os/gfx/linux/os_gfx_linux.h @@ -1,56 +1,89 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -#ifndef OS_GFX_LINUX_H -#define OS_GFX_LINUX_H - -//////////////////////////////// -//~ rjf: Includes - -#include -#include -#include -#include -#include - -//////////////////////////////// -//~ rjf: Window State - -typedef struct OS_LNX_Window OS_LNX_Window; -struct OS_LNX_Window -{ - OS_LNX_Window *next; - OS_LNX_Window *prev; - Window window; - XID counter_xid; - U64 counter_value; -}; - -//////////////////////////////// -//~ rjf: State Bundle - -typedef struct OS_LNX_GfxState OS_LNX_GfxState; -struct OS_LNX_GfxState -{ - Arena *arena; - Display *display; - OS_LNX_Window *first_window; - OS_LNX_Window *last_window; - OS_LNX_Window *free_window; - Atom wm_delete_window_atom; - Atom wm_sync_request_atom; - Atom wm_sync_request_counter_atom; - OS_GfxInfo gfx_info; -}; - -//////////////////////////////// -//~ rjf: Globals - -global OS_LNX_GfxState *os_lnx_gfx_state = 0; - -//////////////////////////////// -//~ rjf: Helpers - -internal OS_LNX_Window *os_lnx_window_from_x11window(Window window); - -#endif // OS_GFX_LINUX_H +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +#ifndef OS_GFX_LINUX_H +#define OS_GFX_LINUX_H + +//////////////////////////////// +//~ dan: Includes + +// Corrected Order: GLEW first +#include // For OpenGL extensions +#include // For GLX types +#include // For GLX extensions + +// Add typedef for function pointer +typedef GLXContext (*PFNGLXCREATECONTEXTATTRIBSARBPROC)(Display*, GLXFBConfig, GLXContext, Bool, const int*); + +#include +#include +#include +#include +#include +#include +#include +#include +//////////////////////////////// +//~ dan: Window State + +typedef struct OS_LNX_Window OS_LNX_Window; +struct OS_LNX_Window +{ + OS_LNX_Window *next; + OS_LNX_Window *prev; + Window window; + XID counter_xid; + U64 counter_value; + B32 has_focus; + F32 dpi; + GLXContext gl_context; +}; + +//////////////////////////////// +//~ dan: State Bundle + +typedef struct OS_LNX_GfxState OS_LNX_GfxState; +struct OS_LNX_GfxState +{ + Arena *arena; + Display *display; + OS_LNX_Window *first_window; + OS_LNX_Window *last_window; + OS_LNX_Window *free_window; + Atom wm_delete_window_atom; + Atom wm_sync_request_atom; + Atom wm_sync_request_counter_atom; + Atom clipboard_atom; + Atom targets_atom; + Atom utf8_string_atom; + Atom wakeup_atom; + String8 clipboard_buffer; + Window clipboard_window; + Atom clipboard_target_prop_atom; + String8 last_retrieved_clipboard_text; + Arena *clipboard_arena; + B8 key_is_down[OS_Key_COUNT]; + KeyCode keycode_from_oskey[OS_Key_COUNT]; + KeyCode keycode_lshift; KeyCode keycode_rshift; + KeyCode keycode_lctrl; KeyCode keycode_rctrl; + KeyCode keycode_lalt; KeyCode keycode_ralt; + Cursor cursors[OS_Cursor_COUNT]; + Cursor current_cursor; + OS_GfxInfo gfx_info; + GLXFBConfig fb_config; + B32 glew_initialized; + + // EWMH Atoms + Atom net_wm_state_atom; + Atom net_wm_state_maximized_vert_atom; + Atom net_wm_state_maximized_horz_atom; + Atom net_wm_state_hidden_atom; + Atom net_wm_state_fullscreen_atom; +}; + +//////////////////////////////// +//~ dan: Globals + +global OS_LNX_GfxState *os_lnx_gfx_state = 0; + +#endif // OS_GFX_LINUX_H diff --git a/src/os/gfx/os_gfx.h b/src/os/gfx/os_gfx.h index f2a860d7c..8dd064b3c 100644 --- a/src/os/gfx/os_gfx.h +++ b/src/os/gfx/os_gfx.h @@ -91,6 +91,7 @@ struct OS_Event Vec2F32 pos; Vec2F32 delta; String8List strings; + U64 flags; }; typedef struct OS_EventList OS_EventList; diff --git a/src/render/opengl/generated/render_opengl.meta.c b/src/render/opengl/generated/render_opengl.meta.c new file mode 100644 index 000000000..6bfa08431 --- /dev/null +++ b/src/render/opengl/generated/render_opengl.meta.c @@ -0,0 +1,49 @@ +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//- GENERATED CODE + +C_LINKAGE_BEGIN +String8 * r_gl_g_vshad_kind_source_ptr_table[4] = +{ +&r_gl_g_rect_shader_vs_src, +&r_gl_g_blur_shader_vs_src, +&r_gl_g_mesh_shader_vs_src, +&r_gl_g_finalize_shader_vs_src, +}; + +String8 r_gl_g_vshad_kind_source_name_table[4] = +{ +str8_lit_comp("r_gl_g_rect_shader_vs_src"), +str8_lit_comp("r_gl_g_blur_shader_vs_src"), +str8_lit_comp("r_gl_g_mesh_shader_vs_src"), +str8_lit_comp("r_gl_g_finalize_shader_vs_src"), +}; + +String8 * r_gl_g_pshad_kind_source_ptr_table[5] = +{ +&r_gl_g_rect_shader_fs_src, +&r_gl_g_blur_shader_fs_src, +&r_gl_g_mesh_shader_fs_src, +&r_gl_g_geo3dcomposite_shader_fs_src, +&r_gl_g_finalize_shader_fs_src, +}; + +String8 r_gl_g_pshad_kind_source_name_table[5] = +{ +str8_lit_comp("r_gl_g_rect_shader_fs_src"), +str8_lit_comp("r_gl_g_blur_shader_fs_src"), +str8_lit_comp("r_gl_g_mesh_shader_fs_src"), +str8_lit_comp("r_gl_g_geo3dcomposite_shader_fs_src"), +str8_lit_comp("r_gl_g_finalize_shader_fs_src"), +}; + +U64 r_gl_g_uniform_type_kind_size_table[3] = +{ +sizeof(R_GL_Uniforms_Rect), +sizeof(R_GL_Uniforms_Blur), +sizeof(R_GL_Uniforms_Mesh), +}; + +C_LINKAGE_END + diff --git a/src/render/opengl/generated/render_opengl.meta.h b/src/render/opengl/generated/render_opengl.meta.h new file mode 100644 index 000000000..aca133b1c --- /dev/null +++ b/src/render/opengl/generated/render_opengl.meta.h @@ -0,0 +1,474 @@ +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//- GENERATED CODE + +#ifndef RENDER_OPENGL_META_H +#define RENDER_OPENGL_META_H + +typedef enum R_GL_VShadKind +{ +R_GL_VShadKind_Rect, +R_GL_VShadKind_Blur, +R_GL_VShadKind_Mesh, +R_GL_VShadKind_FullscreenQuad, +R_GL_VShadKind_COUNT, +} R_GL_VShadKind; + +typedef enum R_GL_PShadKind +{ +R_GL_PShadKind_Rect, +R_GL_PShadKind_Blur, +R_GL_PShadKind_Mesh, +R_GL_PShadKind_Geo3DComposite, +R_GL_PShadKind_Finalize, +R_GL_PShadKind_COUNT, +} R_GL_PShadKind; + +typedef enum R_GL_UniformTypeKind +{ +R_GL_UniformTypeKind_Rect, +R_GL_UniformTypeKind_Blur, +R_GL_UniformTypeKind_Mesh, +R_GL_UniformTypeKind_COUNT, +} R_GL_UniformTypeKind; + +C_LINKAGE_BEGIN +read_only global String8 r_gl_g_rect_shader_vs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"// Instance inputs matching VAO setup (8 vec4s)\n" +"layout(location = 0) in vec4 dst_rect_px_in; // {x0, y0, x1, y1}\n" +"layout(location = 1) in vec4 src_rect_px_in; // {x0, y0, x1, y1}\n" +"layout(location = 2) in vec4 color00_in; // {r, g, b, a} BL\n" +"layout(location = 3) in vec4 color01_in; // {r, g, b, a} TL\n" +"layout(location = 4) in vec4 color10_in; // {r, g, b, a} BR\n" +"layout(location = 5) in vec4 color11_in; // {r, g, b, a} TR\n" +"layout(location = 6) in vec4 corner_radii_px_in; // {bl, tl, br, tr} - CHECK ORDER vs D3D11\n" +"layout(location = 7) in vec4 style_params_in; // {border, soft, omit_tex, unused}\n" +"\n" +"// Uniforms (Unchanged)\n" +"layout (std140) uniform R_GL_Uniforms_Rect { // Binding Point 0\n" +" vec2 viewport_size_px;\n" +" float opacity;\n" +" mat4 texture_sample_channel_map;\n" +" vec2 texture_t2d_size_px;\n" +" mat3 xform; // Column-major\n" +" vec2 xform_scale;\n" +"};\n" +"\n" +"// Outputs to Fragment Shader (Matches D3D11 rect shader outputs)\n" +"out VS_OUT {\n" +" vec2 rect_half_size_px;\n" +" vec2 texcoord_pct;\n" +" vec2 sdf_sample_pos;\n" +" vec4 tint;\n" +" float corner_radius_px;\n" +" flat float border_thickness_px;\n" +" flat float softness_px;\n" +" flat float omit_texture;\n" +"} vs_out;\n" +"\n" +"void main() {\n" +" // Unpack instance data\n" +" vec2 dst_p0_px = dst_rect_px_in.xy;\n" +" vec2 dst_p1_px = dst_rect_px_in.zw;\n" +" vec2 src_p0_px = src_rect_px_in.xy;\n" +" vec2 src_p1_px = src_rect_px_in.zw;\n" +" vec2 dst_size_px = abs(dst_p1_px - dst_p0_px);\n" +"\n" +" float border_thickness_px = style_params_in.x;\n" +" float softness_px = style_params_in.y;\n" +" float omit_texture = style_params_in.z;\n" +"\n" +" // Generate standard CCW triangle strip quad vertex percentages (0,0), (1,0), (0,1), (1,1)\n" +" // gl_VertexID 0 -> (0,0) -> BL\n" +" // gl_VertexID 1 -> (1,0) -> BR\n" +" // gl_VertexID 2 -> (0,1) -> TL\n" +" // gl_VertexID 3 -> (1,1) -> TR\n" +" vec2 vertex_pct = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1));\n" +"\n" +" // Map vertex percentage to destination rect coordinates\n" +" vec2 current_dst_pos_px = mix(dst_p0_px, dst_p1_px, vertex_pct);\n" +"\n" +" // Calculate output position\n" +" vec3 xformed_pos = xform * vec3(current_dst_pos_px, 1.0);\n" +" vec2 ndc_pos = (xformed_pos.xy / viewport_size_px) * 2.0 - 1.0;\n" +" ndc_pos.y = -ndc_pos.y; // Flip Y for GL NDC\n" +" gl_Position = vec4(ndc_pos, 0.0, 1.0);\n" +"\n" +" // Map vertex percentage to source rect coordinates for texture coords\n" +" vec2 current_src_pos_px = mix(src_p0_px, src_p1_px, vertex_pct);\n" +" vs_out.texcoord_pct = current_src_pos_px / texture_t2d_size_px;\n" +"\n" +" // Select color based on vertex ID (matching D3D11 logic for vertex IDs 0..3 -> TL, BL, TR, BR)\n" +" vec4 color_bl = color00_in; // Location 2\n" +" vec4 color_br = color10_in; // Location 4\n" +" vec4 color_tl = color01_in; // Location 3\n" +" vec4 color_tr = color11_in; // Location 5\n" +" vec4 colors[4] = vec4[4](color_bl, color_br, color_tl, color_tr); // GLSL array constructor syntax\n" +" vs_out.tint = colors[gl_VertexID];\n" +"\n" +" // Select corner radius based on vertex ID (matching D3D11 logic)\n" +" // GL Vertex IDs 0, 1, 2, 3 correspond to BL, BR, TL, TR based on vertex_pct calc\n" +" // Input corner_radii_px_in (loc 6) is {bl, tl, br, tr} -> {x, y, z, w}\n" +" // D3D11 mapping (see thought process): ID 0 (BL)->x, ID 1 (BR)->z, ID 2 (TL)->y, ID 3 (TR)->w\n" +" float radii[4] = float[4](corner_radii_px_in.x, corner_radii_px_in.z, corner_radii_px_in.y, corner_radii_px_in.w); // GLSL array constructor syntax\n" +" vs_out.corner_radius_px = radii[gl_VertexID];\n" +"\n" +" // Calculate SDF sample position based on vertex percentage relative to center\n" +" vs_out.rect_half_size_px = dst_size_px / 2.0 * xform_scale;\n" +" vs_out.sdf_sample_pos = (2.0 * vertex_pct - 1.0) * vs_out.rect_half_size_px;\n" +"\n" +" // Pass through flat values\n" +" vs_out.border_thickness_px = border_thickness_px;\n" +" vs_out.softness_px = softness_px;\n" +" vs_out.omit_texture = omit_texture;\n" +"}\n" +"" +); + +read_only global String8 r_gl_g_rect_shader_fs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"in VS_OUT {\n" +" vec2 rect_half_size_px;\n" +" vec2 texcoord_pct;\n" +" vec2 sdf_sample_pos;\n" +" vec4 tint;\n" +" float corner_radius_px;\n" +" flat float border_thickness_px;\n" +" flat float softness_px;\n" +" flat float omit_texture;\n" +"} fs_in;\n" +"\n" +"layout (std140) uniform R_GL_Uniforms_Rect // Binding Point 0\n" +"{\n" +" vec2 viewport_size_px;\n" +" float opacity;\n" +" // float _padding0_;\n" +" mat4 texture_sample_channel_map;\n" +" vec2 texture_t2d_size_px;\n" +" // vec2 translate;\n" +" mat3 xform;\n" +" vec2 xform_scale;\n" +" // vec2 _padding1_;\n" +"};\n" +"\n" +"uniform sampler2D main_t2d; // Texture Unit 0\n" +"\n" +"out vec4 FragColor;\n" +"\n" +"// Corrected SDF function\n" +"float rect_sdf(vec2 sample_pos, vec2 rect_half_size, float r)\n" +"{\n" +" vec2 d = abs(sample_pos) - rect_half_size + r;\n" +" return length(max(d, 0.0)) - r;\n" +"}\n" +"\n" +"void main()\n" +"{\n" +" // Tint is already interpolated\n" +" vec4 tint = fs_in.tint;\n" +"\n" +" // Sample texture\n" +" vec4 albedo_sample = vec4(1.0);\n" +" if(fs_in.omit_texture < 0.5) // Use < 0.5 for bool comparison\n" +" {\n" +" // Assuming texture V is flipped if needed in VS\n" +" albedo_sample = texture(main_t2d, fs_in.texcoord_pct) * texture_sample_channel_map;\n" +" }\n" +"\n" +" // Determine SDF sample position (already interpolated)\n" +" vec2 sdf_sample_pos = fs_in.sdf_sample_pos; // Keep for potential future use\n" +"\n" +" // Sample for borders\n" +" float border_sdf_t = 1.0; // Default to 1 (no border effect)\n" +" float softness_px = max(fs_in.softness_px, 0.01); // Ensure softness is positive\n" +" float border_thickness_px = fs_in.border_thickness_px;\n" +" if(border_thickness_px > 0.0)\n" +" {\n" +" // Adjust half size by softness before calculating SDF, matching D3D11\n" +" vec2 inner_half_size = fs_in.rect_half_size_px - vec2(softness_px * 2.0) - border_thickness_px;\n" +" float inner_radius = max(fs_in.corner_radius_px - border_thickness_px, 0.0);\n" +" float border_sdf_s = rect_sdf(sdf_sample_pos, inner_half_size, inner_radius);\n" +"\n" +" float border_smooth_range = max(2.0 * softness_px, 1.0);\n" +" // Calculate coverage *outside* the inner edge (0 inside fill, ramps to 1 outside)\n" +" border_sdf_t = smoothstep(0.0, border_smooth_range, border_sdf_s);\n" +" }\n" +"\n" +" // Sample for corners (outer edge)\n" +" float corner_sdf_t = 1.0; // Default to 1 (no corner effect)\n" +" // Check radius OR softness > 0.75 (D3D11 logic)\n" +" if(fs_in.corner_radius_px > 0.0 || softness_px > 0.75)\n" +" {\n" +" // Adjust half size by softness before calculating SDF, matching D3D11\n" +" vec2 outer_half_size = fs_in.rect_half_size_px - vec2(softness_px * 2.0);\n" +" float outer_radius = max(fs_in.corner_radius_px, 0.0);\n" +" float corner_sdf_s = rect_sdf(sdf_sample_pos, outer_half_size, outer_radius);\n" +"\n" +" float corner_smooth_range = max(2.0 * softness_px, 1.0);\n" +" // Calculate coverage *inside* the outer edge (1 inside, ramps to 0 outside)\n" +" corner_sdf_t = 1.0 - smoothstep(0.0, corner_smooth_range, corner_sdf_s);\n" +" }\n" +"\n" +" // Form final color\n" +" FragColor = albedo_sample * tint;\n" +" FragColor.a *= opacity;\n" +" // Combine coverages - Base alpha on the outer shape coverage\n" +" FragColor.a *= corner_sdf_t; // Multiply by corner coverage\n" +" FragColor.a *= border_sdf_t; // Multiply by 1.0 (no effect)\n" +" // Final discard if alpha is near zero\n" +" if (FragColor.a < 0.001)\n" +" {\n" +" discard;\n" +" }\n" +"}\n" +"" +); + +read_only global String8 r_gl_g_blur_shader_vs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"// Define uniforms matching the FS for rect/corners\n" +"layout (std140) uniform BlurUniforms // Binding Point 1 \n" +"{\n" +" vec4 rect; // vec4(min.x, min.y, max.x, max.y) \n" +" vec4 corner_radii_px; // vec4(bl, tl, br, tr) \n" +" vec2 direction; // vec2(dx, dy) unit vector for blur pass \n" +" vec2 viewport_size; // vec2(width, height) of render target \n" +" uint blur_count; // Number of samples (adjusted for bilinear) \n" +" // padding... \n" +" vec4 kernel[32]; // vec4(weight, offset, unused, unused) \n" +"};\n" +"\n" +"// Define output block to pass SDF data to FS\n" +"out VS_OUT {\n" +" vec2 sdf_sample_pos;\n" +" flat vec2 rect_half_size;\n" +" float corner_radius;\n" +"} vs_out;\n" +"\n" +"out vec2 TexCoord;\n" +"\n" +"void main()\n" +"{\n" +" // Fullscreen quad vertices (-1,-1), (1,-1), (-1, 1), (1, 1) using triangle strip\n" +" // VertexID: 0 -> (-1,-1), 1 -> (1,-1), 2 -> (-1, 1), 3 -> (1, 1)\n" +" vec2 pos = vec2( (gl_VertexID << 1) - 1.0, (gl_VertexID & 1) * 2.0 - 1.0 );\n" +" gl_Position = vec4(pos, 0.0, 1.0);\n" +" \n" +" // Texcoord (0,0) bottom-left to (1,1) top-right \n" +" TexCoord = pos * 0.5 + 0.5; \n" +" // TexCoord.y = 1.0 - TexCoord.y; // Y is already correct for fullscreen quad\n" +"\n" +" // Calculate SDF inputs\n" +" vs_out.rect_half_size = (rect.zw - rect.xy) * 0.5;\n" +" \n" +" // Determine cornercoords based on vertex ID (0: BL, 1: BR, 2: TL, 3: TR)\n" +" vec2 cornercoords_pct = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1)); // Renamed from cornercoords__pct\n" +" vs_out.sdf_sample_pos = (2.0 * cornercoords_pct - 1.0) * vs_out.rect_half_size;\n" +"\n" +" // Select corner radius based on vertex ID\n" +" // ID 0 (BL) -> radii.x\n" +" // ID 1 (BR) -> radii.z\n" +" // ID 2 (TL) -> radii.y\n" +" // ID 3 (TR) -> radii.w\n" +" if(gl_VertexID == 0) vs_out.corner_radius = corner_radii_px.x;\n" +" else if(gl_VertexID == 1) vs_out.corner_radius = corner_radii_px.z;\n" +" else if(gl_VertexID == 2) vs_out.corner_radius = corner_radii_px.y;\n" +" else /* ID 3 */ vs_out.corner_radius = corner_radii_px.w;\n" +"\n" +" // D3D11 vertex shader passes rect_half_size - 2.0. Match this.\n" +" vs_out.rect_half_size -= 2.0; \n" +"}\n" +"" +); + +read_only global String8 r_gl_g_blur_shader_fs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"in vec2 TexCoord;\n" +"// Added input block matching the VS structure (even if VS isn't updated yet)\n" +"in VS_OUT {\n" +" vec2 sdf_sample_pos;\n" +" flat vec2 rect_half_size;\n" +" float corner_radius;\n" +"} fs_in;\n" +"out vec4 FragColor;\n" +"\n" +"uniform sampler2D main_t2d; // Texture Unit 0 \n" +"\n" +"layout (std140) uniform BlurUniforms // Binding Point 1 \n" +"{\n" +" vec4 rect; // vec4(min.x, min.y, max.x, max.y) \n" +" vec4 corner_radii_px; // vec4(bl, tl, br, tr) \n" +" vec2 direction; // vec2(dx, dy) unit vector for blur pass \n" +" vec2 viewport_size; // vec2(width, height) of render target \n" +" uint blur_count; // Number of samples (adjusted for bilinear) \n" +" // padding... \n" +" vec4 kernel[32]; // vec4(weight, offset, unused, unused) \n" +"};\n" +"\n" +"// SDF for rounded rectangle \n" +"float rect_sdf(vec2 sample_pos, vec2 rect_half_size, float r)\n" +"{\n" +" vec2 d = abs(sample_pos) - rect_half_size + r;\n" +" return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r;\n" +"}\n" +"\n" +"void main()\n" +"{\n" +" vec4 sum = vec4(0.0);\n" +" vec2 uv = TexCoord;\n" +" vec2 texelSize = 1.0 / viewport_size;\n" +" \n" +" // Apply first weight (at offset 0)\n" +" sum += texture(main_t2d, uv) * kernel[0].x;\n" +" \n" +" // Apply remaining weights using bilinear optimization\n" +" for(uint i = 1u; i < blur_count; ++i)\n" +" {\n" +" vec2 offset = direction * kernel[i].y * texelSize;\n" +" sum += texture(main_t2d, uv + offset) * kernel[i].x;\n" +" sum += texture(main_t2d, uv - offset) * kernel[i].x;\n" +" }\n" +" \n" +" // Clip to rounded rectangle using SDF\n" +" float corner_sdf_s = rect_sdf(fs_in.sdf_sample_pos, fs_in.rect_half_size, fs_in.corner_radius);\n" +" // D3D11 uses smoothstep(0, 2*softness, ...), let's use a small fixed softness for now\n" +" float softness = 1.0; \n" +" float corner_sdf_t = 1.0 - smoothstep(-softness, softness, corner_sdf_s);\n" +"\n" +" // Discard fragments outside the rounded rectangle (similar to D3D11)\n" +" if (corner_sdf_t < 0.001) // Use a small threshold instead of 0.9 check\n" +" {\n" +" discard;\n" +" }\n" +"\n" +" FragColor = vec4(sum.rgb, 1.0); // Output color, force alpha to 1\n" +"\n" +" // Optional: Apply coverage to alpha if needed later\n" +" // FragColor.a = corner_sdf_t; \n" +"}\n" +"" +); + +read_only global String8 r_gl_g_mesh_shader_vs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"layout (location = 0) in vec3 aPos;\n" +"layout (location = 1) in vec3 aNormal;\n" +"layout (location = 2) in vec2 aTexCoord;\n" +"layout (location = 3) in vec3 aColor;\n" +"layout (location = 4) in mat4 instance_xform; // Per-instance transform \n" +"\n" +"layout (std140) uniform R_GL_Uniforms_Mesh { // Use correct uniform block name matching C struct\n" +" mat4 view_proj_matrix; // Combined view * projection \n" +"};\n" +"\n" +"out vec3 FragPos;\n" +"out vec3 Normal;\n" +"out vec2 TexCoords;\n" +"out vec3 VertexColor;\n" +"\n" +"void main()\n" +"{\n" +" vec4 worldPos = instance_xform * vec4(aPos, 1.0);\n" +" gl_Position = view_proj_matrix * worldPos;\n" +" FragPos = vec3(worldPos);\n" +" Normal = mat3(transpose(inverse(instance_xform))) * aNormal; // Correct normal transformation\n" +" TexCoords = aTexCoord;\n" +" VertexColor = aColor;\n" +"}\n" +"" +); + +read_only global String8 r_gl_g_mesh_shader_fs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"out vec4 FragColor;\n" +"\n" +"in vec3 FragPos;\n" +"in vec3 Normal;\n" +"in vec2 TexCoords;\n" +"in vec3 VertexColor;\n" +"\n" +"uniform sampler2D main_t2d; // Texture Unit 0 \n" +"\n" +"void main()\n" +"{\n" +" // Simple: Output vertex color modulated by texture (if available) \n" +" // vec4 texColor = texture(main_t2d, TexCoords); \n" +" FragColor = vec4(VertexColor, 1.0); // * texColor; // Modulate later if needed \n" +" // Add lighting calculations here if needed \n" +"}\n" +"" +); + +read_only global String8 r_gl_g_geo3dcomposite_shader_fs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"in vec2 TexCoord;\n" +"uniform sampler2D main_t2d; // Renamed from stage_t2d \n" +"out vec4 FragColor;\n" +"void main()\n" +"{\n" +" FragColor = texture(main_t2d, TexCoord); // Use renamed uniform\n" +"}\n" +"" +); + +read_only global String8 r_gl_g_finalize_shader_vs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"out vec2 TexCoord;\n" +"void main()\n" +"{\n" +" // Correct way for (-1,-1), (1,-1), (-1, 1), (1, 1) triangle strip \n" +" vec2 pos = vec2( (gl_VertexID & 1) * 2.0 - 1.0, (gl_VertexID & 2) - 1.0 ); \n" +" gl_Position = vec4(pos, 0.0, 1.0);\n" +" // Texcoord (0,0) bottom-left to (1,1) top-right \n" +" TexCoord = pos * 0.5 + 0.5;\n" +" // TexCoord.y = 1.0 - TexCoord.y; // Flip V if input texture requires it \n" +"}\n" +"" +); + +read_only global String8 r_gl_g_finalize_shader_fs_src = +str8_lit_comp( +"" +"\n" +"#version 330 core\n" +"in vec2 TexCoord;\n" +"uniform sampler2D main_t2d; // Renamed from stage_t2d \n" +"out vec4 FragColor;\n" +"void main()\n" +"{\n" +" FragColor = texture(main_t2d, TexCoord); // Use renamed uniform\n" +" FragColor.a = 1.0; // Force alpha to 1 for final output \n" +"}\n" +"" +); + + +C_LINKAGE_END + +#endif // RENDER_OPENGL_META_H diff --git a/src/render/opengl/render_opengl.c b/src/render/opengl/render_opengl.c new file mode 100644 index 000000000..28364a600 --- /dev/null +++ b/src/render/opengl/render_opengl.c @@ -0,0 +1,2633 @@ +//////////////////////////////// +//~ dan: Generated Code + +#include "generated/render_opengl.meta.c" + + +//////////////////////////////// +//~ dan: Helper function for rounding a float to an integer +internal S32 +round_f32_s32(F32 v) +{ + return (S32)round_f32(v); +} + +//////////////////////////////// +//~ dan: Helper for initializing 4x4 matrices with 16 values +internal Mat4x4F32 +m4x4f32(F32 m00, F32 m01, F32 m02, F32 m03, + F32 m10, F32 m11, F32 m12, F32 m13, + F32 m20, F32 m21, F32 m22, F32 m23, + F32 m30, F32 m31, F32 m32, F32 m33) +{ + Mat4x4F32 result = { + {{m00, m01, m02, m03}, + {m10, m11, m12, m13}, + {m20, m21, m22, m23}, + {m30, m31, m32, m33}} + }; + return result; +} + +//////////////////////////////// + +//////////////////////////////// +//~ dan: Helpers + +internal R_GL_Window * +r_gl_window_from_handle(R_Handle handle) +{ + // Simple check: lowest bit is 1 for windows + if((handle.u64[0] & 1) == 1) + { + // Ensure the pointer part is non-NULL before dereferencing potentially + R_GL_Window* potential_window = (R_GL_Window *)(handle.u64[0] & ~0x3ull); + if (potential_window != 0 && potential_window->next != potential_window) // Check against self-referencing nil handle + { + return potential_window; + } + } + return &r_gl_window_nil; +} + +internal R_Handle +r_gl_handle_from_window(R_GL_Window *window) +{ + R_Handle result = {0}; + if (window != 0 && window != &r_gl_window_nil) + { + result.u64[0] = (U64)window | 0x1; + } + return result; +} + +internal R_GL_Tex2D * +r_gl_tex2d_from_handle(R_Handle handle) +{ + // Simple check: lowest two bits are 10 for textures + if((handle.u64[0] & 0x3) == 0x2) + { + R_GL_Tex2D* potential_tex = (R_GL_Tex2D *)(handle.u64[0] & ~0x3ull); + if (potential_tex != 0 && potential_tex->next != potential_tex) // Check against self-referencing nil handle + { + return potential_tex; + } + } + return &r_gl_tex2d_nil; +} + +internal R_Handle +r_gl_handle_from_tex2d(R_GL_Tex2D *texture) +{ + R_Handle result = {0}; + if (texture != 0 && texture != &r_gl_tex2d_nil) + { + result.u64[0] = (U64)texture | 0x2; + } + return result; +} + +internal R_GL_Buffer * +r_gl_buffer_from_handle(R_Handle handle) +{ + // Simple check: lowest two bits are 11 for buffers + if((handle.u64[0] & 0x3) == 0x3) + { + R_GL_Buffer* potential_buf = (R_GL_Buffer *)(handle.u64[0] & ~0x3ull); + if (potential_buf != 0 && potential_buf->next != potential_buf) // Check against self-referencing nil handle + { + return potential_buf; + } + } + return &r_gl_buffer_nil; +} + +internal R_Handle +r_gl_handle_from_buffer(R_GL_Buffer *buffer) +{ + R_Handle result = {0}; + if (buffer != 0 && buffer != &r_gl_buffer_nil) + { + result.u64[0] = (U64)buffer | 0x3; + } + return result; +} + +internal GLenum +r_gl_usage_from_resource_kind(R_ResourceKind kind) +{ + GLenum result = GL_STATIC_DRAW; + switch(kind) + { + case R_ResourceKind_Static: result = GL_STATIC_DRAW; break; + case R_ResourceKind_Dynamic: result = GL_DYNAMIC_DRAW; break; + case R_ResourceKind_Stream: result = GL_STREAM_DRAW; break; + default: break; // Should not happen + } + return result; +} + +internal GLenum +r_gl_format_from_tex2d_format(R_Tex2DFormat format) +{ + GLenum result = GL_RGBA; + switch(format) + { + case R_Tex2DFormat_R8: result = GL_RED; break; + case R_Tex2DFormat_RG8: result = GL_RG; break; + case R_Tex2DFormat_RGBA8: result = GL_RGBA; break; + case R_Tex2DFormat_BGRA8: result = GL_BGRA; break; + case R_Tex2DFormat_R16: result = GL_RED; break; // Note: type will be GL_UNSIGNED_SHORT or similar + case R_Tex2DFormat_RGBA16: result = GL_RGBA; break; // Note: type will be GL_UNSIGNED_SHORT or similar + case R_Tex2DFormat_R32: result = GL_RED; break; // Note: type will be GL_FLOAT + case R_Tex2DFormat_RG32: result = GL_RG; break; // Note: type will be GL_FLOAT + case R_Tex2DFormat_RGBA32: result = GL_RGBA; break; // Note: type will be GL_FLOAT + default: break; + } + return result; +} + +internal GLint +r_gl_internal_format_from_tex2d_format(R_Tex2DFormat format) +{ + GLint result = GL_RGBA8; + switch(format) + { + case R_Tex2DFormat_R8: result = GL_R8; break; + case R_Tex2DFormat_RG8: result = GL_RG8; break; + case R_Tex2DFormat_RGBA8: result = GL_RGBA8; break; + case R_Tex2DFormat_BGRA8: result = GL_RGBA8; break; + case R_Tex2DFormat_R16: result = GL_R16; break; // Use GL_R16UI or GL_R16I for integer formats if needed + case R_Tex2DFormat_RGBA16: result = GL_RGBA16; break; // Use GL_RGBA16UI or GL_RGBA16I for integer formats + case R_Tex2DFormat_R32: result = GL_R32F; break; + case R_Tex2DFormat_RG32: result = GL_RG32F; break; + case R_Tex2DFormat_RGBA32: result = GL_RGBA32F; break; + default: break; + } + return result; +} + +internal GLenum +r_gl_type_from_tex2d_format(R_Tex2DFormat format) +{ + GLenum result = GL_UNSIGNED_BYTE; + switch(format) + { + case R_Tex2DFormat_R8: + case R_Tex2DFormat_RG8: + case R_Tex2DFormat_RGBA8: + case R_Tex2DFormat_BGRA8: result = GL_UNSIGNED_BYTE; break; + case R_Tex2DFormat_R16: result = GL_UNSIGNED_SHORT; break; // Or GL_HALF_FLOAT if using FP16 + case R_Tex2DFormat_RGBA16: result = GL_UNSIGNED_SHORT; break; // Or GL_HALF_FLOAT + case R_Tex2DFormat_R32: + case R_Tex2DFormat_RG32: + case R_Tex2DFormat_RGBA32: result = GL_FLOAT; break; + default: break; + } + return result; +} + +internal GLint +r_gl_filter_from_sample_kind(R_Tex2DSampleKind kind) +{ + GLint result = GL_LINEAR; + switch(kind) + { + case R_Tex2DSampleKind_Nearest: result = GL_NEAREST; break; + case R_Tex2DSampleKind_Linear: result = GL_LINEAR; break; + default: break; + } + return result; +} + +//////////////////////////////// +//~ dan: Buffer Update Helpers + +internal void +r_gl_buffer_update_sub_data(R_Handle handle, U64 offset, U64 size, void *data) +{ + R_GL_Buffer *buffer = r_gl_buffer_from_handle(handle); + if (buffer == &r_gl_buffer_nil || buffer->buffer_id == 0 || size == 0) + { + return; + } + + Assert(buffer->kind == R_ResourceKind_Dynamic || buffer->kind == R_ResourceKind_Stream); + Assert(offset + size <= buffer->size); + Assert(buffer->target != 0); // Target should have been set on alloc + + GLenum target = buffer->target; // Use stored target + + glBindBuffer(target, buffer->buffer_id); + r_gl_check_error("glBindBuffer UpdateSubData"); + + glBufferSubData(target, (GLintptr)offset, (GLsizeiptr)size, data); + r_gl_check_error("glBufferSubData"); + + glBindBuffer(target, 0); +} + +internal void* +r_gl_buffer_map_range(R_Handle handle, U64 offset, U64 size, GLbitfield access_flags) +{ + R_GL_Buffer *buffer = r_gl_buffer_from_handle(handle); + if (buffer == &r_gl_buffer_nil || buffer->buffer_id == 0 || size == 0) + { + return NULL; + } + + Assert(buffer->kind == R_ResourceKind_Stream || buffer->kind == R_ResourceKind_Dynamic); // Map usually for Stream/Dynamic + Assert(offset + size <= buffer->size); + Assert(buffer->target != 0); + + GLenum target = buffer->target; // Use stored target + + glBindBuffer(target, buffer->buffer_id); + r_gl_check_error("glBindBuffer MapRange"); + + void *ptr = glMapBufferRange(target, (GLintptr)offset, (GLsizeiptr)size, access_flags); + r_gl_check_error("glMapBufferRange"); + + // Do not unbind here, caller must call unmap which implies binding. + // glBindBuffer(target, 0); + + return ptr; +} + +internal B32 +r_gl_buffer_unmap(R_Handle handle) +{ + R_GL_Buffer *buffer = r_gl_buffer_from_handle(handle); + if (buffer == &r_gl_buffer_nil || buffer->buffer_id == 0) + { + return 0; // Indicate failure or no-op + } + Assert(buffer->target != 0); + + GLenum target = buffer->target; // Use stored target + + glBindBuffer(target, buffer->buffer_id); + r_gl_check_error("glBindBuffer Unmap"); + + GLboolean result = glUnmapBuffer(target); + r_gl_check_error("glUnmapBuffer"); + + glBindBuffer(target, 0); + + // glUnmapBuffer returns GL_TRUE unless the data store contents have become corrupt. + return (result == GL_TRUE); +} + +//////////////////////////////// +//~ dan: Basic OpenGL Error Checking + +internal void +r_gl_check_error_line_file(const char *op, int line, char *file) +{ + if (!r_gl_state->has_valid_context) + { + fprintf(stderr, "Warning: OpenGL error check with no valid context for operation '%s' at %s:%d\n", + op, file, line); + return; + } + + GLenum err = glGetError(); + if(err != GL_NO_ERROR) + { + // Map error code to a readable string + const char* error_str = "Unknown Error"; + switch(err) + { + case GL_INVALID_ENUM: error_str = "GL_INVALID_ENUM"; break; + case GL_INVALID_VALUE: error_str = "GL_INVALID_VALUE"; break; + case GL_INVALID_OPERATION: error_str = "GL_INVALID_OPERATION"; break; + case GL_STACK_OVERFLOW: error_str = "GL_STACK_OVERFLOW"; break; + case GL_STACK_UNDERFLOW: error_str = "GL_STACK_UNDERFLOW"; break; + case GL_OUT_OF_MEMORY: error_str = "GL_OUT_OF_MEMORY"; break; + } + fprintf(stderr, "OpenGL Error 0x%x (%s) for operation '%s' at %s:%d\n", + err, error_str, op, file, line); + } +} +#define r_gl_check_error(op) r_gl_check_error_line_file(op, __LINE__, __FILE__) + +//////////////////////////////// +//~ dan: Shader Compilation Helpers + +internal GLuint +r_gl_compile_shader(GLenum type, const char *source, String8 name) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, NULL); + r_gl_check_error("glShaderSource"); + glCompileShader(shader); + r_gl_check_error("glCompileShader"); + + GLint success = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if(!success) + { + GLint log_length = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length); + char *info_log = malloc(log_length); // Use temp arena if available + if (info_log) { + glGetShaderInfoLog(shader, log_length, NULL, info_log); + fprintf(stderr, "Shader Compilation Failure (%s) '%.*s':\n%s\n", + type == GL_VERTEX_SHADER ? "VS" : "FS", str8_varg(name), info_log); + free(info_log); + } else { + fprintf(stderr, "Shader Compilation Failure (%s) '%.*s': Could not allocate memory for log.\n", + type == GL_VERTEX_SHADER ? "VS" : "FS", str8_varg(name)); + } + glDeleteShader(shader); + return 0; + } + + return shader; +} + +// Updated to optionally take geometry shader source +internal GLuint +r_gl_create_program(const char *vs_source, const char *fs_source, const char *gs_source, String8 name, GLuint *out_vs, GLuint *out_fs, GLuint *out_gs, GLint *out_texture_location) +{ + GLuint vs = 0, fs = 0, gs = 0, program = 0; + GLint link_success = 0; + + // Print first few lines of vertex shader for debugging + fprintf(stderr, "Compiling shader '%.*s':\n", str8_varg(name)); + + vs = r_gl_compile_shader(GL_VERTEX_SHADER, vs_source, str8_lit("vertex")); + if (!vs) goto fail; + fs = r_gl_compile_shader(GL_FRAGMENT_SHADER, fs_source, str8_lit("fragment")); + if (!fs) goto fail; + if (gs_source) { + gs = r_gl_compile_shader(GL_GEOMETRY_SHADER, gs_source, str8_lit("geometry")); + if (!gs) goto fail; + } + + program = glCreateProgram(); + r_gl_check_error("glCreateProgram"); + glAttachShader(program, vs); + r_gl_check_error("glAttachShader VS"); + glAttachShader(program, fs); + r_gl_check_error("glAttachShader FS"); + if (gs) { + glAttachShader(program, gs); + r_gl_check_error("glAttachShader GS"); + } + glLinkProgram(program); + r_gl_check_error("glLinkProgram"); + + glGetProgramiv(program, GL_LINK_STATUS, &link_success); + if(!link_success) + { + GLint log_length = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length); + char *info_log = malloc(log_length); // Use temp arena if available + if (info_log) { + glGetProgramInfoLog(program, log_length, NULL, info_log); + fprintf(stderr, "Shader Linking Failure '%.*s':\n%s\n", str8_varg(name), info_log); + free(info_log); + } else { + fprintf(stderr, "Shader Linking Failure '%.*s': Could not allocate memory for log.\n", str8_varg(name)); + } + goto fail; // Use goto for cleanup + } + + // Validate the program to catch any issues + glValidateProgram(program); + GLint validate_status = 0; + glGetProgramiv(program, GL_VALIDATE_STATUS, &validate_status); + if (validate_status == GL_FALSE) { + GLint log_length = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length); + char *info_log = malloc(log_length); + if (info_log) { + glGetProgramInfoLog(program, log_length, NULL, info_log); + fprintf(stderr, "Shader Program Validation Failed '%.*s':\n%s\n", + str8_varg(name), info_log); + free(info_log); + // Program can still be used even if validation fails, we'll only issue a warning + } + } + + // Detach shaders after successful link (optional but good practice) + glDetachShader(program, vs); + glDetachShader(program, fs); + if(gs) glDetachShader(program, gs); + + // Store shader handles if requested, otherwise delete them + if(out_vs) *out_vs = vs; else glDeleteShader(vs); + if(out_fs) *out_fs = fs; else glDeleteShader(fs); + if(out_gs) *out_gs = gs; else if(gs) glDeleteShader(gs); + + // Get sampler uniform location (assuming name "main_t2d" for texture unit 0) + if (out_texture_location) { + *out_texture_location = glGetUniformLocation(program, "main_t2d"); + fprintf(stderr, "%.*s shader main_t2d location: %d\n", str8_varg(name), *out_texture_location); + } + + r_gl_check_error("Shader Program Creation Success"); + return program; + +fail: + fprintf(stderr, "ERROR: Failed to create shader program '%.*s'\n", str8_varg(name)); + if (vs) glDeleteShader(vs); + if (fs) glDeleteShader(fs); + if (gs) glDeleteShader(gs); + if (program) glDeleteProgram(program); + return 0; +} + +//////////////////////////////// +//~ dan: VAO Helpers (Added) + +// Creates a VAO suitable for drawing a fullscreen quad (no VBO needed) +internal GLuint +r_gl_vao_make_fullscreen_quad(void) +{ + GLuint vao_id; + glGenVertexArrays(1, &vao_id); + // Bind the VAO before checking for errors + glBindVertexArray(vao_id); + r_gl_check_error("Fullscreen VAO Creation"); + glBindVertexArray(0); // Unbind to be safe + return vao_id; +} + +//////////////////////////////// +//~ dan: One-Time Initialization Helpers + +// Initialize GLEW only - called with temp context +internal void +r_gl_init_glew_if_needed(void) +{ + if(r_gl_state->glew_initialized) + { + return; + } + + // Verify we have a valid OpenGL context before trying to initialize GLEW + GLXContext current_ctx = glXGetCurrentContext(); + if (!current_ctx) + { + fprintf(stderr, "ERROR: Attempting to initialize GLEW without a valid OpenGL context!\n"); + return; + } + + fprintf(stderr, "Initializing GLEW with current GLX context: %p\n", (void*)current_ctx); + + // Initialize GLEW + glewExperimental = GL_TRUE; // Enable GLEW experimental features for modern contexts + GLenum err = glewInit(); + + // GLEW initialization can sometimes report GL_INVALID_ENUM but still work correctly + // Clear any error that might have been generated by GLEW + glGetError(); + + if(err != GLEW_OK) + { + fprintf(stderr, "GLEW initialization error: %s\n", glewGetErrorString(err)); + return; // Return without setting glew_initialized if it completely failed + } + + // Mark GLEW as initialized + r_gl_state->glew_initialized = 1; + + // Print OpenGL version information for debugging + const GLubyte* vendor = glGetString(GL_VENDOR); + const GLubyte* renderer = glGetString(GL_RENDERER); + const GLubyte* version = glGetString(GL_VERSION); + const GLubyte* glsl_version = glGetString(GL_SHADING_LANGUAGE_VERSION); + + fprintf(stderr, "OpenGL Vendor: %s\n", vendor ? (const char*)vendor : "unknown"); + fprintf(stderr, "OpenGL Renderer: %s\n", renderer ? (const char*)renderer : "unknown"); + fprintf(stderr, "OpenGL Version: %s\n", version ? (const char*)version : "unknown"); + fprintf(stderr, "GLSL Version: %s\n", glsl_version ? (const char*)glsl_version : "unknown"); + + // Now that GLEW is initialized and we have a valid context, mark the context as valid + // This will trigger processing of any deferred textures + fprintf(stderr, "Setting has_valid_context = 1\n"); + r_gl_set_has_valid_context(1); +} + +// Create global GL resources - called with final context +internal void +r_gl_create_global_resources(void) +{ + if(r_gl_state->global_resources_initialized) + { + return; // Already initialized + } + + log_infof("Creating global OpenGL resources..."); + + // Ensure GLEW is initialized first + r_gl_init_glew_if_needed(); + if (!r_gl_state->has_valid_context) { + log_infof("Skipping global resource creation as there is no valid GL context yet."); + return; + } + + // Make sure we have a valid context before proceeding + if(!r_gl_state->has_valid_context) + { + fprintf(stderr, "Error: Attempted to create OpenGL resources without a valid context.\n"); + return; + } + + // Make sure GLEW is initialized + if(!r_gl_state->glew_initialized) + { + fprintf(stderr, "Error: Attempted to create OpenGL resources before GLEW initialization.\n"); + return; + } + + fprintf(stderr, "Creating OpenGL global resources...\n"); + + // Create fullscreen quad VAO + r_gl_state->fullscreen_vao = r_gl_vao_make_fullscreen_quad(); + + // Create rect VAO + glGenVertexArrays(1, &r_gl_state->rect_vao); + glBindVertexArray(r_gl_state->rect_vao); +r_gl_check_error("UI Bind VAO"); + + // Create shared instance VBO (moved earlier) + glGenBuffers(1, &r_gl_state->instance_vbo); + glBindBuffer(GL_ARRAY_BUFFER, r_gl_state->instance_vbo); +r_gl_check_error("UI Bind Instance VBO"); + // Allocate some initial size, it will be resized later if needed. Use GL_STREAM_DRAW for frequent updates. + glBufferData(GL_ARRAY_BUFFER, MB(1), NULL, GL_STREAM_DRAW); // Example initial size: 1MB + r_gl_check_error("Instance VBO Creation"); + + // Bind the instance VBO before defining attributes that use it + glBindBuffer(GL_ARRAY_BUFFER, r_gl_state->instance_vbo); +r_gl_check_error("UI Bind Instance VBO"); + r_gl_check_error("Rect VAO Bind Instance VBO"); + + // Setup instance attributes for the Rect shader (8 vec4s) + // Assuming R_Rect2DInst packs these 8 vec4s contiguously. + // Stride is sizeof(R_Rect2DInst), which we assume is 8 * 4 * sizeof(float) = 128 bytes. + size_t rect_instance_stride = 8 * 4 * sizeof(float); + for (int i = 0; i < 8; i++) { + glEnableVertexAttribArray(i); + // Each attribute is a vec4 (4 floats) + // The offset is i * sizeof(vec4) + glVertexAttribPointer(i, 4, GL_FLOAT, GL_FALSE, (GLsizei)rect_instance_stride, (void*)(i * 4 * sizeof(float))); + glVertexAttribDivisor(i, 1); // Advance this attribute once per instance + } + r_gl_check_error("Rect VAO Instance Attributes"); + + // Clean up bindings + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); // Unbind VAO as well + + // Create mesh VAO for 3D drawing + glGenVertexArrays(1, &r_gl_state->mesh_vao); + + glBindVertexArray(r_gl_state->mesh_vao); + { + // Per-vertex attributes (binding index 0 implicitly uses currently bound GL_ARRAY_BUFFER) + glEnableVertexAttribArray(0); // Position + glEnableVertexAttribArray(1); // Normal + glEnableVertexAttribArray(2); // TexCoord + glEnableVertexAttribArray(3); // Color + // These will have divisor 0 by default + + // Per-instance attributes (binding index 1 implicitly uses currently bound GL_ARRAY_BUFFER) + // Instance Transform Matrix (mat4 = 4 x vec4) + glEnableVertexAttribArray(4); glVertexAttribDivisor(4, 1); + glEnableVertexAttribArray(5); glVertexAttribDivisor(5, 1); + glEnableVertexAttribArray(6); glVertexAttribDivisor(6, 1); + glEnableVertexAttribArray(7); glVertexAttribDivisor(7, 1); + } + glBindVertexArray(0); // Unbind VAO + r_gl_check_error("Mesh VAO Creation"); + + // Create UBO for each uniform block type + // Note: UBO size must be a multiple of GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT + // For simplicity, we use sizes directly from the struct definitions. + GLuint ubo_sizes[R_GL_UniformTypeKind_COUNT] = { + r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Rect], // R_GL_UniformTypeKind_Rect + r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Blur], // R_GL_UniformTypeKind_Blur + r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Mesh], // R_GL_UniformTypeKind_Mesh + }; + + // Store sizes for later use + for (int i = 0; i < R_GL_UniformTypeKind_COUNT; i++) { + r_gl_state->uniform_type_kind_sizes[i] = ubo_sizes[i]; + } + + glGenBuffers(R_GL_UniformTypeKind_COUNT, r_gl_state->uniform_type_kind_buffers); + for (int i = 0; i < R_GL_UniformTypeKind_COUNT; i++) { + glBindBuffer(GL_UNIFORM_BUFFER, r_gl_state->uniform_type_kind_buffers[i]); + glBufferData(GL_UNIFORM_BUFFER, ubo_sizes[i], NULL, GL_DYNAMIC_DRAW); + } + glBindBuffer(GL_UNIFORM_BUFFER, 0); + + // Create samplers + glGenSamplers(R_Tex2DSampleKind_COUNT, r_gl_state->samplers); + for (R_Tex2DSampleKind kind = 0; kind < R_Tex2DSampleKind_COUNT; kind++) { + GLint filter = r_gl_filter_from_sample_kind(kind); + glSamplerParameteri(r_gl_state->samplers[kind], GL_TEXTURE_MIN_FILTER, filter); + glSamplerParameteri(r_gl_state->samplers[kind], GL_TEXTURE_MAG_FILTER, filter); + // Use consistent wrapping mode: CLAMP_TO_EDGE for all textures + // This ensures consistent behavior between D3D11 and OpenGL + glSamplerParameteri(r_gl_state->samplers[kind], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glSamplerParameteri(r_gl_state->samplers[kind], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + // Create backup texture (1x1 white pixel) + { + // Create backup texture (1x1 white pixel) + R_GL_Tex2D *tex = r_gl_state->first_free_tex2d; + if(!tex) + { + tex = push_array(r_gl_state->arena, R_GL_Tex2D, 1); + } + else + { + SLLStackPop(r_gl_state->first_free_tex2d); + } + // Minimal reset needed after popping from free list + MemoryZeroStruct(tex); // Ensure struct is clean + tex->generation = (tex == r_gl_state->first_free_tex2d) ? 1 : tex->generation + 1; // Increment gen safely + + tex->kind = R_ResourceKind_Static; + tex->size = v2s32(1, 1); + tex->format = R_Tex2DFormat_RGBA8; + + glGenTextures(1, &tex->texture_id); + r_gl_check_error("glGenTextures Backup"); // Added context + glBindTexture(GL_TEXTURE_2D, tex->texture_id); + r_gl_check_error("glBindTexture Backup"); + U8 white_pixel[4] = {255, 255, 255, 255}; + // Set pixel store alignment for the 1x1 texture + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // RGBA8 is 4 bytes + r_gl_check_error("glPixelStorei Backup"); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white_pixel); + r_gl_check_error("glTexImage2D Backup"); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // Added wrap mode + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Added wrap mode + r_gl_check_error("glTexParameteri Backup"); + glBindTexture(GL_TEXTURE_2D, 0); + + r_gl_state->backup_texture = r_gl_handle_from_tex2d(tex); + } + + // NOTE: Rect shader + { + // Use global variables for shader source + const char *vs_source = (const char *)r_gl_g_vshad_kind_source_ptr_table[R_GL_VShadKind_Rect]->str; + const char *fs_source = (const char *)r_gl_g_pshad_kind_source_ptr_table[R_GL_PShadKind_Rect]->str; + + GLint texture_location = 0; + r_gl_state->rect_shader.program_id = r_gl_create_program( + vs_source, fs_source, NULL, + str8_lit("rect_shader"), + &r_gl_state->rect_shader.vertex_shader, + &r_gl_state->rect_shader.fragment_shader, + NULL, + &texture_location); // Pass location pointer + + // If program creation failed, we'll get a zero ID + if (r_gl_state->rect_shader.program_id == 0) { + fprintf(stderr, "ERROR: Failed to create rect shader program\n"); + } else { + fprintf(stderr, "Successfully created rect shader program: %u\n", r_gl_state->rect_shader.program_id); + r_gl_state->rect_shader.main_texture_uniform_location = texture_location; // Store the location + + // Bind the uniform block to its index point for the rect shader + GLuint block_index = glGetUniformBlockIndex(r_gl_state->rect_shader.program_id, "R_GL_Uniforms_Rect"); + if (block_index != GL_INVALID_INDEX) { + glUniformBlockBinding(r_gl_state->rect_shader.program_id, block_index, R_GL_UNIFORM_BINDING_POINT_RECT); + } else { + fprintf(stderr, "WARNING: Could not find uniform block 'R_GL_Uniforms_Rect' in shader\n"); + } + // Removed manual glGetUniformLocation for "main_texture" + } + } + + // NOTE: Blur shader + { + // Use global variables for shader source + const char *vs_source = (const char *)r_gl_g_vshad_kind_source_ptr_table[R_GL_VShadKind_Blur]->str; + const char *fs_source = (const char *)r_gl_g_pshad_kind_source_ptr_table[R_GL_PShadKind_Blur]->str; + + GLint texture_location = 0; + r_gl_state->blur_shader.program_id = r_gl_create_program( + vs_source, fs_source, NULL, + str8_lit("blur_shader"), + &r_gl_state->blur_shader.vertex_shader, + &r_gl_state->blur_shader.fragment_shader, + NULL, + &texture_location); // Pass location pointer + + // Successfully created blur shader? + if (r_gl_state->blur_shader.program_id == 0) { + fprintf(stderr, "ERROR: Failed to create blur shader program\n"); + } else { + fprintf(stderr, "Successfully created blur shader program: %u\n", r_gl_state->blur_shader.program_id); + r_gl_state->blur_shader.main_texture_uniform_location = texture_location; // Store the location + + // Bind the uniform block to its index point + GLuint block_index = glGetUniformBlockIndex(r_gl_state->blur_shader.program_id, "BlurUniforms"); + if (block_index != GL_INVALID_INDEX) { + glUniformBlockBinding(r_gl_state->blur_shader.program_id, block_index, R_GL_UNIFORM_BINDING_POINT_BLUR); + fprintf(stderr, "Blur UBO block bound to binding point %d\n", R_GL_UNIFORM_BINDING_POINT_BLUR); + } else { + fprintf(stderr, "WARNING: Could not find uniform block 'BlurUniforms' in shader\n"); + } + + // Get and store the main texture location + // r_gl_state->blur_shader.main_texture_uniform_location = texture_location; // Already stored above + } + } + + // NOTE: Mesh shader + { + // Use global variables for shader source + const char *vs_source = (const char *)r_gl_g_vshad_kind_source_ptr_table[R_GL_VShadKind_Mesh]->str; + const char *fs_source = (const char *)r_gl_g_pshad_kind_source_ptr_table[R_GL_PShadKind_Mesh]->str; + + GLint texture_location = 0; + r_gl_state->mesh_shader.program_id = r_gl_create_program( + vs_source, fs_source, NULL, + str8_lit("mesh_shader"), + &r_gl_state->mesh_shader.vertex_shader, + &r_gl_state->mesh_shader.fragment_shader, + NULL, + &texture_location); // Pass location pointer + + if (r_gl_state->mesh_shader.program_id == 0) { + fprintf(stderr, "ERROR: Failed to create mesh shader program\n"); + } else { + fprintf(stderr, "Successfully created mesh shader program: %u\n", r_gl_state->mesh_shader.program_id); + r_gl_state->mesh_shader.main_texture_uniform_location = texture_location; // Store the location + + // Bind the uniform block + GLuint block_index = glGetUniformBlockIndex(r_gl_state->mesh_shader.program_id, "R_GL_Uniforms_Mesh"); + if (block_index != GL_INVALID_INDEX) { + glUniformBlockBinding(r_gl_state->mesh_shader.program_id, block_index, R_GL_UNIFORM_BINDING_POINT_MESH); + } else { + fprintf(stderr, "WARNING: Could not find uniform block 'R_GL_Uniforms_Mesh' in shader\n"); + } + + // Get and store the main texture location + // r_gl_state->mesh_shader.main_texture_uniform_location = texture_location; // Already stored above + // Removed manual glGetUniformLocation for "main_texture" + } + } + + // NOTE: Geo3D Composite shader + { + // Use global variable for shader source (assuming VS is simple and FS is main source) + // NOTE: Assuming a simple pass-through VS or defining r_gl_g_geo3dcomposite_shader_vs_src + const char *vs_source = + "#version 330 core\n" + "out vec2 TexCoord;\n" + "void main()\n" + "{\n" + " vec2 pos = vec2( (gl_VertexID & 1) * 2.0 - 1.0, (gl_VertexID & 2) - 1.0 ); \n" + " gl_Position = vec4(pos, 0.0, 1.0);\n" + " TexCoord = pos * 0.5 + 0.5;\n" + " TexCoord.y = 1.0 - TexCoord.y;\n" + "}\n"; // Using inline VS for now as no global was defined + const char *fs_source = (const char *)r_gl_g_pshad_kind_source_ptr_table[R_GL_PShadKind_Geo3DComposite]->str; + + GLint texture_location = 0; // Get the location via the helper + r_gl_state->geo3d_composite_shader.program_id = r_gl_create_program( + vs_source, fs_source, NULL, + str8_lit("geo3d_composite_shader"), + &r_gl_state->geo3d_composite_shader.vertex_shader, + &r_gl_state->geo3d_composite_shader.fragment_shader, + NULL, + &texture_location); // Pass pointer to get "main_t2d" location + + if (r_gl_state->geo3d_composite_shader.program_id == 0) { + fprintf(stderr, "ERROR: Failed to create geo3d composite shader program\n"); + } else { + fprintf(stderr, "Successfully created geo3d composite shader program: %u\n", r_gl_state->geo3d_composite_shader.program_id); + r_gl_state->geo3d_composite_shader.main_texture_uniform_location = texture_location; // Store the location + + // Removed manual glGetUniformLocation and glUniform1i calls here + // Shader uniforms for composite are now set in the render pass submit logic + } + } + + // NOTE: Finalize shader (simple blit to screen) + { + // Use global variables for shader source + const char *vs_source = (const char *)r_gl_g_vshad_kind_source_ptr_table[R_GL_VShadKind_FullscreenQuad]->str; + const char *fs_source = (const char *)r_gl_g_pshad_kind_source_ptr_table[R_GL_PShadKind_Finalize]->str; + + GLint texture_location = 0; + r_gl_state->finalize_shader.program_id = r_gl_create_program( + vs_source, fs_source, NULL, + str8_lit("finalize_shader"), + &r_gl_state->finalize_shader.vertex_shader, + &r_gl_state->finalize_shader.fragment_shader, + NULL, + &texture_location); // Pass location pointer + + if (r_gl_state->finalize_shader.program_id == 0) { + fprintf(stderr, "ERROR: Failed to create finalize shader program\n"); + } else { + fprintf(stderr, "Successfully created finalize shader program: %u\n", r_gl_state->finalize_shader.program_id); + + // Get and store the main texture location + r_gl_state->finalize_shader.main_texture_uniform_location = texture_location; // Store the location + // Removed manual glGetUniformLocation for "main_texture" + } + } + + fprintf(stderr, "OpenGL resources created successfully.\n"); + + // Mark initialization as complete + r_gl_state->global_resources_initialized = 1; + + //- Create uniform buffer objects (UBOs) + { + // Define UBO sizes for each uniform type + r_gl_state->uniform_type_kind_sizes[R_GL_UniformTypeKind_Rect] = r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Rect]; + r_gl_state->uniform_type_kind_sizes[R_GL_UniformTypeKind_Blur] = r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Blur]; + r_gl_state->uniform_type_kind_sizes[R_GL_UniformTypeKind_Mesh] = r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Mesh]; + + // Create and initialize UBOs for each uniform type + for (R_GL_UniformTypeKind kind = 0; kind < R_GL_UniformTypeKind_COUNT; kind++) + { + GLuint buffer = 0; + glGenBuffers(1, &buffer); + glBindBuffer(GL_UNIFORM_BUFFER, buffer); + + // Calculate size with proper alignment (multiple of 16 bytes) + GLsizeiptr size = r_gl_state->uniform_type_kind_sizes[kind]; + size = (size + 15) & ~15; // Round up to multiple of 16 + + // Initialize the buffer with NULL data (allocate space only) + glBufferData(GL_UNIFORM_BUFFER, size, NULL, GL_DYNAMIC_DRAW); + + // Bind to pre-defined binding point + glBindBufferBase(GL_UNIFORM_BUFFER, kind, buffer); + + // Store the buffer handle + r_gl_state->uniform_type_kind_buffers[kind] = buffer; + + // Unbind the buffer + glBindBuffer(GL_UNIFORM_BUFFER, 0); + } + } + + r_gl_check_error("Post UBO Creation"); // Check errors after UBO setup + + log_infof("Global OpenGL resources created successfully."); + r_gl_state->global_resources_initialized = 1; + + // ADDED: Process deferred textures *after* global resources are ready + r_gl_process_deferred_tex2d_queue(); +} + +// This function initializes GLEW and creates global OpenGL resources. +// It MUST be called after *some* GL context is made current for the first time. +// It's designed to be called idempotently. +internal void +r_gl_init_extensions_if_needed(void) +{ + // Call the split initialization functions for backward compatibility + r_gl_init_glew_if_needed(); + r_gl_create_global_resources(); +} + +//////////////////////////////// +//~ dan: Backend Hooks Implementation + +r_hook void +r_init(CmdLine *cmdln) +{ + ProfBeginFunction(); + Arena *arena = arena_alloc(GB(1)); // Consider arena params + r_gl_state = push_array(arena, R_GL_State, 1); + r_gl_state->arena = arena; + r_gl_state->device_rw_mutex = os_rw_mutex_alloc(); + r_gl_state->global_resources_initialized = 0; // Initialize the flag + r_gl_state->glew_initialized = 0; // Initialize GLEW flag + r_gl_state->has_valid_context = 0; // No valid context yet + + // Initialize deferred texture allocation + r_gl_state->deferred_tex2d_arena = arena_alloc(); + r_gl_state->first_deferred_tex2d = NULL; + r_gl_state->last_deferred_tex2d = NULL; + + // Store UBO sizes (does not require GL context) + r_gl_state->uniform_type_kind_sizes[R_GL_UniformTypeKind_Rect] = r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Rect]; + r_gl_state->uniform_type_kind_sizes[R_GL_UniformTypeKind_Blur] = r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Blur]; + r_gl_state->uniform_type_kind_sizes[R_GL_UniformTypeKind_Mesh] = r_gl_g_uniform_type_kind_size_table[R_GL_UniformTypeKind_Mesh]; + + // NOTE: All gl* calls are removed from here and moved to r_window_equip / r_gl_create_global_resources + ProfEnd(); +} + +// Add this function after r_init but before other functions +internal void +r_gl_set_has_valid_context(B32 has_context) +{ + log_infof("Setting has_valid_context = %d", has_context); + r_gl_state->has_valid_context = has_context; + // Process deferred textures *only* if the context is now valid + if(has_context) + { + // REMOVED: r_gl_process_deferred_tex2d_queue(); + } +} + +// New function to process the deferred texture allocation queue +internal void +r_gl_process_deferred_tex2d_queue(void) +{ + // Add early return if we don't have a valid context or GLEW isn't initialized + if(!r_gl_state->has_valid_context || !r_gl_state->glew_initialized) + { + fprintf(stderr, "Cannot process deferred textures: has_valid_context=%d, glew_initialized=%d\n", + r_gl_state->has_valid_context, r_gl_state->glew_initialized); + return; + } + + // Check if queue is empty + if(!r_gl_state->first_deferred_tex2d) + { + // No deferred textures to process + return; + } + + fprintf(stderr, "Processing deferred texture queue...\n"); + + // Continue with normal processing now that we have a valid context + for(R_GL_DeferredTex2D *n = r_gl_state->first_deferred_tex2d, *next = 0; + n != 0; + n = next) + { + next = n->next; + + // Create the texture now that we have a valid context + R_GL_Tex2D *tex = n->texture; + tex->kind = n->kind; + tex->size = n->size; + tex->format = n->format; + + // Generate texture - clear any previous errors first + glGetError(); // Clear previous errors + + glGenTextures(1, &tex->texture_id); + GLenum err = glGetError(); + if(err != GL_NO_ERROR) + { + fprintf(stderr, "Error in deferred texture creation (glGenTextures): 0x%x\n", err); + continue; // Skip this texture and move to the next + } + + glBindTexture(GL_TEXTURE_2D, tex->texture_id); + err = glGetError(); + if(err != GL_NO_ERROR) + { + fprintf(stderr, "Error in deferred texture creation (glBindTexture): 0x%x\n", err); + continue; + } + + // Setup texture based on format + GLint internal_format = r_gl_internal_format_from_tex2d_format(n->format); + GLenum format = r_gl_format_from_tex2d_format(n->format); + GLenum type = r_gl_type_from_tex2d_format(n->format); + + // Upload texture data + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, n->size.x, n->size.y, 0, format, type, n->data); + err = glGetError(); + if(err != GL_NO_ERROR) + { + fprintf(stderr, "Error in deferred texture creation (glTexImage2D): 0x%x\n", err); + fprintf(stderr, " format=%d, internal_format=%d, type=%d, size=%dx%d\n", + format, internal_format, type, n->size.x, n->size.y); + continue; + } + + // Set default parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + err = glGetError(); + if(err != GL_NO_ERROR) + { + fprintf(stderr, "Error in deferred texture creation (glTexParameteri): 0x%x\n", err); + } + + // Free our copy of the data if we allocated one + if(n->data_size > 0 && n->data != NULL) + { + // arena_pop(r_gl_state->deferred_tex2d_arena, n->data_size); // INCORRECT: Arena pop is LIFO, loop is FIFO. + } + } + + // Clear the queue + fprintf(stderr, "Deferred texture queue processed\n"); + r_gl_state->first_deferred_tex2d = NULL; + r_gl_state->last_deferred_tex2d = NULL; + + // Clear the entire arena now that all data has been used + if(r_gl_state->deferred_tex2d_arena) + { + arena_clear(r_gl_state->deferred_tex2d_arena); + } +} + +r_hook R_Handle +r_window_equip(OS_Handle window) +{ + // This function equips subsequent windows or the first window *after* context creation. + R_GL_Window *gl_window = 0; + OS_LNX_Window *lnx_window = (OS_LNX_Window *)window.u64[0]; + + fprintf(stderr, "r_window_equip: OS window handle %p, X11 window %lu\n", + (void*)lnx_window, lnx_window ? lnx_window->window : 0); + + OS_MutexScope(r_gl_state->device_rw_mutex) + { + if(r_gl_state->first_free_window) + { + gl_window = r_gl_state->first_free_window; + SLLStackPop(r_gl_state->first_free_window); + MemoryZeroStruct(gl_window); // Reset struct for reuse + } + else + { + gl_window = push_array(r_gl_state->arena, R_GL_Window, 1); + } + + // Store OS_LNX_Window pointer as generation to make context current later + gl_window->generation = (U64)lnx_window; + gl_window->last_resolution = v2s32(0, 0); // Force resize on first frame + + // Store the display pointer in r_gl_state for later use + r_gl_state->display = os_lnx_gfx_state->display; + + // Print OpenGL info + const GLubyte* renderer = glGetString(GL_RENDERER); + const GLubyte* version = glGetString(GL_VERSION); + fprintf(stderr, "OpenGL Renderer: %s\n", renderer ? (const char*)renderer: "unknown"); + fprintf(stderr, "OpenGL Version: %s\n", version ? (const char*)version : "unknown"); + } + + R_Handle result = r_gl_handle_from_window(gl_window); + return result; +} + +// Helper function to delete window FBO resources +internal void +r_gl_delete_window_fbo_resources(R_GL_Window *gl_window) +{ + // Delete FBOs + if (gl_window->stage_fbo) { glDeleteFramebuffers(1, &gl_window->stage_fbo); gl_window->stage_fbo = 0; } + if (gl_window->stage_scratch_fbo) { glDeleteFramebuffers(1, &gl_window->stage_scratch_fbo); gl_window->stage_scratch_fbo = 0; } + if (gl_window->geo3d_fbo) { glDeleteFramebuffers(1, &gl_window->geo3d_fbo); gl_window->geo3d_fbo = 0; } + r_gl_check_error("FBO Deletion"); + + // Delete Textures used as attachments + if (gl_window->stage_color_texture) { glDeleteTextures(1, &gl_window->stage_color_texture); gl_window->stage_color_texture = 0; } + if (gl_window->stage_scratch_color_texture) { glDeleteTextures(1, &gl_window->stage_scratch_color_texture); gl_window->stage_scratch_color_texture = 0; } + if (gl_window->geo3d_color_texture) { glDeleteTextures(1, &gl_window->geo3d_color_texture); gl_window->geo3d_color_texture = 0; } + if (gl_window->geo3d_depth_texture) { glDeleteTextures(1, &gl_window->geo3d_depth_texture); gl_window->geo3d_depth_texture = 0; } + r_gl_check_error("FBO Texture Deletion"); + + // Reset resolution to force recreation + gl_window->last_resolution = v2s32(0, 0); +} + +// Implementation of the make context current function +internal void +r_gl_make_context_current(R_GL_Window *window) +{ + if (window != NULL && window != &r_gl_window_nil) { + OS_LNX_Window *os_window = (OS_LNX_Window *)window->generation; + if (os_window && os_window->gl_context) { + Display *display = os_lnx_gfx_state->display; + + // Check if this context is already current + GLXContext current = glXGetCurrentContext(); + Window current_drawable = glXGetCurrentDrawable(); + + if (current != os_window->gl_context || current_drawable != os_window->window) { + fprintf(stderr, "Making context current: display=%p, window=%lu, context=%p\n", + display, os_window->window, os_window->gl_context); + + if (!glXMakeCurrent(display, os_window->window, os_window->gl_context)) { + fprintf(stderr, "ERROR: Failed to make context current!\n"); + + // Try to diagnose the error + if (!display) { + fprintf(stderr, " - Display is NULL\n"); + } + if (os_window->window == 0) { + fprintf(stderr, " - Window is 0 (None)\n"); + } + if (!os_window->gl_context) { + fprintf(stderr, " - GL Context is NULL\n"); + } + } else { + // Verify the context was made current + GLXContext new_current = glXGetCurrentContext(); + Window new_drawable = glXGetCurrentDrawable(); + + if (new_current == os_window->gl_context && new_drawable == os_window->window) { + fprintf(stderr, "Successfully made context current\n"); + + // Check if we can make GL calls + GLint max_texture_size = 0; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size); + GLenum err = glGetError(); + + if (err != GL_NO_ERROR || max_texture_size == 0) { + fprintf(stderr, "WARNING: Context made current but GL calls failing (err=0x%x, max_tex_size=%d)\n", + err, max_texture_size); + } + } else { + fprintf(stderr, "ERROR: Context not current after glXMakeCurrent! current=%p, drawable=%lu\n", + new_current, new_drawable); + } + } + } + } else { + fprintf(stderr, "ERROR: Invalid OS window or GL context in r_gl_make_context_current\n"); + if (!os_window) { + fprintf(stderr, " - OS window is NULL (window->generation=%lu)\n", window->generation); + } else if (!os_window->gl_context) { + fprintf(stderr, " - gl_context is NULL\n"); + } + } + } else { + fprintf(stderr, "ERROR: Invalid R_GL_Window in r_gl_make_context_current\n"); + } +} + +r_hook void +r_window_unequip(OS_Handle window, R_Handle window_equip) +{ + R_GL_Window *gl_window = r_gl_window_from_handle(window_equip); + if(gl_window != &r_gl_window_nil) + { + // Check if we're unequipping the currently active window + if (glXGetCurrentContext() == ((OS_LNX_Window *)gl_window->generation)->gl_context) { + r_gl_make_context_current(NULL); // Switch to another valid context + } + + r_gl_delete_window_fbo_resources(gl_window); + SLLStackPush(r_gl_state->first_free_window, gl_window); + } +} + +r_hook R_Handle +r_tex2d_alloc(R_ResourceKind kind, Vec2S32 size, R_Tex2DFormat format, void *data) +{ + ProfBeginFunction(); + R_Handle result = {0}; + + // Check if we need to defer allocation due to no OpenGL context + if(!r_gl_state->has_valid_context) + { + // Allocate a texture object that will be populated later + R_GL_Tex2D *texture = r_gl_state->first_free_tex2d; + if(texture) + { + SLLStackPop(r_gl_state->first_free_tex2d); + } + else + { + texture = push_array(r_gl_state->arena, R_GL_Tex2D, 1); + } + texture->generation += 1; + + // Create the deferred allocation entry + if(!r_gl_state->deferred_tex2d_arena) + { + r_gl_state->deferred_tex2d_arena = arena_alloc(); + } + + R_GL_DeferredTex2D *deferred = push_array(r_gl_state->deferred_tex2d_arena, R_GL_DeferredTex2D, 1); + deferred->kind = kind; + deferred->size = size; + deferred->format = format; + deferred->texture = texture; + + // Copy the data if provided + if(data) + { + size_t data_size = size.x * size.y * r_tex2d_format_bytes_per_pixel_table[format]; + void *data_copy = push_array(r_gl_state->deferred_tex2d_arena, U8, data_size); + MemoryCopy(data_copy, data, data_size); + deferred->data = data_copy; + deferred->data_size = data_size; + } + + // Add to deferred list + SLLQueuePush(r_gl_state->first_deferred_tex2d, r_gl_state->last_deferred_tex2d, deferred); + + // Return handle that will be valid when processed later + result = r_gl_handle_from_tex2d(texture); + + fprintf(stderr, "Warning: r_tex2d_alloc called with no active GL context. Deferring creation.\n"); + + ProfEnd(); + return result; + } + + // Original implementation for when we have a valid context + // Allocate the texture object + R_GL_Tex2D *texture = r_gl_state->first_free_tex2d; + if(texture) + { + SLLStackPop(r_gl_state->first_free_tex2d); + } + else + { + texture = push_array(r_gl_state->arena, R_GL_Tex2D, 1); + } + texture->generation += 1; + + // Create the OpenGL texture + glGenTextures(1, &texture->texture_id); + r_gl_check_error("r_tex2d_alloc - glGenTextures"); + + glBindTexture(GL_TEXTURE_2D, texture->texture_id); +r_gl_check_error("UI Bind Texture"); + r_gl_check_error("r_tex2d_alloc - glBindTexture"); + + // Get format information + GLenum gl_format = r_gl_format_from_tex2d_format(format); + GLint gl_internal_format = r_gl_internal_format_from_tex2d_format(format); + GLenum gl_type = r_gl_type_from_tex2d_format(format); + + // Configure the texture + glTexImage2D(GL_TEXTURE_2D, 0, gl_internal_format, + size.x, size.y, + 0, gl_format, gl_type, data); + r_gl_check_error("r_tex2d_alloc - glTexImage2D"); + + // Set filtering parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + r_gl_check_error("r_tex2d_alloc - glTexParameteri"); + + glBindTexture(GL_TEXTURE_2D, 0); + + // Store metadata in the texture object + texture->kind = kind; + texture->size = size; + texture->format = format; + + result = r_gl_handle_from_tex2d(texture); + ProfEnd(); + return result; +} + +r_hook void +r_tex2d_release(R_Handle handle) +{ + ProfBeginFunction(); + OS_MutexScopeW(r_gl_state->device_rw_mutex) +{ + R_GL_Tex2D *texture = r_gl_tex2d_from_handle(handle); + SLLStackPush(r_gl_state->first_to_free_tex2d, texture); + } + ProfEnd(); +} + +r_hook R_Handle +r_buffer_alloc(R_ResourceKind kind, U64 size, void *data) +{ + ProfBeginFunction(); + + // NEW: Check if we have a valid OpenGL context + GLXContext current_ctx = glXGetCurrentContext(); + if (!current_ctx) { + // No valid context - can't create buffers yet + fprintf(stderr, "Warning: r_buffer_alloc called with no active GL context. Deferring creation.\n"); + R_Handle result = {0}; + return result; + } + + //- dan: allocate buffer structure + R_GL_Buffer *buffer = 0; + OS_MutexScopeW(r_gl_state->device_rw_mutex) + { + buffer = r_gl_state->first_free_buffer; + if(buffer == 0) + { + buffer = push_array(r_gl_state->arena, R_GL_Buffer, 1); + } + else + { + U64 gen = buffer->generation; + SLLStackPop(r_gl_state->first_free_buffer); + MemoryZeroStruct(buffer); + buffer->generation = gen; + } + buffer->generation += 1; + } + + //- dan: verify static buffer has data + if(kind == R_ResourceKind_Static) + { + Assert(data != 0 && "static buffer must have initial data provided"); + } + + //- dan: get buffer usage from resource kind + GLenum usage = r_gl_usage_from_resource_kind(kind); + + //- dan: create buffer object + GLuint buffer_id = 0; + glGenBuffers(1, &buffer_id); + r_gl_check_error("glGenBuffers"); + + //- dan: bind to appropriate target and upload data + // Default to vertex buffer, but support both targets + GLenum target = GL_ARRAY_BUFFER; + + glBindBuffer(target, buffer_id); + r_gl_check_error("glBindBuffer"); + + // Upload the data or allocate space + glBufferData(target, size, data, usage); + r_gl_check_error("glBufferData"); + + // Unbind the buffer + glBindBuffer(target, 0); + + //- dan: fill buffer structure + buffer->buffer_id = buffer_id; + buffer->kind = kind; + buffer->size = size; + buffer->target = target; + + R_Handle result = r_gl_handle_from_buffer(buffer); + ProfEnd(); + return result; +} + +r_hook void +r_buffer_release(R_Handle handle) +{ + ProfBeginFunction(); + OS_MutexScopeW(r_gl_state->device_rw_mutex) + { + R_GL_Buffer *buffer = r_gl_buffer_from_handle(handle); + SLLStackPush(r_gl_state->first_to_free_buffer, buffer); + } + ProfEnd(); +} + +r_hook void +r_buffer_write(R_Handle handle, Rng1U64 range, void *data) +{ + R_GL_Buffer *buffer = r_gl_buffer_from_handle(handle); + if(buffer != 0 && buffer != &r_gl_buffer_nil) + { + //- dan: check writability + Assert(buffer->kind == R_ResourceKind_Dynamic || buffer->kind == R_ResourceKind_Stream); + + //- dan: clamp range + Rng1U64 buffer_range = r1u64(0, buffer->size); + Rng1U64 write_range = range; + write_range.min = ClampTop(write_range.min, buffer_range.max); + write_range.max = ClampTop(write_range.max, buffer_range.max); + U64 write_size = dim_1u64(write_range); + + if(write_size > 0) + { + //- dan: bind and upload data + // NOTE: Binding to GL_ARRAY_BUFFER here is just for the glBufferSubData call. + glBindBuffer(GL_ARRAY_BUFFER, buffer->buffer_id); + r_gl_check_error("glBindBuffer Write"); + glBufferSubData(GL_ARRAY_BUFFER, write_range.min, write_size, data); + r_gl_check_error("glBufferSubData"); + glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind + } + } +} + +r_hook void +r_begin_frame(void) +{ + OS_MutexScopeW(r_gl_state->device_rw_mutex) + { + // dan: delete queued-up resources + // ... existing code ... + } +} + +r_hook void +r_end_frame(void) +{ + OS_MutexScopeW(r_gl_state->device_rw_mutex) + { + // Clean up textures that were marked for deletion + for(R_GL_Tex2D *tex = r_gl_state->first_to_free_tex2d, *next = 0; + tex != 0; + tex = next) + { + next = tex->next; + if(tex->texture_id != 0) + { + glDeleteTextures(1, &tex->texture_id); + tex->texture_id = 0; + } + tex->generation += 1; + SLLStackPush(r_gl_state->first_free_tex2d, tex); + } + r_gl_state->first_to_free_tex2d = 0; + + // Clean up buffers that were marked for deletion + for(R_GL_Buffer *buf = r_gl_state->first_to_free_buffer, *next = 0; + buf != 0; + buf = next) + { + next = buf->next; + if(buf->buffer_id != 0) + { + glDeleteBuffers(1, &buf->buffer_id); + buf->buffer_id = 0; + } + buf->generation += 1; + SLLStackPush(r_gl_state->first_free_buffer, buf); + } + r_gl_state->first_to_free_buffer = 0; + + // Clean up any framebuffers/renderbuffers marked for deletion + for(R_GL_Framebuffer *fbo = r_gl_state->first_to_free_framebuffer, *fbo_next = 0; + fbo != 0; + fbo = fbo_next) + { + fbo_next = fbo->next; + if(fbo->fbo_id != 0) + { + glDeleteFramebuffers(1, &fbo->fbo_id); + } + SLLStackPush(r_gl_state->first_free_framebuffer, fbo); + } + r_gl_state->first_to_free_framebuffer = 0; + + for(R_GL_Renderbuffer *rbo = r_gl_state->first_to_free_renderbuffer, *rbo_next = 0; + rbo != 0; + rbo = rbo_next) + { + rbo_next = rbo->next; + if(rbo->rbo_id != 0) + { + glDeleteRenderbuffers(1, &rbo->rbo_id); + } + SLLStackPush(r_gl_state->first_free_renderbuffer, rbo); + } + r_gl_state->first_to_free_renderbuffer = 0; + + // Clear buffer flush arena if it exists + if(r_gl_state->buffer_flush_arena) + { + arena_clear(r_gl_state->buffer_flush_arena); + } + + r_gl_check_error("r_end_frame cleanup"); + } + + // debug_log_current_context(); +} + +r_hook void +r_window_begin_frame(OS_Handle window_handle, R_Handle window_equip) // Renamed OS_Handle param +{ + ProfBeginFunction(); + + R_GL_Window *gl_window = r_gl_window_from_handle(window_equip); + if (gl_window == &r_gl_window_nil) { return; } // Early exit if window handle is invalid + + // Ensure context is current for this window + r_gl_make_context_current(gl_window); + if (!r_gl_state->has_valid_context) + { + log_infof("r_window_begin_frame: No valid context, skipping frame for window %p", gl_window); + return; // Cannot proceed without a valid context + } + + // ADDED: Create global resources if they haven't been initialized yet. + // This ensures resources are created *after* a context is successfully made current. + if (!r_gl_state->global_resources_initialized) + { + log_infof("r_window_begin_frame: Creating global resources for the first time."); + r_gl_create_global_resources(); + // Check if resource creation failed (might happen if context became invalid) + if (!r_gl_state->global_resources_initialized) + { + log_infof("r_window_begin_frame: Failed to create global resources, skipping frame."); + return; + } + } + + // Get current window dimensions + Rng2F32 client_rect = os_client_rect_from_window(window_handle); + Vec2S32 resolution = v2s32((S32)client_rect.x1, (S32)client_rect.y1); + + // Ensure minimum resolution + if (resolution.x < 1) resolution.x = 1; + if (resolution.y < 1) resolution.y = 1; + + // Debug info + // fprintf(stderr, "Window resolution: %d x %d\\n", resolution.x, resolution.y); // Less verbose + + //- dan: check / allocate FBO resources if needed + if(gl_window->last_resolution.x != resolution.x || + gl_window->last_resolution.y != resolution.y || + gl_window->stage_fbo == 0) + { + fprintf(stderr, "Creating/resizing FBOs to %d x %d\\n", resolution.x, resolution.y); + + // Delete any existing resources + r_gl_delete_window_fbo_resources(gl_window); + + // Create stage FBO and attachments + glGenTextures(1, &gl_window->stage_color_texture); + glBindTexture(GL_TEXTURE_2D, gl_window->stage_color_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, resolution.x, resolution.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // Set wrap modes explicitly (good practice) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + r_gl_check_error("stage color texture"); + + // REMOVED: Depth texture creation for stage_fbo + // glGenTextures(1, &gl_window->stage_depth_texture); + // glBindTexture(GL_TEXTURE_2D, gl_window->stage_depth_texture); + // glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, resolution.x, resolution.y, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + // r_gl_check_error("stage depth texture"); + + glGenFramebuffers(1, &gl_window->stage_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->stage_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl_window->stage_color_texture, 0); + // REMOVED: Depth texture attachment for stage_fbo + // glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, gl_window->stage_depth_texture, 0); + + // Check FBO completeness + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + const char* status_str = "Unknown"; + switch(status) { + case GL_FRAMEBUFFER_COMPLETE: status_str = "Complete"; break; + case GL_FRAMEBUFFER_UNDEFINED: status_str = "Undefined"; break; // Added GL_FRAMEBUFFER_UNDEFINED + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: status_str = "Incomplete Attachment"; break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: status_str = "Missing Attachment"; break; + case GL_FRAMEBUFFER_UNSUPPORTED: status_str = "Unsupported"; break; + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: status_str = "Incomplete Draw Buffer"; break; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: status_str = "Incomplete Read Buffer"; break; + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: status_str = "Incomplete Multisample"; break; // Added Multisample + case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: status_str = "Incomplete Layer Targets"; break; // Added Layer Targets + } + fprintf(stderr, "stage_fbo status: %s (0x%x)\\n", status_str, status); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + fprintf(stderr, "ERROR: stage_fbo is not complete! Status: 0x%x\\n", status); + // Fall back to default framebuffer or handle error appropriately + glBindFramebuffer(GL_FRAMEBUFFER, 0); + // Potentially delete the incomplete FBO resources? + // r_gl_delete_window_fbo_resources(gl_window); // Careful not to infinite loop if creation always fails + } else { + fprintf(stderr, "stage_fbo is complete: %u\\n", gl_window->stage_fbo); + } + + // Create scratch FBO (if needed for effects like blur) + glGenTextures(1, &gl_window->stage_scratch_color_texture); + glBindTexture(GL_TEXTURE_2D, gl_window->stage_scratch_color_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, resolution.x, resolution.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + r_gl_check_error("scratch color texture"); + + glGenFramebuffers(1, &gl_window->stage_scratch_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->stage_scratch_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl_window->stage_scratch_color_texture, 0); + + // Check scratch FBO completeness + status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + fprintf(stderr, "ERROR: stage_scratch_fbo is not complete! Status: 0x%x\n", status); + } else { + fprintf(stderr, "stage_scratch_fbo is complete\n"); + } + + gl_window->last_resolution = resolution; + } + + // Now bind the stage FBO + GLenum bind_target = (gl_window->stage_fbo != 0) ? gl_window->stage_fbo : 0; + glBindFramebuffer(GL_FRAMEBUFFER, bind_target); + r_gl_check_error("binding framebuffer"); + + // Set up viewport + glViewport(0, 0, resolution.x, resolution.y); + + // Clear with a dark gray + glClearColor(0.2f, 0.2f, 0.2f, 1.f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable blending for UI + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + + r_gl_check_error("UI Blend Func"); +} + +r_hook void +r_window_end_frame(OS_Handle window_handle, R_Handle window_equip) // Renamed OS_Handle param +{ + OS_MutexScope(r_gl_state->device_rw_mutex) + { + R_GL_Window *gl_window = r_gl_window_from_handle(window_equip); + if(gl_window == &r_gl_window_nil) { return; } + + // Get OS window + OS_LNX_Window *lnx_window = (OS_LNX_Window *)window_handle.u64[0]; + if (!lnx_window) { + fprintf(stderr, "ERROR: Invalid OS window handle\n"); + return; + } + + // Render final stage texture to screen using finalize shader + { + glBindFramebuffer(GL_FRAMEBUFFER, 0); // Bind default framebuffer + r_gl_check_error("bind default framebuffer finalize"); + + Vec2S32 size = gl_window->last_resolution; + glViewport(0, 0, size.x, size.y); + r_gl_check_error("glViewport finalize"); + + // Set pipeline state for simple blit + glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); +r_gl_check_error("UI Depth Test Disable"); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_CULL_FACE); + r_gl_check_error("pipeline state finalize"); + + // Bind shader and VAO + glUseProgram(r_gl_state->finalize_shader.program_id); + r_gl_check_error("glUseProgram finalize"); + glBindVertexArray(r_gl_state->fullscreen_vao); + r_gl_check_error("glBindVertexArray finalize"); + + // Bind texture and sampler + glActiveTexture(GL_TEXTURE0); +r_gl_check_error("UI Active Texture"); + glBindTexture(GL_TEXTURE_2D, gl_window->stage_color_texture); + r_gl_check_error("glBindTexture finalize"); + glBindSampler(0, r_gl_state->samplers[R_Tex2DSampleKind_Nearest]); // Nearest for final blit + r_gl_check_error("glBindSampler finalize"); + + // Set texture uniform location + if(r_gl_state->finalize_shader.main_texture_uniform_location != -1) + { + glUniform1i(r_gl_state->finalize_shader.main_texture_uniform_location, 0); // Texture unit 0 + r_gl_check_error("glUniform1i finalize"); + } + + // Draw fullscreen quad + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + r_gl_check_error("glDrawArrays finalize"); + } + + // Swap buffers + glXSwapBuffers(os_lnx_gfx_state->display, lnx_window->window); + + // Unbind anything we've bound + glUseProgram(0); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } +} + +r_hook R_ResourceKind +r_kind_from_tex2d(R_Handle handle) +{ + R_GL_Tex2D *texture = r_gl_tex2d_from_handle(handle); + return texture->kind; +} + +r_hook Vec2S32 +r_size_from_tex2d(R_Handle handle) +{ + R_GL_Tex2D *texture = r_gl_tex2d_from_handle(handle); + return texture->size; +} + +r_hook R_Tex2DFormat +r_format_from_tex2d(R_Handle handle) +{ + R_GL_Tex2D *texture = r_gl_tex2d_from_handle(handle); + return texture->format; +} + +r_hook void +r_fill_tex2d_region(R_Handle handle, Rng2S32 subrect, void *data) +{ + OS_MutexScopeW(r_gl_state->device_rw_mutex) +{ + R_GL_Tex2D *texture = r_gl_tex2d_from_handle(handle); + if(texture == &r_gl_tex2d_nil || texture->texture_id == 0) + { + return; + } + + // Ensure the texture kind allows updates + // Assert(texture->kind == R_ResourceKind_Dynamic || texture->kind == R_ResourceKind_Stream); + if (texture->kind == R_ResourceKind_Static) { + // Cannot update static textures + return; + } + + Vec2S32 size = v2s32(subrect.x1 - subrect.x0, subrect.y1 - subrect.y0); + if (size.x <= 0 || size.y <= 0) { + return; // Invalid region + } + + GLenum format = r_gl_format_from_tex2d_format(texture->format); + GLenum type = r_gl_type_from_tex2d_format(texture->format); + U64 bytes_per_pixel = r_tex2d_format_bytes_per_pixel_table[texture->format]; + + // Set appropriate unpack alignment + // Common alignments are 1, 2, 4, 8. Pixel data rows must start on multiples of this alignment. + GLint previous_alignment; + glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_alignment); + r_gl_check_error("glGetIntegerv GL_UNPACK_ALIGNMENT"); // Check error after get + if (bytes_per_pixel == 1) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + } + else if (bytes_per_pixel == 2) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 2); + } + else { + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // Default for 3, 4, 8, 16 bytes per pixel usually safe + } + r_gl_check_error("glPixelStorei UNPACK"); + + glBindTexture(GL_TEXTURE_2D, texture->texture_id); +r_gl_check_error("UI Bind Texture"); + r_gl_check_error("glBindTexture FillRegion"); + + glTexSubImage2D( + GL_TEXTURE_2D, // target + 0, // level (mipmap level) + subrect.x0, // xoffset + subrect.y0, // yoffset + size.x, // width + size.y, // height + format, // format of pixel data + type, // type of pixel data + data // pointer to image data + ); + r_gl_check_error("glTexSubImage2D"); + + glBindTexture(GL_TEXTURE_2D, 0); + + // Restore previous unpack alignment + glPixelStorei(GL_UNPACK_ALIGNMENT, previous_alignment); + r_gl_check_error("glPixelStorei UNPACK Restore"); + } +} + +////////////////////////////////// +//~ dan: Primitive Topology Mapping + +internal GLenum +r_gl_primitive_from_topology(R_GeoTopologyKind kind) +{ + GLenum result = GL_TRIANGLES; // Default + switch(kind) + { + case R_GeoTopologyKind_Lines: result = GL_LINES; break; + case R_GeoTopologyKind_LineStrip: result = GL_LINE_STRIP; break; + case R_GeoTopologyKind_Triangles: result = GL_TRIANGLES; break; + case R_GeoTopologyKind_TriangleStrip: result = GL_TRIANGLE_STRIP; break; + default: Assert(!"Invalid primitive topology kind"); break; // Should not happen + } + return result; +} + +////////////////////////////////// +//~ dan: UBO Binding Points + +#define R_GL_UNIFORM_BINDING_POINT_RECT 0 +#define R_GL_UNIFORM_BINDING_POINT_BLUR 1 +#define R_GL_UNIFORM_BINDING_POINT_MESH 2 +// #define R_GL_UNIFORM_BINDING_POINT_UI 3 // Define if UI UBO is used + +// Note: The UBO/Instance struct definitions are in render_opengl.h +// Do NOT redefine them here - use those definitions + + +r_hook void +r_window_submit(OS_Handle window_handle, R_Handle window_equip, R_PassList *passes) +{ + OS_MutexScopeW(r_gl_state->device_rw_mutex) + { + //- dan: unpack arguments + R_GL_Window *gl_window = r_gl_window_from_handle(window_equip); + Vec2S32 resolution = gl_window->last_resolution; + + fprintf(stderr, "r_window_submit called - window resolution: %d x %d, FBO: %u\n", + resolution.x, resolution.y, gl_window->stage_fbo); + + // Verify we have valid FBOs + if (gl_window->stage_fbo == 0) { + fprintf(stderr, "ERROR: Invalid stage FBO in r_window_submit!\n"); + } + + // Ensure the correct GL context is current (should be set by r_window_begin_frame) + // TODO: Add check if necessary: if (glXGetCurrentContext() != gl_window->context) { /* make current or error */ } + + //- dan: do passes + for(R_PassNode *pass_n = passes->first; pass_n != 0; pass_n = pass_n->next) + { + R_Pass *pass = &pass_n->v; + fprintf(stderr, "Processing pass kind: %d\n", pass->kind); + + switch(pass->kind) + { + default:{}break; + + //////////////////////// + //- dan: ui rendering pass + // + case R_PassKind_UI: + { + //- dan: unpack params + R_PassParams_UI *params = pass->params_ui; + R_BatchGroup2DList *rect_batch_groups = ¶ms->rects; + + // Count the batch groups manually instead of using node_count + int batch_group_count = 0; + for(R_BatchGroup2DNode *node = rect_batch_groups->first; node != NULL; node = node->next) { + batch_group_count++; + } + fprintf(stderr, "UI pass has %d batch groups\n", batch_group_count); + + //- dan: set common GL state for UI + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + +r_gl_check_error("UI Blend Func"); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); +r_gl_check_error("UI Depth Test Disable"); + glEnable(GL_SCISSOR_TEST); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CW); + glViewport(0, 0, resolution.x, resolution.y); // Full window viewport for UI passes + + // Check if VAO and shader program are valid before using them + if (r_gl_state->rect_vao == 0 || r_gl_state->rect_shader.program_id == 0) { + fprintf(stderr, "Error: UI rendering resources not valid (VAO: %u, Shader: %u)\n", + r_gl_state->rect_vao, r_gl_state->rect_shader.program_id); + break; // Skip this pass if resources are invalid + } + + glBindVertexArray(r_gl_state->rect_vao); +r_gl_check_error("UI Bind VAO"); + glUseProgram(r_gl_state->rect_shader.program_id); + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + fprintf(stderr, "GL Error after binding VAO/shader: 0x%x\n", err); + } + + //- dan: draw each batch group + for(R_BatchGroup2DNode *group_n = rect_batch_groups->first; group_n != 0; group_n = group_n->next) + { + R_BatchList *batches = &group_n->batches; + R_BatchGroup2DParams *group_params = &group_n->params; + + // <<< ADD LOGGING HERE >>> + fprintf(stderr, " UI Batch Group:\n"); + fprintf(stderr, " Texture Handle: %lu\n", group_params->tex.u64[0]); // Fixed format specifier + fprintf(stderr, " Sample Kind: %d\n", group_params->tex_sample_kind); + fprintf(stderr, " Transform: [[%.2f, %.2f, %.2f], [%.2f, %.2f, %.2f], [%.2f, %.2f, %.2f]]\n", + group_params->xform.v[0][0], group_params->xform.v[0][1], group_params->xform.v[0][2], + group_params->xform.v[1][0], group_params->xform.v[1][1], group_params->xform.v[1][2], + group_params->xform.v[2][0], group_params->xform.v[2][1], group_params->xform.v[2][2]); + fprintf(stderr, " Clip Rect: (%.1f, %.1f) -> (%.1f, %.1f)\n", + group_params->clip.x0, group_params->clip.y0, group_params->clip.x1, group_params->clip.y1); + fprintf(stderr, " Transparency: %.2f\n", group_params->transparency); + + + // dan: bind sampler + glBindSampler(0, r_gl_state->samplers[group_params->tex_sample_kind]); // Bind to texture unit 0 + r_gl_check_error("glBindSampler UI"); + + // dan: bind texture + R_GL_Tex2D *texture = r_gl_tex2d_from_handle(group_params->tex); + if(texture == &r_gl_tex2d_nil || texture->texture_id == 0) + { + texture = r_gl_tex2d_from_handle(r_gl_state->backup_texture); + } + glActiveTexture(GL_TEXTURE0); +r_gl_check_error("UI Active Texture"); + glBindTexture(GL_TEXTURE_2D, texture->texture_id); +r_gl_check_error("UI Bind Texture"); + if(r_gl_state->rect_shader.main_texture_uniform_location != -1) + { + glUniform1i(r_gl_state->rect_shader.main_texture_uniform_location, 0); // Texture unit 0 + } + else + { + // Try with the known uniform name if location wasn't found during initialization + GLint tex_loc = glGetUniformLocation(r_gl_state->rect_shader.program_id, "main_t2d"); + if (tex_loc != -1) { + glUniform1i(tex_loc, 0); + // Cache it for future use + r_gl_state->rect_shader.main_texture_uniform_location = tex_loc; + } + } + r_gl_check_error("Texture Binding UI"); + + + // dan: upload uniforms + { + R_GL_Uniforms_Rect uniforms = {0}; + uniforms.viewport_size_px = v2f32(resolution.x, resolution.y); + uniforms.opacity = 1.0f - group_params->transparency; + uniforms.texture_t2d_size_px = v2f32(texture->size.x > 0 ? texture->size.x : 1.f, + texture->size.y > 0 ? texture->size.y : 1.f); // Avoid divide by zero + + // Texture channel mapping based on format + Mat4x4F32 tex_map_matrix = m4x4f32(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1); // Identity + if(texture->format == R_Tex2DFormat_R8) + { + // Map R channel to R,G,B,A (effectively grayscale for shader) + // Shader: albedo_sample = texture(...) * texture_sample_channel_map; + // Matrix should be column-major for GLSL. C=Row, GL=Col. Need transpose. + // To get (R,R,R,R): First column of transposed matrix = (1,1,1,1). + // C Row-Major Matrix: + // [ 1 0 0 0 ] -> Col 0 -> GLSL sample.r + // [ 1 0 0 0 ] -> Col 1 -> GLSL sample.g + // [ 1 0 0 0 ] -> Col 2 -> GLSL sample.b + // [ 1 0 0 0 ] -> Col 3 -> GLSL sample.a + tex_map_matrix = m4x4f32(1,1,1,1, 0,0,0,0, 0,0,0,0, 0,0,0,0); + } + Mat4x4F32 transposed = transpose_4x4f32(tex_map_matrix); + for (int i = 0; i < 4; i++) { + uniforms.texture_sample_channel_map[i] = v4f32( + transposed.v[i][0], + transposed.v[i][1], + transposed.v[i][2], + transposed.v[i][3] + ); + } + + // Transform: C Mat3x3F32 (row-major) to GLSL mat3 (column-major in std140) + Mat3x3F32 c_xform = group_params->xform; + // Manually transpose and copy into the std140 padded structure + // Remember GLSL mat3 is represented as 3 Vec4F32 columns in std140 layout + uniforms.xform[0] = v4f32(c_xform.v[0][0], c_xform.v[1][0], c_xform.v[2][0], 0); // Col 0 + uniforms.xform[1] = v4f32(c_xform.v[0][1], c_xform.v[1][1], c_xform.v[2][1], 0); // Col 1 + uniforms.xform[2] = v4f32(c_xform.v[0][2], c_xform.v[1][2], c_xform.v[2][2], 0); // Col 2 + + // Calculate xform_scale from the GLSL-layout columns (before sending to GPU) + Vec2F32 gl_col0 = v2f32(uniforms.xform[0].x, uniforms.xform[0].y); + Vec2F32 gl_col1 = v2f32(uniforms.xform[1].x, uniforms.xform[1].y); + uniforms.xform_scale.x = length_2f32(gl_col0); + uniforms.xform_scale.y = length_2f32(gl_col1); + uniforms.xform_scale.x = Max(uniforms.xform_scale.x, 0.0001f); // Avoid zero scale + uniforms.xform_scale.y = Max(uniforms.xform_scale.y, 0.0001f); + + // <<< ADD LOGGING HERE >>> + fprintf(stderr, " Uniforms Upload:\n"); + fprintf(stderr, " Viewport: %.1f x %.1f\n", uniforms.viewport_size_px.x, uniforms.viewport_size_px.y); + fprintf(stderr, " Opacity: %.2f\n", uniforms.opacity); + fprintf(stderr, " Tex Size: %.1f x %.1f\n", uniforms.texture_t2d_size_px.x, uniforms.texture_t2d_size_px.y); + // Optional: Log matrices if needed, they can be verbose + // fprintf(stderr, " Tex Map Matrix: ...\n"); + // fprintf(stderr, " Xform Matrix (ColMajor): [[%.2f, %.2f, %.2f], [%.2f, %.2f, %.2f], [%.2f, %.2f, %.2f]]\n", ...); + fprintf(stderr, " Xform Scale: %.2f, %.2f\n", uniforms.xform_scale.x, uniforms.xform_scale.y); + + + // Bind and update UBO + glBindBufferBase(GL_UNIFORM_BUFFER, R_GL_UNIFORM_BINDING_POINT_RECT, r_gl_state->uniform_type_kind_buffers[R_GL_UniformTypeKind_Rect]); + glBindBuffer(GL_UNIFORM_BUFFER, r_gl_state->uniform_type_kind_buffers[R_GL_UniformTypeKind_Rect]); // Rebind for glBufferSubData + r_gl_check_error("glBindBufferBase Rect UBO"); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(uniforms), &uniforms); + r_gl_check_error("UI UBO SubData"); + r_gl_check_error("glBufferSubData Rect UBO"); + //glBindBuffer(GL_UNIFORM_BUFFER, 0); // REMOVED: Unbind UBO after update + } + + // dan: upload instance data + U64 hardcoded_size = 0; // Size for hardcoded data + GLsizei instance_count = 0; // Instance count for draw call + U64 total_instance_bytes = batches->byte_count; + if (total_instance_bytes > 0 && batches->bytes_per_inst > 0) { + // <<< ADD LOGGING HERE (Log first instance of the first batch) >>> + if (batches->first && batches->first->v.byte_count >= sizeof(R_Rect2DInst) && batches->bytes_per_inst == sizeof(R_Rect2DInst)) { + R_Rect2DInst *first_inst = (R_Rect2DInst *)batches->first->v.v; + fprintf(stderr, " First Instance Data:\n"); + fprintf(stderr, " Dst: (%.1f, %.1f) -> (%.1f, %.1f)\n", first_inst->dst.x0, first_inst->dst.y0, first_inst->dst.x1, first_inst->dst.y1); + fprintf(stderr, " Src: (%.1f, %.1f) -> (%.1f, %.1f)\n", first_inst->src.x0, first_inst->src.y0, first_inst->src.x1, first_inst->src.y1); + // Use integer indices instead of Corner_ enum names and x,y,z,w members + fprintf(stderr, " Colors[BL]: (%.2f, %.2f, %.2f, %.2f)\n", first_inst->colors[0].x, first_inst->colors[0].y, first_inst->colors[0].z, first_inst->colors[0].w); + fprintf(stderr, " Colors[TL]: (%.2f, %.2f, %.2f, %.2f)\n", first_inst->colors[1].x, first_inst->colors[1].y, first_inst->colors[1].z, first_inst->colors[1].w); + fprintf(stderr, " Colors[BR]: (%.2f, %.2f, %.2f, %.2f)\n", first_inst->colors[2].x, first_inst->colors[2].y, first_inst->colors[2].z, first_inst->colors[2].w); + fprintf(stderr, " Colors[TR]: (%.2f, %.2f, %.2f, %.2f)\n", first_inst->colors[3].x, first_inst->colors[3].y, first_inst->colors[3].z, first_inst->colors[3].w); + fprintf(stderr, " Radii[BL,TL,BR,TR]: %.1f, %.1f, %.1f, %.1f\n", first_inst->corner_radii[0], first_inst->corner_radii[1], first_inst->corner_radii[2], first_inst->corner_radii[3]); + fprintf(stderr, " Border: %.1f, Softness: %.1f, WhiteOverride: %.1f\n", first_inst->border_thickness, first_inst->edge_softness, first_inst->white_texture_override); + } + + glBindBuffer(GL_ARRAY_BUFFER, r_gl_state->instance_vbo); + r_gl_check_error("glBindBuffer Instance VBO"); + + GLint current_vbo_size = 0; + glGetBufferParameteriv(GL_ARRAY_BUFFER, GL_BUFFER_SIZE, ¤t_vbo_size); + r_gl_check_error("glGetBufferParameteriv Instance VBO Size"); + + if (total_instance_bytes > (U64)current_vbo_size) { + glBufferData(GL_ARRAY_BUFFER, total_instance_bytes, NULL, GL_STREAM_DRAW); + r_gl_check_error("Instance VBO Resize (glBufferData)"); + } + U64 current_offset = 0; + for(R_BatchNode *batch_n = batches->first; batch_n != 0; batch_n = batch_n->next) + { + U64 copy_size = batch_n->v.byte_count; + if (copy_size > 0) { + Assert(current_offset + copy_size <= total_instance_bytes); + glBufferSubData(GL_ARRAY_BUFFER, (GLintptr)current_offset, (GLsizeiptr)copy_size, batch_n->v.v); + r_gl_check_error("Instance VBO Upload (glBufferSubData)"); + current_offset += copy_size; + } + } + Assert(current_offset == total_instance_bytes); + instance_count = total_instance_bytes / batches->bytes_per_inst; + } else { + instance_count = 0; // No data or invalid size info + } + + // dan: setup scissor rect + { + Rng2F32 clip_rect_f = group_params->clip; + Rng2S32 clip_rect_i = {0}; + // Check for zero rect which means no clipping + if(clip_rect_f.x0 == 0 && clip_rect_f.y0 == 0 && clip_rect_f.x1 == 0 && clip_rect_f.y1 == 0) + { + clip_rect_i = r2s32p(0, 0, resolution.x, resolution.y); + } + else + { + // Clamp and round clip rect provided in top-left coordinates + clip_rect_i = r2s32p(round_f32_s32(clip_rect_f.x0), round_f32_s32(clip_rect_f.y0), round_f32_s32(clip_rect_f.x1), round_f32_s32(clip_rect_f.y1)); + clip_rect_i.x0 = Clamp(0, clip_rect_i.x0, resolution.x); + clip_rect_i.y0 = Clamp(0, clip_rect_i.y0, resolution.y); + clip_rect_i.x1 = Clamp(0, clip_rect_i.x1, resolution.x); + clip_rect_i.y1 = Clamp(0, clip_rect_i.y1, resolution.y); + } + // GL scissor origin is bottom-left. Convert Y. + // y_gl = resolution.y - y_topleft + // height_gl = y_topleft_bottom - y_topleft_top + GLint y_gl = resolution.y - clip_rect_i.y1; + GLsizei height_gl = Max(0, clip_rect_i.y1 - clip_rect_i.y0); + GLsizei width_gl = Max(0, clip_rect_i.x1 - clip_rect_i.x0); + glScissor(clip_rect_i.x0, y_gl, width_gl, height_gl); + r_gl_check_error("glScissor UI"); + } + + // dan: bind target FBO (UI draws to stage FBO) + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->stage_fbo); + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + fprintf(stderr, "Error binding stage FBO for UI drawing: 0x%x\n", err); + } + + // Verify the FBO is actually bound + GLint current_fbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤t_fbo); + if (current_fbo != gl_window->stage_fbo) { + fprintf(stderr, "ERROR: Wrong FBO bound! Expected %u, got %d\n", + gl_window->stage_fbo, current_fbo); + } + +r_gl_check_error("glBindFramebuffer UI Stage"); + + // dan: draw + if (instance_count > 0) { // Use the calculated instance_count + // Check scissor settings before draw + GLint scissor[4]; + glGetIntegerv(GL_SCISSOR_BOX, scissor); + // <<< ADD LOGGING HERE >>> + fprintf(stderr, " Draw Call: %d instances, Scissor(%d, %d, %d, %d)\n", + instance_count, scissor[0], scissor[1], scissor[2], scissor[3]); + + // Extra verification of viewport and other states + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + // fprintf(stderr, "UI Draw: Viewport(%d, %d, %d, %d)\n", + // viewport[0], viewport[1], viewport[2], viewport[3]); + + // Verify transform feedback isn't active (can prevent drawing) + glDisable(GL_RASTERIZER_DISCARD); + + // Ensure blend mode is set correctly + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + + r_gl_check_error("UI Blend Func"); + + // Draw the instances + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, instance_count); // Use calculated instance_count + r_gl_check_error("glDrawArraysInstanced UI"); + } + + // Reset state to avoid affecting subsequent batches + glDisable(GL_SCISSOR_TEST); + glEnable(GL_SCISSOR_TEST); + r_gl_check_error("UI Draw State Reset"); + } // end batch group loop + + // Reset texture/sampler bindings for unit 0 + glActiveTexture(GL_TEXTURE0); +r_gl_check_error("UI Active Texture"); + glBindTexture(GL_TEXTURE_2D, 0); + glBindSampler(0, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind instance VBO + glBindVertexArray(0); // Unbind VAO + glUseProgram(0); // Unbind shader program + + }break; + + //////////////////////// + //- dan: blur rendering pass + // + case R_PassKind_Blur: + { + R_PassParams_Blur *params = pass->params_blur; + + //- dan: skip if zero size + if(params->blur_size <= 0.001f) + + //- dan: common blur state + glBindVertexArray(r_gl_state->fullscreen_vao); // VAO for fullscreen quad (no attributes needed) + glUseProgram(r_gl_state->blur_shader.program_id); + glBindSampler(0, r_gl_state->samplers[R_Tex2DSampleKind_Linear]); // Use linear sampling for blur + glDisable(GL_BLEND); // Blur passes usually overwrite destination + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); +r_gl_check_error("UI Depth Test Disable"); + glDisable(GL_SCISSOR_TEST); // Blur affects whole texture (unless masked later) + glDisable(GL_CULL_FACE); + glViewport(0, 0, resolution.x, resolution.y); // Full texture viewport + r_gl_check_error("Blur Pass Setup"); + + // dan: set up uniforms (calculate kernel, fill struct) + R_GL_Uniforms_Blur uniforms = {0}; + { + // Calculate Gaussian weights + F32 weights[ ArrayCount(uniforms.kernel)*2 + 1 ] = {0}; // Space for center + pairs + U64 kernel_radius = Min((U64)ArrayCount(uniforms.kernel), (U64)ceilf((params->blur_size - 1.0f) / 2.0f)); + U64 kernel_size = kernel_radius * 2 + 1; + F32 sigma = Max(0.01f, (F32)kernel_radius / 2.0f); // Sigma heuristic + + F32 weight_sum = 0.0f; + for (S64 i = -(S64)kernel_radius; i <= (S64)kernel_radius; ++i) { + F32 w = expf(-((F32)i * (F32)i) / (2.0f * sigma * sigma)); + weights[i + kernel_radius] = w; + weight_sum += w; + } + // Normalize weights + if (weight_sum > 0.0001f) { + for (U64 i = 0; i < kernel_size; ++i) { + weights[i] /= weight_sum; + } + } else { // Handle zero sum case (e.g., blur_size 1 -> radius 0) + MemoryZeroArray(weights); + weights[0] = 1.0f; + kernel_size = 1; + kernel_radius = 0; + } + + // Prepare kernel for bilinear optimization (combine pairs of weights) + uniforms.kernel[0].x = weights[kernel_radius]; // Center tap weight + uniforms.kernel[0].y = 0.0f; // Center tap offset + uniforms.blur_count = 1; // Start with center tap + for (U64 i = 1; i <= kernel_radius; i += 2) { + if (uniforms.blur_count >= ArrayCount(uniforms.kernel)) break; // Check bounds + F32 w0 = weights[kernel_radius + i]; + F32 w1 = (i + 1 <= kernel_radius) ? weights[kernel_radius + i + 1] : 0.0f; + F32 w_sum = w0 + w1; + if (w_sum > 1e-6f) { + F32 offset = (F32)i + (w1 / w_sum); // Weighted offset from center + uniforms.kernel[uniforms.blur_count].x = w_sum; + uniforms.kernel[uniforms.blur_count].y = offset; + uniforms.blur_count++; + } + } + + // Common blur parameters + uniforms.rect = v4f32(params->rect.x0, params->rect.y0, params->rect.x1, params->rect.y1); + uniforms.viewport_size = v2f32(resolution.x, resolution.y); + MemoryCopy(&uniforms.corner_radii_px, params->corner_radii, sizeof(params->corner_radii)); + } + + // Bind UBO once + glBindBufferBase(GL_UNIFORM_BUFFER, R_GL_UNIFORM_BINDING_POINT_BLUR, r_gl_state->uniform_type_kind_buffers[R_GL_UniformTypeKind_Blur]); + r_gl_check_error("Blur UBO Bind"); + + glActiveTexture(GL_TEXTURE0); +r_gl_check_error("UI Active Texture"); + if (r_gl_state->blur_shader.main_texture_uniform_location != -1) { + glUniform1i(r_gl_state->blur_shader.main_texture_uniform_location, 0); // Use texture unit 0 + } + + // Pass 1: Horizontal Blur (Stage Color -> Scratch Color) + { + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->stage_scratch_fbo); + r_gl_check_error("glBindFramebuffer Blur Scratch"); + uniforms.direction = v2f32(1.0f, 0.0f); // Direction (texel size applied in shader) + glBindBuffer(GL_UNIFORM_BUFFER, r_gl_state->uniform_type_kind_buffers[R_GL_UniformTypeKind_Blur]); // Bind for update + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(uniforms), &uniforms); // Update UBO +r_gl_check_error("UI UBO SubData"); + glBindBuffer(GL_UNIFORM_BUFFER, 0); // Unbind after update + r_gl_check_error("glBufferSubData Blur H"); + glBindTexture(GL_TEXTURE_2D, gl_window->stage_color_texture); // Input texture + r_gl_check_error("glBindTexture Blur H Input"); + glBindVertexArray(r_gl_state->fullscreen_vao); + r_gl_check_error("glBindVertexArray fullscreen_vao Blur"); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // Draw fullscreen quad + r_gl_check_error("glDrawArrays Blur H"); + glBindTexture(GL_TEXTURE_2D, 0); // Unbind texture + r_gl_check_error("glBindTexture Blur H Unbind"); + } + + // Pass 2: Vertical Blur (Scratch Color -> Stage Color) + { + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->stage_fbo); // Target main stage FBO + r_gl_check_error("glBindFramebuffer Blur Stage"); + uniforms.direction = v2f32(0.0f, 1.0f); // Direction + glBindBuffer(GL_UNIFORM_BUFFER, r_gl_state->uniform_type_kind_buffers[R_GL_UniformTypeKind_Blur]); // Explicitly bind before update + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(uniforms), &uniforms); // Update UBO +r_gl_check_error("UI UBO SubData"); + glBindBuffer(GL_UNIFORM_BUFFER, 0); // Unbind after update + r_gl_check_error("glBufferSubData Blur V"); + glBindTexture(GL_TEXTURE_2D, gl_window->stage_scratch_color_texture); // Input texture + r_gl_check_error("glBindTexture Blur V Input"); + glBindVertexArray(r_gl_state->fullscreen_vao); + r_gl_check_error("glBindVertexArray fullscreen_vao Blur"); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // Draw fullscreen quad + r_gl_check_error("glDrawArrays Blur V"); + glBindTexture(GL_TEXTURE_2D, 0); // Unbind texture + r_gl_check_error("glBindTexture Blur V Unbind"); + } + + // Cleanup blur state + glBindTexture(GL_TEXTURE_2D, 0); + glBindSampler(0, 0); + glEnable(GL_BLEND); // Re-enable blend potentially needed by subsequent passes + glEnable(GL_SCISSOR_TEST); // Re-enable scissor + + // Unbind the VAO after both blur passes are done + glBindVertexArray(0); + r_gl_check_error("glBindVertexArray 0 Blur End"); + }break; + + + //////////////////////// + //- dan: 3d geometry rendering pass + // + case R_PassKind_Geo3D: + { + //- dan: unpack params + R_PassParams_Geo3D *params = pass->params_geo3d; + R_BatchGroup3DMap *mesh_group_map = ¶ms->mesh_batches; + + //- dan: common Geo3D state + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CW); // Standard convention + glEnable(GL_BLEND); // Blend fragments onto target + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + +r_gl_check_error("UI Blend Func"); + glEnable(GL_SCISSOR_TEST); // Use viewport as scissor initially + glBindVertexArray(r_gl_state->mesh_vao); // VAO for mesh vertex & instance attributes + glUseProgram(r_gl_state->mesh_shader.program_id); + r_gl_check_error("Geo3D Pass Setup"); + + //- dan: bind & clear Geo3D FBO + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->geo3d_fbo); + r_gl_check_error("glBindFramebuffer Geo3D"); + Vec4F32 bg_color = {0.1f, 0.1f, 0.1f, 0.0f}; // Example clear color (transparent black or grey) + glClearBufferfv(GL_COLOR, 0, bg_color.v); // Clear color attachment 0 + glClearBufferfi(GL_DEPTH_STENCIL, 0, 1.0f, 0); // Clear depth to 1.0, stencil to 0 + r_gl_check_error("Geo3D FBO Clear"); + + + //- dan: set viewport & scissor from params + Rng2F32 viewport_rng_f = params->viewport; // Top-left coordinate system + Vec2S32 viewport_pos_tl = v2s32(round_f32_s32(viewport_rng_f.x0), round_f32_s32(viewport_rng_f.y0)); + Vec2S32 viewport_dim = v2s32(Max(0, round_f32_s32(viewport_rng_f.x1 - viewport_rng_f.x0)), + Max(0, round_f32_s32(viewport_rng_f.y1 - viewport_rng_f.y0))); + viewport_pos_tl.x = Clamp(0, viewport_pos_tl.x, resolution.x); + viewport_pos_tl.y = Clamp(0, viewport_pos_tl.y, resolution.y); + // Clamp dimensions based on position + viewport_dim.x = Clamp(0, viewport_dim.x, resolution.x - viewport_pos_tl.x); + viewport_dim.y = Clamp(0, viewport_dim.y, resolution.y - viewport_pos_tl.y); + + // GL viewport/scissor origin is bottom-left. Convert Y. + GLint y_gl = resolution.y - (viewport_pos_tl.y + viewport_dim.y); + glViewport(viewport_pos_tl.x, y_gl, viewport_dim.x, viewport_dim.y); + glScissor(viewport_pos_tl.x, y_gl, viewport_dim.x, viewport_dim.y); + r_gl_check_error("Geo3D Viewport/Scissor"); + + + // dan: setup uniforms (view/projection) + { + R_GL_Uniforms_Mesh uniforms = {0}; + // Combine and transpose projection * view matrix + // Flip Y in projection matrix for OpenGL coordinate system + Mat4x4F32 projection_gl = params->projection; + projection_gl.v[1][1] *= -1.0f; + Mat4x4F32 view_proj_c = mul_4x4f32(projection_gl, params->view); + uniforms.view_proj_matrix = transpose_4x4f32(view_proj_c); // Transpose for GLSL column-major + + glBindBufferBase(GL_UNIFORM_BUFFER, R_GL_UNIFORM_BINDING_POINT_MESH, r_gl_state->uniform_type_kind_buffers[R_GL_UniformTypeKind_Mesh]); + r_gl_check_error("glBindBufferBase Mesh UBO"); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(uniforms), &uniforms); +r_gl_check_error("UI UBO SubData"); + r_gl_check_error("glBufferSubData Mesh UBO"); + } + + + // dan: draw mesh batches (instanced) + for(U64 slot_idx = 0; slot_idx < mesh_group_map->slots_count; slot_idx += 1) + { + for(R_BatchGroup3DMapNode *n = mesh_group_map->slots[slot_idx]; n != 0; n = n->next) + { + R_BatchList *batches = &n->batches; + R_BatchGroup3DParams *group_params = &n->params; + + + R_GL_Buffer *vtx_buffer = r_gl_buffer_from_handle(group_params->mesh_vertices); + R_GL_Buffer *idx_buffer = r_gl_buffer_from_handle(group_params->mesh_indices); + + if (vtx_buffer == &r_gl_buffer_nil || idx_buffer == &r_gl_buffer_nil || vtx_buffer->buffer_id == 0 || idx_buffer->buffer_id == 0) { + log_info(str8_lit("Skipping Geo3D batch group due to invalid vertex or index buffer.")); + } + + // Bind vertex buffer and ensure VAO attributes are set correctly for it + // VAO should already be bound (glBindVertexArray(r_gl_state->mesh_vao)) + glBindBuffer(GL_ARRAY_BUFFER, vtx_buffer->buffer_id); + r_gl_check_error("glBindBuffer Geo3D VBO"); + // Re-specify vertex attrib pointers as VBO binding is part of VAO state in Core Profile *only if* + // GL_VERTEX_ARRAY_BINDING is used, which is not the case here. So we must reset them. + size_t mesh_vert_stride = 11 * sizeof(F32); // Pos(3), Norm(3), Tex(2), Col(3) + glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, mesh_vert_stride, (void*)(0 * sizeof(F32))); // Pos + glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, mesh_vert_stride, (void*)(3 * sizeof(F32))); // Norm + glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, mesh_vert_stride, (void*)(6 * sizeof(F32))); // Tex + glEnableVertexAttribArray(3); glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, mesh_vert_stride, (void*)(8 * sizeof(F32))); // Col + // Ensure vertex attrib divisors are 0 for per-vertex data + glVertexAttribDivisor(0, 0); + glVertexAttribDivisor(1, 0); + glVertexAttribDivisor(2, 0); + glVertexAttribDivisor(3, 0); + r_gl_check_error("glVertexAttribPointer Geo3D Vertex"); + + // Bind index buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idx_buffer->buffer_id); + r_gl_check_error("glBindBuffer Geo3D IBO"); + + // Upload instance data (transforms) to the shared instance VBO + U64 total_instance_bytes = batches->byte_count; // Total bytes already available in batches + if (total_instance_bytes > 0) { + glBindBuffer(GL_ARRAY_BUFFER, r_gl_state->instance_vbo); +r_gl_check_error("UI Bind Instance VBO"); + r_gl_check_error("glBindBuffer Geo3D Instance VBO"); + + // Check if VBO needs resize/reallocation + GLint current_vbo_size = 0; + glGetBufferParameteriv(GL_ARRAY_BUFFER, GL_BUFFER_SIZE, ¤t_vbo_size); + r_gl_check_error("glGetBufferParameteriv Instance VBO Size Geo3D"); + + B32 vbo_resized = 0; + if (total_instance_bytes > (U64)current_vbo_size) { + glBufferData(GL_ARRAY_BUFFER, total_instance_bytes, NULL, GL_STREAM_DRAW); // Orphan & Resize + r_gl_check_error("Instance VBO Resize (glBufferData) Geo3D"); + vbo_resized = 1; + } + + // Map and copy instance data + void *instance_data_ptr = glMapBufferRange(GL_ARRAY_BUFFER, 0, total_instance_bytes, GL_MAP_WRITE_BIT | (vbo_resized ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT) ); + if (instance_data_ptr) { + U64 offset = 0; + for(R_BatchNode *batch_n = batches->first; batch_n != 0; batch_n = batch_n->next) + { + // Assuming batch_n->v.v points to R_Mesh3DInst array + U64 copy_size = batch_n->v.byte_count; + Assert(offset + copy_size <= total_instance_bytes); + MemoryCopy((U8*)instance_data_ptr + offset, batch_n->v.v, copy_size); + offset += copy_size; + } + Assert(offset == total_instance_bytes); + glUnmapBuffer(GL_ARRAY_BUFFER); + r_gl_check_error("Instance VBO Upload (glUnmapBuffer) Geo3D"); + } else { + r_gl_check_error("glMapBufferRange Failed Geo3D"); + glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind on failure + continue; // Skip draw if map failed + } + + // Ensure instance attribute pointers are correctly set *after* binding the instance VBO + // These should point to the r_gl_state->instance_vbo + size_t mesh_inst_stride = sizeof(R_Mesh3DInst); // Assuming R_Mesh3DInst is the instance structure + size_t xform_offset = OffsetOf(R_Mesh3DInst, xform); + glEnableVertexAttribArray(4); glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, mesh_inst_stride, (void*)(xform_offset + 0*sizeof(Vec4F32))); glVertexAttribDivisor(4, 1); + glEnableVertexAttribArray(5); glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, mesh_inst_stride, (void*)(xform_offset + 1*sizeof(Vec4F32))); glVertexAttribDivisor(5, 1); + glEnableVertexAttribArray(6); glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, mesh_inst_stride, (void*)(xform_offset + 2*sizeof(Vec4F32))); glVertexAttribDivisor(6, 1); + glEnableVertexAttribArray(7); glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, mesh_inst_stride, (void*)(xform_offset + 3*sizeof(Vec4F32))); glVertexAttribDivisor(7, 1); + r_gl_check_error("glVertexAttribPointer Geo3D Instance"); + } else { + // Disable instance attributes if not drawing instanced? No, draw with instance count 0? + // Or better, skip the draw call if total_inst_count is 0. + } + + // Draw instanced + GLenum topology = r_gl_primitive_from_topology(group_params->mesh_geo_topology); + U64 index_count = idx_buffer->size / sizeof(U32); // Assuming U32 indices + if (index_count > 0 && batches->byte_count > 0 && batches->bytes_per_inst > 0) { + glDrawElementsInstanced(topology, index_count, GL_UNSIGNED_INT, (void*)0, batches->byte_count / batches->bytes_per_inst); + r_gl_check_error("glDrawElementsInstanced Geo3D"); + } + } + } // End Geo3D group map loop + + // Unbind element array buffer before compositing if needed + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind the instance VBO now + + //- dan: composite geo3d result into stage fbo + glBindFramebuffer(GL_FRAMEBUFFER, gl_window->stage_fbo); // Target main stage FBO + r_gl_check_error("glBindFramebuffer Geo3D Composite"); + glUseProgram(r_gl_state->geo3d_composite_shader.program_id); + r_gl_check_error("glUseProgram Geo3D Composite"); + glBindVertexArray(r_gl_state->fullscreen_vao); // Use fullscreen quad VAO + r_gl_check_error("glBindVertexArray Geo3D Composite"); + + // Reset viewport and scissor to cover the composite area (usually full window or target rect) + // Use the clip rect from the Geo3D params for compositing area? Or always full stage? Assume full stage. + glViewport(0, 0, resolution.x, resolution.y); // Full window viewport + glScissor(0, 0, resolution.x, resolution.y); + r_gl_check_error("glScissor"); + glDisable(GL_SCISSOR_TEST); // Ensure scissor test is off for fullscreen blit + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); + r_gl_check_error("UI Blend Func"); + // Or potentially use GL_ONE, GL_ZERO if result is opaque and overwrites? Assume alpha blend. + // Bind the Geo3D color texture as input + glActiveTexture(GL_TEXTURE0); +r_gl_check_error("UI Active Texture"); + glBindTexture(GL_TEXTURE_2D, gl_window->geo3d_color_texture); + r_gl_check_error("glBindTexture Geo3D Composite Input"); + glBindSampler(0, r_gl_state->samplers[R_Tex2DSampleKind_Nearest]); // Nearest neighbor for composite + r_gl_check_error("glBindSampler Geo3D Composite"); + if (r_gl_state->geo3d_composite_shader.main_texture_uniform_location != -1) { + glUniform1i(r_gl_state->geo3d_composite_shader.main_texture_uniform_location, 0); // Texture unit 0 + } + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // Draw fullscreen quad + r_gl_check_error("Geo3D Composite Draw"); + + // Cleanup composite state + glBindTexture(GL_TEXTURE_2D, 0); + glBindSampler(0, 0); + glUseProgram(0); // Unbind shader + glBindVertexArray(0); // Unbind VAO + }break; + } // end switch pass->kind + } // end pass loop + + //- dan: unbind resources / reset state after all passes + glBindVertexArray(0); + glUseProgram(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); // Bind default framebuffer + glDisable(GL_SCISSOR_TEST); // Disable scissor if default state requires it + + } // end mutex scope +} + +// Debug function to draw a simple test pattern +internal void +r_gl_debug_draw_test_pattern(void) +{ + // Create a simple 2x2 checkerboard texture + GLuint test_texture; + glGenTextures(1, &test_texture); + glBindTexture(GL_TEXTURE_2D, test_texture); + + // Create a black and white checkerboard + unsigned char pattern[] = { + 255, 255, 255, 255, 0, 0, 0, 255, + 0, 0, 0, 255, 255, 255, 255, 255 + }; + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Create a simple fullscreen quad to display it + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + // Draw fullscreen quad + glUseProgram(r_gl_state->finalize_shader.program_id); + glActiveTexture(GL_TEXTURE0); +r_gl_check_error("UI Active Texture"); + glBindTexture(GL_TEXTURE_2D, test_texture); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + // Clean up + glBindVertexArray(0); + glUseProgram(0); + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &test_texture); + glDeleteVertexArrays(1, &vao); +} + +#define r_gl_check_error(op) r_gl_check_error_line_file(op, __LINE__, __FILE__) + +// Add this function to help debug context issues +internal void +debug_log_current_context(void) +{ + // IMPORTANT: Ensure a valid context exists before calling GL functions. + if(!r_gl_state || !r_gl_state->has_valid_context) + { + fprintf(stderr, "Debug Log: No valid GL context detected by r_gl_state->has_valid_context.\n"); + return; + } + + // Use the correct display handle stored in the state + Display *display = r_gl_state->display; + if (!display) { + fprintf(stderr, "Debug Log: No X11 display found in r_gl_state.\n"); + return; + } + + GLXContext current = glXGetCurrentContext(); + fprintf(stderr, "Debug Log: Current GLX context: %p (Display: %p, Window: %lu)\n", + current, glXGetCurrentDisplay(), (unsigned long)glXGetCurrentDrawable()); + + // Check OpenGL version (can help identify context switches) + const char* version = (const char*)glGetString(GL_VERSION); + fprintf(stderr, "Debug Log: Current OpenGL version: %s\n", version ? version : "NULL"); + + // Check for any pending GL errors + GLenum err; + while((err = glGetError()) != GL_NO_ERROR) { + fprintf(stderr, "Debug Log: GL error AFTER previous operation: 0x%x\n", err); + } +} + +// Add to your texture allocation system +internal void +r_debug_texture_info(GLuint texture_id) +{ + if (!glIsTexture(texture_id)) + { + fprintf(stderr, "Texture %u is not a valid texture object\n", texture_id); + return; + } + + GLint current_texture_binding = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_texture_binding); + + GLint width = 0, height = 0, internal_format = 0; + glBindTexture(GL_TEXTURE_2D, texture_id); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internal_format); + + fprintf(stderr, "Texture %u: %dx%d, internal format: 0x%x\n", + texture_id, width, height, internal_format); + + // Restore previous binding + glBindTexture(GL_TEXTURE_2D, current_texture_binding); +} + diff --git a/src/render/opengl/render_opengl.h b/src/render/opengl/render_opengl.h new file mode 100644 index 000000000..b50856743 --- /dev/null +++ b/src/render/opengl/render_opengl.h @@ -0,0 +1,290 @@ +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +#ifndef RENDER_OPENGL_H +#define RENDER_OPENGL_H + +//////////////////////////////// +//~ dan: Generated Code + +#include "generated/render_opengl.meta.h" + + +// Need R_GL_UniformTypeKind enum if not already included via render_core.h +// Assuming render_core.h includes generated/render.meta.h which defines R_PassKind etc. +// We will need a similar generation step for OpenGL specific types like UniformTypeKind if separate +// For now, let's manually define the enum count based on d3d11's mdesk. + +// Uniform Binding Points +#define R_GL_UNIFORM_BINDING_POINT_RECT 0 +#define R_GL_UNIFORM_BINDING_POINT_BLUR 1 +#define R_GL_UNIFORM_BINDING_POINT_MESH 2 +// ... add more if needed + +////////////////////////////////// +//~ dan: C-side Shader Uniform Structures (Matching GLSL std140 layout) + +// NOTE(Memory Layout): GLSL std140 layout rules: +// - Scalars (float, int, bool): aligned to 4 bytes. +// - vec2: aligned to 8 bytes. +// - vec3, vec4: aligned to 16 bytes. +// - mat3: column 0 aligned to 16, col 1 aligned to 16, col 2 aligned to 16 (treated as 3 vec4s essentially for alignment). +// - mat4: column 0 aligned to 16, col 1 aligned to 16, col 2 aligned to 16, col 3 aligned to 16. +// - Arrays: Each element aligned according to its type (as above), potentially padded up to vec4 alignment. +// - Structs: Aligned to the largest alignment of its members, potentially padded up to a multiple of 16 bytes. + +// GLSL vec2 aligns to 8 bytes +// GLSL vec3/vec4 align to 16 bytes +// GLSL mat3 aligns as 3 vec4 columns (total 48 bytes) +// GLSL mat4 aligns as 4 vec4 columns (total 64 bytes) +// GLSL struct aligned to largest member alignment (often 16 bytes) + +// Matches GLSL uniform block "R_GL_Uniforms_Rect" +typedef struct R_GL_Uniforms_Rect R_GL_Uniforms_Rect; +struct R_GL_Uniforms_Rect +{ + // Aligned(8) + Vec2F32 viewport_size_px; // offset 0, size 8 + // Aligned(4), needs padding after + F32 opacity; // offset 8, size 4 + // --- Explicit std140 Padding --- + F32 _padding0; // offset 12, size 4 (Ensure alignment for mat4) + // Aligned(16) per column -> Mat4x4F32 is 4*16 = 64 bytes + Vec4F32 texture_sample_channel_map[4]; // offset 16, size 64 + // Aligned(8) + Vec2F32 texture_t2d_size_px; // offset 80, size 8 + // --- Explicit std140 Padding --- + Vec2F32 _padding1; // offset 88, size 8 (Ensure alignment for mat3) + // Aligned(16) per column -> Mat3x3F32 needs 3*16 = 48 bytes + Vec4F32 xform[3]; // offset 96, size 48 + // Aligned(8) + Vec2F32 xform_scale; // offset 144, size 8 + // --- Explicit std140 Padding --- + Vec2F32 _padding2; // offset 152, size 8 (Ensure total size is multiple of 16, MUST be Vec2F32) +}; // Total size: 160 bytes +StaticAssert(sizeof(R_GL_Uniforms_Rect) == 160, R_GL_Uniforms_Rect_SizeCheck); + +// Matches GLSL uniform block "R_GL_Uniforms_Blur" +typedef struct R_GL_Uniforms_Blur R_GL_Uniforms_Blur; +struct R_GL_Uniforms_Blur +{ + // Aligned(16) + Vec4F32 rect; // offset 0 + // Aligned(16) + Vec4F32 corner_radii_px; // offset 16 + // Aligned(8) + Vec2F32 direction; // offset 32 + // Aligned(8) + Vec2F32 viewport_size; // offset 40 + // Aligned(4) + U32 blur_count; // offset 48 (Using U32 as GLSL bool might be int) + // --- Explicit std140 Padding --- + U32 _padding0[3]; // offset 52, size 12 (Ensure kernel starts at offset 64) + // Aligned(16) per element + Vec4F32 kernel[32]; // offset 64 (Array stride is 16) +}; // Total size: 64 + 32 * 16 = 576 bytes +StaticAssert(sizeof(R_GL_Uniforms_Blur) == 576, R_GL_Uniforms_Blur_SizeCheck); + +// Matches GLSL uniform block "MeshUniforms" +// Name MUST match the GLSL block name for UBO binding to work. +typedef struct R_GL_Uniforms_Mesh R_GL_Uniforms_Mesh; +struct R_GL_Uniforms_Mesh +{ + // IMPORTANT: Remember layout(std140) rules for alignment/padding in GLSL. + // Mat4x4F32 is typically 4x vec4. + Mat4x4F32 view_proj_matrix; // Combined view * projection matrix, transposed for GLSL. + // Instance transform is now passed via vertex attributes. +}; + +//////////////////////////////// +//~ dan: Main State Types + +typedef struct R_GL_Tex2D R_GL_Tex2D; +struct R_GL_Tex2D +{ + R_GL_Tex2D *next; + U64 generation; + GLuint texture_id; + R_ResourceKind kind; + Vec2S32 size; + R_Tex2DFormat format; +}; + +typedef struct R_GL_Buffer R_GL_Buffer; +struct R_GL_Buffer +{ + R_GL_Buffer *next; + U64 generation; + GLuint buffer_id; + R_ResourceKind kind; + U64 size; + GLenum target; +}; + +// Added helper structs for delayed deletion +typedef struct R_GL_Framebuffer R_GL_Framebuffer; +struct R_GL_Framebuffer +{ + R_GL_Framebuffer *next; + GLuint fbo_id; +}; + +typedef struct R_GL_Renderbuffer R_GL_Renderbuffer; +struct R_GL_Renderbuffer +{ + R_GL_Renderbuffer *next; + GLuint rbo_id; +}; + +typedef struct R_GL_Window R_GL_Window; +struct R_GL_Window +{ + R_GL_Window *next; + U64 generation; + + // GL context related (might be managed by OS layer) + // void *gl_context; // Example + + // Framebuffers (FBOs) & Attachments - Need cleanup on unequip/resize + GLuint stage_fbo; + GLuint stage_color_texture; // Texture used as color attachment + GLuint stage_depth_texture; // Using texture for depth + + GLuint stage_scratch_fbo; + GLuint stage_scratch_color_texture; + + GLuint geo3d_fbo; + GLuint geo3d_color_texture; + GLuint geo3d_depth_texture; + + // Last known state for resize detection + Vec2S32 last_resolution; +}; + +typedef struct R_GL_ShaderProgram R_GL_ShaderProgram; +struct R_GL_ShaderProgram +{ + GLuint program_id; + GLuint vertex_shader; + GLuint fragment_shader; + GLint main_texture_uniform_location; // Added for sampler uniforms + // TODO(graphics): Add Geometry, Compute shader handles if needed +}; + +// Added: Structure to hold information for deferred texture allocations +typedef struct R_GL_DeferredTex2D R_GL_DeferredTex2D; +struct R_GL_DeferredTex2D +{ + R_GL_DeferredTex2D *next; + R_ResourceKind kind; + Vec2S32 size; + R_Tex2DFormat format; + void *data; // Will need to copy this data + U64 data_size; // Track data size for allocation + R_Handle result; // Handle to return to caller early + R_GL_Tex2D *texture; // Pre-allocated texture object +}; + +typedef struct R_GL_State R_GL_State; +struct R_GL_State +{ + Arena *arena; + OS_Handle os_window_handle; // Added: Store the OS handle for context management + Display *display; // Added for Linux: store the display pointer + OS_Handle device_rw_mutex; + + // dan: resources + R_GL_Window *first_free_window; + R_GL_Tex2D *first_free_tex2d; + R_GL_Buffer *first_free_buffer; + R_GL_Framebuffer *first_free_framebuffer; // Added free list + R_GL_Renderbuffer *first_free_renderbuffer; // Added free list + + // Queued Deletions (Processed in r_begin_frame) + R_GL_Tex2D *first_to_free_tex2d; + R_GL_Buffer *first_to_free_buffer; + R_GL_Framebuffer *first_to_free_framebuffer; // Added deletion queue + R_GL_Renderbuffer *first_to_free_renderbuffer; // Added deletion queue + // Note: Window FBO attachment textures are deleted directly in resize/unequip queue logic + + Arena *buffer_flush_arena; + R_Handle backup_texture; + + // Added: Queue for deferred texture allocations + R_GL_DeferredTex2D *first_deferred_tex2d; + R_GL_DeferredTex2D *last_deferred_tex2d; + Arena *deferred_tex2d_arena; + B32 has_valid_context; // Flag to track if we have a valid OpenGL context + + // dan: pipeline state object cache + GLuint rect_vao; + GLuint mesh_vao; + GLuint fullscreen_vao; // Added: VAO for fullscreen passes + R_GL_ShaderProgram rect_shader; + R_GL_ShaderProgram blur_shader; + R_GL_ShaderProgram mesh_shader; + R_GL_ShaderProgram geo3d_composite_shader; + R_GL_ShaderProgram finalize_shader; + GLuint samplers[R_Tex2DSampleKind_COUNT]; + GLuint uniform_type_kind_buffers[R_GL_UniformTypeKind_COUNT]; + U64 uniform_type_kind_sizes[R_GL_UniformTypeKind_COUNT]; // Added: store UBO sizes + + GLuint instance_vbo; // Shared VBO for instance data + + B32 glew_initialized; // Added: Track GLEW initialization separately + B32 global_resources_initialized; // Renamed from opengl_extensions_initialized for clarity + B32 create_context_arb_available; // Added: Flag for GLX context creation extension +}; + +//////////////////////////////// +//~ dan: Globals + +global R_GL_State *r_gl_state = 0; +// Define NIL handles robustly to avoid accidental use of address +global R_GL_Window r_gl_window_nil = { .next = &r_gl_window_nil }; +global R_GL_Tex2D r_gl_tex2d_nil = { .next = &r_gl_tex2d_nil }; +global R_GL_Buffer r_gl_buffer_nil = { .next = &r_gl_buffer_nil }; + +//////////////////////////////// +//~ dan: Helpers + +// Maps R_GeoTopologyKind to GLenum primitive type +internal GLenum r_gl_primitive_from_topology(R_GeoTopologyKind kind); + +internal R_GL_Window *r_gl_window_from_handle(R_Handle handle); +internal R_Handle r_gl_handle_from_window(R_GL_Window *window); +internal R_GL_Tex2D *r_gl_tex2d_from_handle(R_Handle handle); +internal R_Handle r_gl_handle_from_tex2d(R_GL_Tex2D *texture); +internal R_GL_Buffer *r_gl_buffer_from_handle(R_Handle handle); +internal R_Handle r_gl_handle_from_buffer(R_GL_Buffer *buffer); +internal GLenum r_gl_usage_from_resource_kind(R_ResourceKind kind); +internal GLenum r_gl_format_from_tex2d_format(R_Tex2DFormat format); +internal GLint r_gl_internal_format_from_tex2d_format(R_Tex2DFormat format); +internal GLenum r_gl_type_from_tex2d_format(R_Tex2DFormat format); // Added +internal GLint r_gl_filter_from_sample_kind(R_Tex2DSampleKind kind); +internal void r_gl_check_error_line_file(const char *op, int line, char *file); // Added prototype +internal void r_gl_make_context_current(R_GL_Window *window); // Add this to fix declaration error +internal void r_gl_delete_window_fbo_resources(R_GL_Window *gl_window); // Add this too +internal void r_gl_debug_draw_test_pattern(void); // Add debug test pattern function +#define r_gl_check_error(op) r_gl_check_error_line_file(op, __LINE__, __FILE__) // Added define + +// Buffer Update Helpers +internal void r_gl_buffer_update_sub_data(R_Handle handle, U64 offset, U64 size, void *data); +internal void* r_gl_buffer_map_range(R_Handle handle, U64 offset, U64 size, GLbitfield access_flags); +internal B32 r_gl_buffer_unmap(R_Handle handle); + +// One-Time Initialization Helpers (Split) +internal void r_gl_init_glew_if_needed(void); +internal void r_gl_create_global_resources(void); +// Leave this function for backward compatibility +internal void r_gl_init_extensions_if_needed(void); + +// Add new function declarations +internal void r_gl_process_deferred_tex2d_queue(void); +internal void r_gl_set_has_valid_context(B32 has_context); + +// Debugging Helpers +internal void debug_log_current_context(void); // Add this +internal void r_debug_texture_info(GLuint texture_id); // Add declaration + +#endif // RENDER_OPENGL_H \ No newline at end of file diff --git a/src/render/opengl/render_opengl.mdesk b/src/render/opengl/render_opengl.mdesk new file mode 100644 index 000000000..3b3ffe106 --- /dev/null +++ b/src/render/opengl/render_opengl.mdesk @@ -0,0 +1,526 @@ +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +//////////////////////////////// +//~ dan: Pipeline Tables + +// NOTE: Input layouts are handled by VAO setup in GL, not directly tied to shaders like D3D11 ILAY. +// We'll keep the table structure similar but omit ILAY tables for now. +@table(name, vs_source, fs_source) +R_GL_ShaderPairTable: +{ + {Rect r_gl_g_rect_shader_vs_src r_gl_g_rect_shader_fs_src } + {Blur r_gl_g_blur_shader_vs_src r_gl_g_blur_shader_fs_src } + {Mesh r_gl_g_mesh_shader_vs_src r_gl_g_mesh_shader_fs_src } + // NOTE: Geo3DComposite and Finalize use a generic VS in the C code, embed FS only? Or embed the simple VS too? + // For now, let's assume a generic VS might be defined elsewhere or inline. + {Geo3DComposite r_gl_g_finalize_shader_vs_src r_gl_g_geo3dcomposite_shader_fs_src } // Using finalize VS for composite + {Finalize r_gl_g_finalize_shader_vs_src r_gl_g_finalize_shader_fs_src } +} + +// Separate tables for VS/FS might still be useful if some shaders are reused. +@table(name, source) +R_GL_VShadTable: +{ + {Rect r_gl_g_rect_shader_vs_src} + {Blur r_gl_g_blur_shader_vs_src} + {Mesh r_gl_g_mesh_shader_vs_src} + {FullscreenQuad r_gl_g_finalize_shader_vs_src} // Generic fullscreen VS used by Composite/Finalize +} + +@table(name, source) +R_GL_PShadTable: +{ + {Rect r_gl_g_rect_shader_fs_src} + {Blur r_gl_g_blur_shader_fs_src} + {Mesh r_gl_g_mesh_shader_fs_src} + {Geo3DComposite r_gl_g_geo3dcomposite_shader_fs_src} + {Finalize r_gl_g_finalize_shader_fs_src} +} + +@table(name) +R_GL_UniformTypeTable: +{ + {Rect} + {Blur} + {Mesh} +} + +//////////////////////////////// +//~ dan: UI Rectangle Shaders + +@embed_string r_gl_g_rect_shader_vs_src: +""" +#version 330 core +// Instance inputs matching VAO setup (8 vec4s) +layout(location = 0) in vec4 dst_rect_px_in; // {x0, y0, x1, y1} +layout(location = 1) in vec4 src_rect_px_in; // {x0, y0, x1, y1} +layout(location = 2) in vec4 color00_in; // {r, g, b, a} BL +layout(location = 3) in vec4 color01_in; // {r, g, b, a} TL +layout(location = 4) in vec4 color10_in; // {r, g, b, a} BR +layout(location = 5) in vec4 color11_in; // {r, g, b, a} TR +layout(location = 6) in vec4 corner_radii_px_in; // {bl, tl, br, tr} - CHECK ORDER vs D3D11 +layout(location = 7) in vec4 style_params_in; // {border, soft, omit_tex, unused} + +// Uniforms (Unchanged) +layout (std140) uniform R_GL_Uniforms_Rect { // Binding Point 0 + vec2 viewport_size_px; + float opacity; + mat4 texture_sample_channel_map; + vec2 texture_t2d_size_px; + mat3 xform; // Column-major + vec2 xform_scale; +}; + +// Outputs to Fragment Shader (Matches D3D11 rect shader outputs) +out VS_OUT { + vec2 rect_half_size_px; + vec2 texcoord_pct; + vec2 sdf_sample_pos; + vec4 tint; + float corner_radius_px; + flat float border_thickness_px; + flat float softness_px; + flat float omit_texture; +} vs_out; + +void main() { + // Unpack instance data + vec2 dst_p0_px = dst_rect_px_in.xy; + vec2 dst_p1_px = dst_rect_px_in.zw; + vec2 src_p0_px = src_rect_px_in.xy; + vec2 src_p1_px = src_rect_px_in.zw; + vec2 dst_size_px = abs(dst_p1_px - dst_p0_px); + + float border_thickness_px = style_params_in.x; + float softness_px = style_params_in.y; + float omit_texture = style_params_in.z; + + // Generate standard CCW triangle strip quad vertex percentages (0,0), (1,0), (0,1), (1,1) + // gl_VertexID 0 -> (0,0) -> BL + // gl_VertexID 1 -> (1,0) -> BR + // gl_VertexID 2 -> (0,1) -> TL + // gl_VertexID 3 -> (1,1) -> TR + vec2 vertex_pct = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1)); + + // Map vertex percentage to destination rect coordinates + vec2 current_dst_pos_px = mix(dst_p0_px, dst_p1_px, vertex_pct); + + // Calculate output position + vec3 xformed_pos = xform * vec3(current_dst_pos_px, 1.0); + vec2 ndc_pos = (xformed_pos.xy / viewport_size_px) * 2.0 - 1.0; + ndc_pos.y = -ndc_pos.y; // Flip Y for GL NDC + gl_Position = vec4(ndc_pos, 0.0, 1.0); + + // Map vertex percentage to source rect coordinates for texture coords + vec2 current_src_pos_px = mix(src_p0_px, src_p1_px, vertex_pct); + vs_out.texcoord_pct = current_src_pos_px / texture_t2d_size_px; + + // Select color based on vertex ID (matching D3D11 logic for vertex IDs 0..3 -> TL, BL, TR, BR) + vec4 color_bl = color00_in; // Location 2 + vec4 color_br = color10_in; // Location 4 + vec4 color_tl = color01_in; // Location 3 + vec4 color_tr = color11_in; // Location 5 + vec4 colors[4] = vec4[4](color_bl, color_br, color_tl, color_tr); // GLSL array constructor syntax + vs_out.tint = colors[gl_VertexID]; + + // Select corner radius based on vertex ID (matching D3D11 logic) + // GL Vertex IDs 0, 1, 2, 3 correspond to BL, BR, TL, TR based on vertex_pct calc + // Input corner_radii_px_in (loc 6) is {bl, tl, br, tr} -> {x, y, z, w} + // D3D11 mapping (see thought process): ID 0 (BL)->x, ID 1 (BR)->z, ID 2 (TL)->y, ID 3 (TR)->w + float radii[4] = float[4](corner_radii_px_in.x, corner_radii_px_in.z, corner_radii_px_in.y, corner_radii_px_in.w); // GLSL array constructor syntax + vs_out.corner_radius_px = radii[gl_VertexID]; + + // Calculate SDF sample position based on vertex percentage relative to center + vs_out.rect_half_size_px = dst_size_px / 2.0 * xform_scale; + vs_out.sdf_sample_pos = (2.0 * vertex_pct - 1.0) * vs_out.rect_half_size_px; + + // Pass through flat values + vs_out.border_thickness_px = border_thickness_px; + vs_out.softness_px = softness_px; + vs_out.omit_texture = omit_texture; +} +""" +@embed_string r_gl_g_rect_shader_fs_src: +""" +#version 330 core +in VS_OUT { + vec2 rect_half_size_px; + vec2 texcoord_pct; + vec2 sdf_sample_pos; + vec4 tint; + float corner_radius_px; + flat float border_thickness_px; + flat float softness_px; + flat float omit_texture; +} fs_in; + +layout (std140) uniform R_GL_Uniforms_Rect // Binding Point 0 +{ + vec2 viewport_size_px; + float opacity; + // float _padding0_; + mat4 texture_sample_channel_map; + vec2 texture_t2d_size_px; + // vec2 translate; + mat3 xform; + vec2 xform_scale; + // vec2 _padding1_; +}; + +uniform sampler2D main_t2d; // Texture Unit 0 + +out vec4 FragColor; + +// Corrected SDF function +float rect_sdf(vec2 sample_pos, vec2 rect_half_size, float r) +{ + vec2 d = abs(sample_pos) - rect_half_size + r; + return length(max(d, 0.0)) - r; +} + +void main() +{ + // Tint is already interpolated + vec4 tint = fs_in.tint; + + // Sample texture + vec4 albedo_sample = vec4(1.0); + if(fs_in.omit_texture < 0.5) // Use < 0.5 for bool comparison + { + // Assuming texture V is flipped if needed in VS + albedo_sample = texture(main_t2d, fs_in.texcoord_pct) * texture_sample_channel_map; + } + + // Determine SDF sample position (already interpolated) + vec2 sdf_sample_pos = fs_in.sdf_sample_pos; // Keep for potential future use + + // Sample for borders + float border_sdf_t = 1.0; // Default to 1 (no border effect) + float softness_px = max(fs_in.softness_px, 0.01); // Ensure softness is positive + float border_thickness_px = fs_in.border_thickness_px; + if(border_thickness_px > 0.0) + { + // Adjust half size by softness before calculating SDF, matching D3D11 + vec2 inner_half_size = fs_in.rect_half_size_px - vec2(softness_px * 2.0) - border_thickness_px; + float inner_radius = max(fs_in.corner_radius_px - border_thickness_px, 0.0); + float border_sdf_s = rect_sdf(sdf_sample_pos, inner_half_size, inner_radius); + + float border_smooth_range = max(2.0 * softness_px, 1.0); + // Calculate coverage *outside* the inner edge (0 inside fill, ramps to 1 outside) + border_sdf_t = smoothstep(0.0, border_smooth_range, border_sdf_s); + } + + // Sample for corners (outer edge) + float corner_sdf_t = 1.0; // Default to 1 (no corner effect) + // Check radius OR softness > 0.75 (D3D11 logic) + if(fs_in.corner_radius_px > 0.0 || softness_px > 0.75) + { + // Adjust half size by softness before calculating SDF, matching D3D11 + vec2 outer_half_size = fs_in.rect_half_size_px - vec2(softness_px * 2.0); + float outer_radius = max(fs_in.corner_radius_px, 0.0); + float corner_sdf_s = rect_sdf(sdf_sample_pos, outer_half_size, outer_radius); + + float corner_smooth_range = max(2.0 * softness_px, 1.0); + // Calculate coverage *inside* the outer edge (1 inside, ramps to 0 outside) + corner_sdf_t = 1.0 - smoothstep(0.0, corner_smooth_range, corner_sdf_s); + } + + // Form final color + FragColor = albedo_sample * tint; + FragColor.a *= opacity; + // Combine coverages - Base alpha on the outer shape coverage + FragColor.a *= corner_sdf_t; // Multiply by corner coverage + FragColor.a *= border_sdf_t; // Multiply by 1.0 (no effect) + // Final discard if alpha is near zero + if (FragColor.a < 0.001) + { + discard; + } +} +""" + +//////////////////////////////// +//~ dan: Blur Shaders + +@embed_string r_gl_g_blur_shader_vs_src: +""" +#version 330 core +// Define uniforms matching the FS for rect/corners +layout (std140) uniform BlurUniforms // Binding Point 1 +{ + vec4 rect; // vec4(min.x, min.y, max.x, max.y) + vec4 corner_radii_px; // vec4(bl, tl, br, tr) + vec2 direction; // vec2(dx, dy) unit vector for blur pass + vec2 viewport_size; // vec2(width, height) of render target + uint blur_count; // Number of samples (adjusted for bilinear) + // padding... + vec4 kernel[32]; // vec4(weight, offset, unused, unused) +}; + +// Define output block to pass SDF data to FS +out VS_OUT { + vec2 sdf_sample_pos; + flat vec2 rect_half_size; + float corner_radius; +} vs_out; + +out vec2 TexCoord; + +void main() +{ + // Fullscreen quad vertices (-1,-1), (1,-1), (-1, 1), (1, 1) using triangle strip + // VertexID: 0 -> (-1,-1), 1 -> (1,-1), 2 -> (-1, 1), 3 -> (1, 1) + vec2 pos = vec2( (gl_VertexID << 1) - 1.0, (gl_VertexID & 1) * 2.0 - 1.0 ); + gl_Position = vec4(pos, 0.0, 1.0); + + // Texcoord (0,0) bottom-left to (1,1) top-right + TexCoord = pos * 0.5 + 0.5; + // TexCoord.y = 1.0 - TexCoord.y; // Y is already correct for fullscreen quad + + // Calculate SDF inputs + vs_out.rect_half_size = (rect.zw - rect.xy) * 0.5; + + // Determine cornercoords based on vertex ID (0: BL, 1: BR, 2: TL, 3: TR) + vec2 cornercoords_pct = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1)); // Renamed from cornercoords__pct + vs_out.sdf_sample_pos = (2.0 * cornercoords_pct - 1.0) * vs_out.rect_half_size; + + // Select corner radius based on vertex ID + // ID 0 (BL) -> radii.x + // ID 1 (BR) -> radii.z + // ID 2 (TL) -> radii.y + // ID 3 (TR) -> radii.w + if(gl_VertexID == 0) vs_out.corner_radius = corner_radii_px.x; + else if(gl_VertexID == 1) vs_out.corner_radius = corner_radii_px.z; + else if(gl_VertexID == 2) vs_out.corner_radius = corner_radii_px.y; + else /* ID 3 */ vs_out.corner_radius = corner_radii_px.w; + + // D3D11 vertex shader passes rect_half_size - 2.0. Match this. + vs_out.rect_half_size -= 2.0; +} +""" + +@embed_string r_gl_g_blur_shader_fs_src: +""" +#version 330 core +in vec2 TexCoord; +// Added input block matching the VS structure (even if VS isn't updated yet) +in VS_OUT { + vec2 sdf_sample_pos; + flat vec2 rect_half_size; + float corner_radius; +} fs_in; +out vec4 FragColor; + +uniform sampler2D main_t2d; // Texture Unit 0 + +layout (std140) uniform BlurUniforms // Binding Point 1 +{ + vec4 rect; // vec4(min.x, min.y, max.x, max.y) + vec4 corner_radii_px; // vec4(bl, tl, br, tr) + vec2 direction; // vec2(dx, dy) unit vector for blur pass + vec2 viewport_size; // vec2(width, height) of render target + uint blur_count; // Number of samples (adjusted for bilinear) + // padding... + vec4 kernel[32]; // vec4(weight, offset, unused, unused) +}; + +// SDF for rounded rectangle +float rect_sdf(vec2 sample_pos, vec2 rect_half_size, float r) +{ + vec2 d = abs(sample_pos) - rect_half_size + r; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r; +} + +void main() +{ + vec4 sum = vec4(0.0); + vec2 uv = TexCoord; + vec2 texelSize = 1.0 / viewport_size; + + // Apply first weight (at offset 0) + sum += texture(main_t2d, uv) * kernel[0].x; + + // Apply remaining weights using bilinear optimization + for(uint i = 1u; i < blur_count; ++i) + { + vec2 offset = direction * kernel[i].y * texelSize; + sum += texture(main_t2d, uv + offset) * kernel[i].x; + sum += texture(main_t2d, uv - offset) * kernel[i].x; + } + + // Clip to rounded rectangle using SDF + float corner_sdf_s = rect_sdf(fs_in.sdf_sample_pos, fs_in.rect_half_size, fs_in.corner_radius); + // D3D11 uses smoothstep(0, 2*softness, ...), let's use a small fixed softness for now + float softness = 1.0; + float corner_sdf_t = 1.0 - smoothstep(-softness, softness, corner_sdf_s); + + // Discard fragments outside the rounded rectangle (similar to D3D11) + if (corner_sdf_t < 0.001) // Use a small threshold instead of 0.9 check + { + discard; + } + + FragColor = vec4(sum.rgb, 1.0); // Output color, force alpha to 1 + + // Optional: Apply coverage to alpha if needed later + // FragColor.a = corner_sdf_t; +} +""" + +//////////////////////////////// +//~ dan: Mesh Shaders + +@embed_string r_gl_g_mesh_shader_vs_src: +""" +#version 330 core +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; +layout (location = 2) in vec2 aTexCoord; +layout (location = 3) in vec3 aColor; +layout (location = 4) in mat4 instance_xform; // Per-instance transform + +layout (std140) uniform R_GL_Uniforms_Mesh { // Use correct uniform block name matching C struct + mat4 view_proj_matrix; // Combined view * projection +}; + +out vec3 FragPos; +out vec3 Normal; +out vec2 TexCoords; +out vec3 VertexColor; + +void main() +{ + vec4 worldPos = instance_xform * vec4(aPos, 1.0); + gl_Position = view_proj_matrix * worldPos; + FragPos = vec3(worldPos); + Normal = mat3(transpose(inverse(instance_xform))) * aNormal; // Correct normal transformation + TexCoords = aTexCoord; + VertexColor = aColor; +} +""" + +@embed_string r_gl_g_mesh_shader_fs_src: +""" +#version 330 core +out vec4 FragColor; + +in vec3 FragPos; +in vec3 Normal; +in vec2 TexCoords; +in vec3 VertexColor; + +uniform sampler2D main_t2d; // Texture Unit 0 + +void main() +{ + // Simple: Output vertex color modulated by texture (if available) + // vec4 texColor = texture(main_t2d, TexCoords); + FragColor = vec4(VertexColor, 1.0); // * texColor; // Modulate later if needed + // Add lighting calculations here if needed +} +""" + +//////////////////////////////// +//~ dan: Geo3D Composition Shaders + +// NOTE: Geo3D Composite uses the same fullscreen VS as Finalize in the C code. +// We will embed the FS here and reference the Finalize VS in the tables. +@embed_string r_gl_g_geo3dcomposite_shader_fs_src: +""" +#version 330 core +in vec2 TexCoord; +uniform sampler2D main_t2d; // Renamed from stage_t2d +out vec4 FragColor; +void main() +{ + FragColor = texture(main_t2d, TexCoord); // Use renamed uniform +} +""" + +//////////////////////////////// +//~ dan: Finalize Shaders + +@embed_string r_gl_g_finalize_shader_vs_src: +""" +#version 330 core +out vec2 TexCoord; +void main() +{ + // Correct way for (-1,-1), (1,-1), (-1, 1), (1, 1) triangle strip + vec2 pos = vec2( (gl_VertexID & 1) * 2.0 - 1.0, (gl_VertexID & 2) - 1.0 ); + gl_Position = vec4(pos, 0.0, 1.0); + // Texcoord (0,0) bottom-left to (1,1) top-right + TexCoord = pos * 0.5 + 0.5; + // TexCoord.y = 1.0 - TexCoord.y; // Flip V if input texture requires it +} +""" + +@embed_string r_gl_g_finalize_shader_fs_src: +""" +#version 330 core +in vec2 TexCoord; +uniform sampler2D main_t2d; // Renamed from stage_t2d +out vec4 FragColor; +void main() +{ + FragColor = texture(main_t2d, TexCoord); // Use renamed uniform + FragColor.a = 1.0; // Force alpha to 1 for final output +} +""" + +//////////////////////////////// +//~ dan: Table Generators + +// NOTE: Input layouts (VAOs) are configured separately in OpenGL C code. +// The D3D11 `ilay_table` concept doesn't directly map here. +// We'll generate tables for shader sources only. + +@enum R_GL_VShadKind: +{ + @expand(R_GL_VShadTable a) `$(a.name)`, + COUNT, +} + +@enum R_GL_PShadKind: +{ + @expand(R_GL_PShadTable a) `$(a.name)`, + COUNT, +} + +@enum R_GL_UniformTypeKind: +{ + @expand(R_GL_UniformTypeTable a) `$(a.name)`, + COUNT, +} + +@c_file @data(`String8 *`) // Pointers to String8 global variables +r_gl_g_vshad_kind_source_ptr_table: +{ + @expand(R_GL_VShadTable a) `&$(a.source)`; +} + +@c_file @data(String8) +r_gl_g_vshad_kind_source_name_table: +{ + @expand(R_GL_VShadTable a) `str8_lit_comp("$(a.source)")`; +} + +@c_file @data(`String8 *`) // Pointers to String8 global variables +r_gl_g_pshad_kind_source_ptr_table: +{ + @expand(R_GL_PShadTable a) `&$(a.source)`; +} + +@c_file @data(String8) +r_gl_g_pshad_kind_source_name_table: +{ + @expand(R_GL_PShadTable a) `str8_lit_comp("$(a.source)")`; +} + +// Generate size table based on C struct definitions (defined in render_opengl.h) +@c_file @data(U64) +r_gl_g_uniform_type_kind_size_table: +{ + @expand(R_GL_UniformTypeTable a) `sizeof(R_GL_Uniforms_$(a.name))`; +} \ No newline at end of file diff --git a/src/render/render_inc.c b/src/render/render_inc.c index 84612fa8b..d25efbe2a 100644 --- a/src/render/render_inc.c +++ b/src/render/render_inc.c @@ -7,6 +7,8 @@ # include "stub/render_stub.c" #elif R_BACKEND == R_BACKEND_D3D11 # include "d3d11/render_d3d11.c" +#elif R_BACKEND == R_BACKEND_OPENGL +# include "opengl/render_opengl.c" #else # error Renderer backend not specified. #endif diff --git a/src/render/render_inc.h b/src/render/render_inc.h index 64802e5c5..263db9ad5 100644 --- a/src/render/render_inc.h +++ b/src/render/render_inc.h @@ -9,12 +9,14 @@ #define R_BACKEND_STUB 0 #define R_BACKEND_D3D11 1 - +#define R_BACKEND_OPENGL 2 //////////////////////////////// //~ rjf: Decide On Backend #if !defined(R_BACKEND) && OS_WINDOWS -# define R_BACKEND R_BACKEND_D3D11 +#define R_BACKEND R_BACKEND_D3D11 +#elif !defined(R_BACKEND) && OS_LINUX +#define R_BACKEND R_BACKEND_OPENGL #endif //////////////////////////////// @@ -29,6 +31,8 @@ # include "stub/render_stub.h" #elif R_BACKEND == R_BACKEND_D3D11 # include "d3d11/render_d3d11.h" +#elif R_BACKEND == R_BACKEND_OPENGL +# include "opengl/render_opengl.h" #else # error Renderer backend not specified. #endif From 7f9521f17ed8b32c48a8a8c57ea5a713c169ab19 Mon Sep 17 00:00:00 2001 From: Danilo Dumeljic Date: Mon, 7 Apr 2025 16:54:29 +0200 Subject: [PATCH 2/4] build fix with asan --- build.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index d40ea3551..60ab70a0c 100755 --- a/build.sh +++ b/build.sh @@ -10,6 +10,7 @@ if [ -v debug ]; then echo "[debug mode]"; fi if [ -v release ]; then echo "[release mode]"; fi if [ -v clang ]; then compiler="${CC:-clang}"; echo "[clang compile]"; fi if [ -v gcc ]; then compiler="${CC:-gcc}"; echo "[gcc compile]"; fi +if [ -v asan ]; then echo "[AddressSanitizer enabled]"; fi # --- Unpack Command Line Build Arguments ------------------------------------- @@ -20,17 +21,27 @@ git_hash=$(git rev-parse HEAD) git_hash_full=$(git rev-parse HEAD) # --- Compile/Link Line Definitions ------------------------------------------- -clang_common="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\\"$git_hash\\" -DBUILD_GIT_HASH_FULL=\\"$git_hash_full\\" -Wno-unknown-warning-option -fdiagnostics-absolute-paths -Wall -Wno-missing-braces -Wno-unused-function -Wno-writable-strings -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-register -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-single-bit-bitfield-constant-conversion -Wno-compare-distinct-pointer-types -Wno-initializer-overrides -Wno-incompatible-pointer-types-discards-qualifiers -Wno-for-loop-analysis -Xclang -flto-visibility-public-std -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf -ferror-limit=1000000" +# AddressSanitizer flags +asan_flags="" +if [ -v asan ]; then + asan_flags="-fsanitize=address -fno-omit-frame-pointer" +fi + +clang_common="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\"$git_hash\" -DBUILD_GIT_HASH_FULL=\"$git_hash_full\" -Wno-unknown-warning-option -fdiagnostics-absolute-paths -Wall -Wno-missing-braces -Wno-unused-function -Wno-writable-strings -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-register -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-single-bit-bitfield-constant-conversion -Wno-compare-distinct-pointer-types -Wno-initializer-overrides -Wno-incompatible-pointer-types-discards-qualifiers -Wno-for-loop-analysis -Xclang -flto-visibility-public-std -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf -ferror-limit=1000000 $asan_flags" clang_debug="$compiler -g -O0 -DBUILD_DEBUG=1 ${clang_common} ${auto_compile_flags}" clang_release="$compiler -g -O2 -DBUILD_DEBUG=0 ${clang_common} ${auto_compile_flags}" clang_link="-lpthread -lm -lrt -ldl -lfreetype" clang_out="-o" -gcc_common="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\\"$git_hash\\" -DBUILD_GIT_HASH_FULL=\\"$git_hash_full\\" -Wno-unknown-warning-option -Wall -Wno-missing-braces -Wno-unused-function -Wno-attributes -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-compare-distinct-pointer-types -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf" +gcc_common="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\"$git_hash\" -DBUILD_GIT_HASH_FULL=\"$git_hash_full\" -Wno-unknown-warning-option -Wall -Wno-missing-braces -Wno-unused-function -Wno-attributes -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-compare-distinct-pointer-types -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf $asan_flags" gcc_debug="$compiler -g -O0 -DBUILD_DEBUG=1 ${gcc_common} ${auto_compile_flags}" gcc_release="$compiler -g -O2 -DBUILD_DEBUG=0 ${gcc_common} ${auto_compile_flags}" gcc_link="-lpthread -lm -lrt -ldl -lfreetype" gcc_out="-o" +# Version without ASan for metagen +clang_common_no_asan="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\"$git_hash\" -DBUILD_GIT_HASH_FULL=\"$git_hash_full\" -Wno-unknown-warning-option -fdiagnostics-absolute-paths -Wall -Wno-missing-braces -Wno-unused-function -Wno-writable-strings -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-register -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-single-bit-bitfield-constant-conversion -Wno-compare-distinct-pointer-types -Wno-initializer-overrides -Wno-incompatible-pointer-types-discards-qualifiers -Wno-for-loop-analysis -Xclang -flto-visibility-public-std -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf -ferror-limit=1000000" +gcc_common_no_asan="-I../src/ -I../local/ -I/usr/include/freetype2 -g -DBUILD_GIT_HASH=\"$git_hash\" -DBUILD_GIT_HASH_FULL=\"$git_hash_full\" -Wno-unknown-warning-option -Wall -Wno-missing-braces -Wno-unused-function -Wno-attributes -Wno-unused-value -Wno-unused-variable -Wno-unused-local-typedef -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-compare-distinct-pointer-types -D_USE_MATH_DEFINES -Dstrdup=_strdup -Dgnu_printf=printf" + # --- Per-Build Settings ------------------------------------------------------ link_dll="-fPIC" link_os_gfx="-lX11 -lXext -lXrandr -lGL -lGLEW" @@ -48,6 +59,10 @@ if [ -v clang ]; then out="$clang_out"; fi if [ -v debug ]; then compile="$compile_debug"; fi if [ -v release ]; then compile="$compile_release"; fi +# Define special compiler flags for metagen (without ASan) +if [ -v gcc ]; then compile_metagen="$compiler -g -O0 -DBUILD_DEBUG=1 ${gcc_common_no_asan} ${auto_compile_flags} -I/usr/local/include"; fi +if [ -v clang ]; then compile_metagen="$compiler -g -O0 -DBUILD_DEBUG=1 ${clang_common_no_asan} ${auto_compile_flags}"; fi + # --- Prep Directories -------------------------------------------------------- mkdir -p build mkdir -p local From a58bc15846bde6ea928c491ea7fa00e2ca220936 Mon Sep 17 00:00:00 2001 From: Danilo Dumeljic Date: Mon, 7 Apr 2025 21:14:50 +0200 Subject: [PATCH 3/4] Refactor font rasterization and OpenGL texture handling. Improved handling of zero dimensions in font atlas allocation and clarified RGBA channel mapping in shaders. Removed unnecessary comments and ensured consistent memory management. --- .../freetype/font_provider_freetype.c | 30 ++++++++++--------- .../opengl/generated/render_opengl.meta.h | 21 +++++++------ src/render/opengl/render_opengl.c | 30 ++++++++++--------- src/render/opengl/render_opengl.mdesk | 21 +++++++------ 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/font_provider/freetype/font_provider_freetype.c b/src/font_provider/freetype/font_provider_freetype.c index e3d7bbd7c..57e132547 100644 --- a/src/font_provider/freetype/font_provider_freetype.c +++ b/src/font_provider/freetype/font_provider_freetype.c @@ -309,29 +309,31 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S total_width_fixed = pen_x; S32 total_width_pixels = (total_width_fixed + 63) >> 6; - // TODO: Re-evaluate this padding/alignment for clarity/necessity - total_width_pixels += 2; - total_width_pixels += 7; - total_width_pixels -= total_width_pixels % 8; - + // Calculate total height based on maximum ascent/descent values recorded during layout. S32 total_height = max_ascent + max_descent; + + // Handle cases like empty strings where calculated dimensions might be zero or negative. + // Return a valid result structure but indicate zero dimensions and no atlas data. if (total_width_pixels <= 0 || total_height <= 0) { - // Handle zero-size case more carefully - scratch_end(scratch); + scratch_end(scratch); // Release temporary memory. ProfEnd(); - result.advance = (F32)(total_width_fixed >> 6); // Still report advance - result.atlas_dim = v2s16(0,0); - result.atlas = 0; // Or point to zero-size allocation? Original was unclear. + result.advance = (F32)(total_width_fixed >> 6); // Report the calculated advance. + result.atlas_dim = v2s16(0, 0); // Atlas dimensions are zero. + result.atlas = 0; // No atlas buffer allocated. return result; } - //- dan: Allocate atlas buffer + // Allocate the atlas buffer using the precise, unpadded dimensions. + // Atlas format is RGBA8, requiring 4 bytes per pixel. result.atlas_dim = v2s16((S16)total_width_pixels, (S16)total_height); - result.atlas = push_array(arena, U8, total_width_pixels * total_height * 4); - MemoryZero(result.atlas, (U64)total_width_pixels * total_height * 4); + U64 atlas_bytes = (U64)total_width_pixels * (U64)total_height * 4; + // Use push_array, which allocates and zero-initializes the memory. + result.atlas = push_array(arena, U8, atlas_bytes); + + // Calculate row pitch for blitting operations. U8 *out_base = (U8 *)result.atlas; - U64 out_pitch = (U64)total_width_pixels * 4; + U64 out_pitch = (U64)total_width_pixels * 4; // Bytes per row. //- dan: Second pass: Render and blit glyphs using stored positions U64 non_empty_pixel_count = 0; diff --git a/src/render/opengl/generated/render_opengl.meta.h b/src/render/opengl/generated/render_opengl.meta.h index aca133b1c..74189b980 100644 --- a/src/render/opengl/generated/render_opengl.meta.h +++ b/src/render/opengl/generated/render_opengl.meta.h @@ -179,7 +179,7 @@ str8_lit_comp( " vec4 albedo_sample = vec4(1.0);\n" " if(fs_in.omit_texture < 0.5) // Use < 0.5 for bool comparison\n" " {\n" -" // Assuming texture V is flipped if needed in VS\n" +" // REMOVED explicit V flip added previously. Use coords directly from VS.\n" " albedo_sample = texture(main_t2d, fs_in.texcoord_pct) * texture_sample_channel_map;\n" " }\n" "\n" @@ -217,17 +217,20 @@ str8_lit_comp( " corner_sdf_t = 1.0 - smoothstep(0.0, corner_smooth_range, corner_sdf_s);\n" " }\n" "\n" -" // Form final color\n" -" FragColor = albedo_sample * tint;\n" +" // Form final color - Multiply tint and texture colors, combine alphas\n" +" FragColor.rgb = tint.rgb * albedo_sample.rgb; // Multiply RGB components\n" +" FragColor.a = tint.a * albedo_sample.a; // Multiply Alpha components\n" +"\n" +" // Apply global opacity and SDF alpha factors\n" " FragColor.a *= opacity;\n" -" // Combine coverages - Base alpha on the outer shape coverage\n" " FragColor.a *= corner_sdf_t; // Multiply by corner coverage\n" -" FragColor.a *= border_sdf_t; // Multiply by 1.0 (no effect)\n" +" FragColor.a *= border_sdf_t; // Multiply by border coverage\n" +"\n" " // Final discard if alpha is near zero\n" -" if (FragColor.a < 0.001)\n" -" {\n" -" discard;\n" -" }\n" +" if (FragColor.a < 0.001)\n" +" {\n" +" discard;\n" +" }\n" "}\n" "" ); diff --git a/src/render/opengl/render_opengl.c b/src/render/opengl/render_opengl.c index 28364a600..07e72d0dd 100644 --- a/src/render/opengl/render_opengl.c +++ b/src/render/opengl/render_opengl.c @@ -1966,27 +1966,29 @@ r_gl_check_error("UI Bind Texture"); texture->size.y > 0 ? texture->size.y : 1.f); // Avoid divide by zero // Texture channel mapping based on format + // DEFAULT TO IDENTITY for standard RGBA/BGRA formats. Mat4x4F32 tex_map_matrix = m4x4f32(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1); // Identity + + // EXPLICITLY handle single-channel R8 format. if(texture->format == R_Tex2DFormat_R8) { - // Map R channel to R,G,B,A (effectively grayscale for shader) - // Shader: albedo_sample = texture(...) * texture_sample_channel_map; - // Matrix should be column-major for GLSL. C=Row, GL=Col. Need transpose. - // To get (R,R,R,R): First column of transposed matrix = (1,1,1,1). - // C Row-Major Matrix: - // [ 1 0 0 0 ] -> Col 0 -> GLSL sample.r - // [ 1 0 0 0 ] -> Col 1 -> GLSL sample.g - // [ 1 0 0 0 ] -> Col 2 -> GLSL sample.b - // [ 1 0 0 0 ] -> Col 3 -> GLSL sample.a + // Map R channel -> R,G,B,A for grayscale rendering in the shader. + // GLSL matrix multiplication means the first column of the uniform matrix affects the result. + // Target: (R,R,R,R) = Sample(R,G,B,A) * MapMatrix tex_map_matrix = m4x4f32(1,1,1,1, 0,0,0,0, 0,0,0,0, 0,0,0,0); } - Mat4x4F32 transposed = transpose_4x4f32(tex_map_matrix); + // OPTIONAL: Add explicit handling for other formats like BGRA if necessary. + // else if (texture->format == R_Tex2DFormat_BGRA8) { ...swizzle matrix... } + + // Transpose C row-major matrix -> GLSL std140 column-major format for the UBO. + Mat4x4F32 transposed_tex_map = transpose_4x4f32(tex_map_matrix); + // Assign the transposed matrix columns to the uniform structure fields. for (int i = 0; i < 4; i++) { uniforms.texture_sample_channel_map[i] = v4f32( - transposed.v[i][0], - transposed.v[i][1], - transposed.v[i][2], - transposed.v[i][3] + transposed_tex_map.v[i][0], + transposed_tex_map.v[i][1], + transposed_tex_map.v[i][2], + transposed_tex_map.v[i][3] ); } diff --git a/src/render/opengl/render_opengl.mdesk b/src/render/opengl/render_opengl.mdesk index 3b3ffe106..c55f16b1e 100644 --- a/src/render/opengl/render_opengl.mdesk +++ b/src/render/opengl/render_opengl.mdesk @@ -188,7 +188,7 @@ void main() vec4 albedo_sample = vec4(1.0); if(fs_in.omit_texture < 0.5) // Use < 0.5 for bool comparison { - // Assuming texture V is flipped if needed in VS + // REMOVED explicit V flip added previously. Use coords directly from VS. albedo_sample = texture(main_t2d, fs_in.texcoord_pct) * texture_sample_channel_map; } @@ -226,17 +226,20 @@ void main() corner_sdf_t = 1.0 - smoothstep(0.0, corner_smooth_range, corner_sdf_s); } - // Form final color - FragColor = albedo_sample * tint; + // Form final color - Multiply tint and texture colors, combine alphas + FragColor.rgb = tint.rgb * albedo_sample.rgb; // Multiply RGB components + FragColor.a = tint.a * albedo_sample.a; // Multiply Alpha components + + // Apply global opacity and SDF alpha factors FragColor.a *= opacity; - // Combine coverages - Base alpha on the outer shape coverage FragColor.a *= corner_sdf_t; // Multiply by corner coverage - FragColor.a *= border_sdf_t; // Multiply by 1.0 (no effect) + FragColor.a *= border_sdf_t; // Multiply by border coverage + // Final discard if alpha is near zero - if (FragColor.a < 0.001) - { - discard; - } + if (FragColor.a < 0.001) + { + discard; + } } """ From 2f5e255c9fe4d587f6e7714d3b94f7d4c520aa01 Mon Sep 17 00:00:00 2001 From: Danilo Dumeljic Date: Mon, 7 Apr 2025 22:40:33 +0200 Subject: [PATCH 4/4] not sure about the effects of these interface changes to dwrite, but font rendering looks decent now --- src/font_cache/font_cache.c | 15 +- src/font_cache/font_cache.h | 3 +- src/font_provider/font_provider.h | 138 +++++++++------- .../freetype/font_provider_freetype.c | 154 ++++++++++++++---- 4 files changed, 211 insertions(+), 99 deletions(-) diff --git a/src/font_cache/font_cache.c b/src/font_cache/font_cache.c index f302dd22d..e975ea5a6 100644 --- a/src/font_cache/font_cache.c +++ b/src/font_cache/font_cache.c @@ -773,7 +773,15 @@ fnt_push_run_from_string(Arena *arena, FNT_Tag tag, F32 size, F32 base_align_px, info->subrect = chosen_atlas_region; info->atlas_num = chosen_atlas_num; info->raster_dim = raster.atlas_dim; - info->advance = raster.advance; + info->advance = 0; + info->first_glyph_bitmap_top = 0; + if (raster.glyph_count > 0) { + info->first_glyph_bitmap_top = raster.metrics[0].bitmap_top; + for(U64 j = 0; j < raster.glyph_count; ++j) + { + info->advance += raster.metrics[j].advance_x; + } + } } } @@ -819,11 +827,12 @@ fnt_push_run_from_string(Arena *arena, FNT_Tag tag, F32 size, F32 base_align_px, info->subrect.y0 + info->raster_dim.y); piece->advance = advance; piece->decode_size = piece_substring.size; - piece->offset = v2s16(0, -(hash2style_node->ascent + hash2style_node->descent)); + piece->offset.x = 0; + piece->offset.y = -(info->first_glyph_bitmap_top); } base_align_px += advance; dim.x += piece->advance; - dim.y = Max(dim.y, info->raster_dim.y); + dim.y = Max(dim.y, (F32)(hash2style_node->ascent + hash2style_node->descent)); } } } diff --git a/src/font_cache/font_cache.h b/src/font_cache/font_cache.h index 9f343eca6..ba538b8a4 100644 --- a/src/font_cache/font_cache.h +++ b/src/font_cache/font_cache.h @@ -97,9 +97,10 @@ typedef struct F_RasterCacheInfo F_RasterCacheInfo; struct F_RasterCacheInfo { Rng2S16 subrect; - Vec2S16 raster_dim; S16 atlas_num; + Vec2S16 raster_dim; F32 advance; + S16 first_glyph_bitmap_top; }; typedef struct F_Hash2InfoRasterCacheNode F_Hash2InfoRasterCacheNode; diff --git a/src/font_provider/font_provider.h b/src/font_provider/font_provider.h index f836aa191..b30a4a033 100644 --- a/src/font_provider/font_provider.h +++ b/src/font_provider/font_provider.h @@ -1,59 +1,79 @@ -// Copyright (c) 2024 Epic Games Tools -// Licensed under the MIT license (https://opensource.org/license/mit/) - -#ifndef FONT_PROVIDER_H -#define FONT_PROVIDER_H - -#define fp_hook C_LINKAGE - -//////////////////////////////// -//~ rjf: Types - -typedef U32 FP_RasterFlags; -enum -{ - FP_RasterFlag_Smooth = (1<<0), - FP_RasterFlag_Hinted = (1<<1), -}; - -typedef struct FP_Handle FP_Handle; -struct FP_Handle -{ - U64 u64[2]; -}; - -typedef struct FP_Metrics FP_Metrics; -struct FP_Metrics -{ - F32 design_units_per_em; - F32 ascent; - F32 descent; - F32 line_gap; - F32 capital_height; -}; - -typedef struct FP_RasterResult FP_RasterResult; -struct FP_RasterResult -{ - Vec2S16 atlas_dim; - void *atlas; - F32 advance; -}; - -//////////////////////////////// -//~ rjf: Basic Type Functions - -internal FP_Handle fp_handle_zero(void); -internal B32 fp_handle_match(FP_Handle a, FP_Handle b); - -//////////////////////////////// -//~ rjf: Backend Hooks - -fp_hook void fp_init(void); -fp_hook FP_Handle fp_font_open(String8 path); -fp_hook FP_Handle fp_font_open_from_static_data_string(String8 *data_ptr); -fp_hook void fp_font_close(FP_Handle handle); -fp_hook FP_Metrics fp_metrics_from_font(FP_Handle font); -fp_hook NO_ASAN FP_RasterResult fp_raster(Arena *arena, FP_Handle font, F32 size, FP_RasterFlags flags, String8 string); - -#endif // FONT_PROVIDER_H +// Copyright (c) 2024 Epic Games Tools +// Licensed under the MIT license (https://opensource.org/license/mit/) + +#ifndef FONT_PROVIDER_H +#define FONT_PROVIDER_H + +#define fp_hook C_LINKAGE + +//////////////////////////////// +//~ rjf: Types + +typedef U32 FP_RasterFlags; +enum +{ + FP_RasterFlag_Smooth = (1<<0), + FP_RasterFlag_Hinted = (1<<1), +}; + +typedef struct FP_Handle FP_Handle; +struct FP_Handle +{ + U64 u64[2]; +}; + +typedef struct FP_Metrics FP_Metrics; +struct FP_Metrics +{ + F32 design_units_per_em; + F32 ascent; + F32 descent; + F32 line_gap; + F32 capital_height; +}; + +typedef struct FP_GlyphMetrics FP_GlyphMetrics; +struct FP_GlyphMetrics +{ + // Atlas Position & Size (Source Rect) + Rng2S16 src_rect_px; // Position (x0,y0) and dimensions (x1-x0, y1-y0) within the atlas texture + + // Layout Metrics (relative to baseline & pen position) + S16 bitmap_left; // Horizontal offset from pen_x to bitmap's left edge + S16 bitmap_top; // Vertical offset from baseline up to bitmap's top edge + F32 advance_x; // Horizontal advance width for this glyph (in pixels) + + // Original Character Info (optional, but useful for debugging) + U32 codepoint; + U64 string_index; +}; + +typedef struct FP_RasterResult FP_RasterResult; +struct FP_RasterResult +{ + // Atlas Data + Vec2S16 atlas_dim; // Dimensions of the atlas texture + void *atlas; // Pixel data (RGBA8), aligned + + // Per-Glyph Layout Information + U64 glyph_count; // Number of glyphs in the metrics array + FP_GlyphMetrics *metrics; // Array of metrics for each glyph +}; + +//////////////////////////////// +//~ rjf: Basic Type Functions + +internal FP_Handle fp_handle_zero(void); +internal B32 fp_handle_match(FP_Handle a, FP_Handle b); + +//////////////////////////////// +//~ rjf: Backend Hooks + +fp_hook void fp_init(void); +fp_hook FP_Handle fp_font_open(String8 path); +fp_hook FP_Handle fp_font_open_from_static_data_string(String8 *data_ptr); +fp_hook void fp_font_close(FP_Handle handle); +fp_hook FP_Metrics fp_metrics_from_font(FP_Handle font); +fp_hook NO_ASAN FP_RasterResult fp_raster(Arena *arena, FP_Handle font, F32 size, FP_RasterFlags flags, String8 string); + +#endif // FONT_PROVIDER_H diff --git a/src/font_provider/freetype/font_provider_freetype.c b/src/font_provider/freetype/font_provider_freetype.c index 57e132547..28e29c8b1 100644 --- a/src/font_provider/freetype/font_provider_freetype.c +++ b/src/font_provider/freetype/font_provider_freetype.c @@ -194,12 +194,17 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S return result; } - //- dan: Define glyph render info storage - typedef struct GlyphRenderInfo GlyphRenderInfo; - struct GlyphRenderInfo { + //- dan: Define glyph render info storage (layout pass temporary data) + typedef struct GlyphLayoutInfo GlyphLayoutInfo; + struct GlyphLayoutInfo { FT_UInt glyph_index; S32 render_pen_x; // Pen position before this glyph (26.6 fixed point) - S32 advance_x; // Advance width for this glyph (26.6 fixed point) + // Store metrics needed for the second pass and the final result + S16 bitmap_left; + S16 bitmap_top; + U16 bitmap_width; + U16 bitmap_rows; + S32 advance_x; // Advance width for this glyph (26.6 fixed point) }; //- dan: Set pixel size @@ -255,8 +260,8 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S } //- dan: First pass: Calculate layout metrics and store glyph positions - GlyphRenderInfo *glyph_infos = push_array(scratch.arena, GlyphRenderInfo, string32.size); - MemoryZero(glyph_infos, sizeof(GlyphRenderInfo) * string32.size); + GlyphLayoutInfo *layout_infos = push_array(scratch.arena, GlyphLayoutInfo, string32.size); + MemoryZero(layout_infos, sizeof(GlyphLayoutInfo) * string32.size); S32 total_width_fixed = 0; // Use 26.6 for total width calculation S32 max_ascent = 0; @@ -267,15 +272,20 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S for (U64 i = 0; i < string32.size; ++i) { FT_UInt glyph_index = FT_Get_Char_Index(face, string32.str[i]); - glyph_infos[i].glyph_index = glyph_index; + layout_infos[i].glyph_index = glyph_index; // Load glyph metrics *using the final load flags* // FT_LOAD_NO_BITMAP could optimize this, but FT_LOAD_DEFAULT is okay. error = FT_Load_Glyph(face, glyph_index, load_flags); // <<< USE CONSISTENT load_flags if (error) { log_infof("fp_raster: FT_Load_Glyph (metrics pass) failed for glyph index %u (char 0x%x) with error %d", glyph_index, string32.str[i], error); - glyph_infos[i].render_pen_x = pen_x; - glyph_infos[i].advance_x = 0; + layout_infos[i].render_pen_x = pen_x; + layout_infos[i].advance_x = 0; + // Store zero metrics for failed glyphs + layout_infos[i].bitmap_left = 0; + layout_infos[i].bitmap_top = 0; + layout_infos[i].bitmap_width = 0; + layout_infos[i].bitmap_rows = 0; prev_glyph_index = glyph_index; continue; } @@ -290,64 +300,114 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S pen_x += delta.x; } - glyph_infos[i].render_pen_x = pen_x; // Store pen position before advance + layout_infos[i].render_pen_x = pen_x; // Store pen position before advance // Use metrics (ascent/descent) from the loaded glyph (now potentially hinted) - S32 glyph_ascent = slot->bitmap_top; - S32 glyph_descent = (S32)slot->bitmap.rows - slot->bitmap_top; + // NOTE: DO NOT render here. Use metrics directly from the loaded glyph slot. + // FT_Render_Glyph might change metrics slightly due to hinting. + // if (slot->format != FT_GLYPH_FORMAT_BITMAP) + // { + // // Render temporarily to get bitmap metrics, won't affect final render pass + // FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL); + // } + + // Use metrics directly from the slot after FT_Load_Glyph + S32 glyph_ascent = slot->metrics.horiBearingY >> 6; // Use FUnit metrics initially? + S32 glyph_descent = (slot->metrics.height >> 6) - glyph_ascent; + // Use bitmap metrics if available and potentially more accurate after load_flags applied? + // Let's stick to bitmap_top/rows for consistency with blitting, but load them *before* render. + S32 reported_bitmap_top = slot->bitmap_top; + S32 reported_bitmap_rows = (S32)slot->bitmap.rows; + + // Recalculate ascent/descent based on bitmap positioning relative to baseline + glyph_ascent = reported_bitmap_top; + glyph_descent = reported_bitmap_rows - reported_bitmap_top; + if (glyph_ascent > max_ascent) { max_ascent = glyph_ascent; } if (glyph_descent > max_descent) { max_descent = glyph_descent; } + // Store metrics needed for second pass and result (using values from loaded slot) + layout_infos[i].bitmap_left = (S16)slot->bitmap_left; + layout_infos[i].bitmap_top = (S16)reported_bitmap_top; // Store the value used for max_ascent + layout_infos[i].bitmap_width = (U16)slot->bitmap.width; + layout_infos[i].bitmap_rows = (U16)reported_bitmap_rows; // Store the value used for glyph_descent calc + // Use advance from the potentially hinted glyph pen_x += slot->advance.x; - glyph_infos[i].advance_x = slot->advance.x; // Store potentially hinted advance + layout_infos[i].advance_x = slot->advance.x; // Store potentially hinted advance prev_glyph_index = glyph_index; } + // Also log the final calculated atlas height parameters + log_infof("fp_raster Metric Log: Final max_ascent=%d, max_descent=%d, total_height=%d", max_ascent, max_descent, max_ascent + max_descent); + // Calculate total width and height based on layout (using potentially hinted metrics) total_width_fixed = pen_x; - S32 total_width_pixels = (total_width_fixed + 63) >> 6; + S32 total_width_pixels = (total_width_fixed + 32) >> 6; // Round up width slightly maybe? or +63? + if (total_width_pixels < 0) total_width_pixels = 0; // Calculate total height based on maximum ascent/descent values recorded during layout. S32 total_height = max_ascent + max_descent; + if (total_height < 0) total_height = 0; - // Handle cases like empty strings where calculated dimensions might be zero or negative. - // Return a valid result structure but indicate zero dimensions and no atlas data. + // Handle cases like empty strings where calculated dimensions might be zero. if (total_width_pixels <= 0 || total_height <= 0) { - scratch_end(scratch); // Release temporary memory. + // Return zero dimensions but still allocate metrics array for consistency + result.atlas_dim = v2s16(0, 0); + result.atlas = 0; + result.glyph_count = string32.size; + result.metrics = push_array(arena, FP_GlyphMetrics, string32.size); // Allocate zeroed metrics + // Populate metrics with zero values or minimal info + for (U64 i = 0; i < string32.size; ++i) { + result.metrics[i].src_rect_px = r2s16p(0, 0, 0, 0); + result.metrics[i].bitmap_left = 0; + result.metrics[i].bitmap_top = 0; + result.metrics[i].advance_x = (F32)(layout_infos[i].advance_x >> 6); // Still provide advance + result.metrics[i].codepoint = string32.str[i]; + result.metrics[i].string_index = i; + } + scratch_end(scratch); ProfEnd(); - result.advance = (F32)(total_width_fixed >> 6); // Report the calculated advance. - result.atlas_dim = v2s16(0, 0); // Atlas dimensions are zero. - result.atlas = 0; // No atlas buffer allocated. return result; } // Allocate the atlas buffer using the precise, unpadded dimensions. - // Atlas format is RGBA8, requiring 4 bytes per pixel. result.atlas_dim = v2s16((S16)total_width_pixels, (S16)total_height); U64 atlas_bytes = (U64)total_width_pixels * (U64)total_height * 4; - // Use push_array, which allocates and zero-initializes the memory. - result.atlas = push_array(arena, U8, atlas_bytes); + result.atlas = push_array_aligned(arena, U8, atlas_bytes, 16); // Align atlas memory + + // Allocate the result metrics array + result.glyph_count = string32.size; + result.metrics = push_array(arena, FP_GlyphMetrics, string32.size); + MemoryZero(result.metrics, sizeof(FP_GlyphMetrics) * result.glyph_count); // Zero initialize // Calculate row pitch for blitting operations. U8 *out_base = (U8 *)result.atlas; U64 out_pitch = (U64)total_width_pixels * 4; // Bytes per row. - //- dan: Second pass: Render and blit glyphs using stored positions + //- dan: Second pass: Render and blit glyphs using stored positions, populate metrics U64 non_empty_pixel_count = 0; for (U64 i = 0; i < string32.size; ++i) { - FT_UInt glyph_index = glyph_infos[i].glyph_index; - S32 render_pen_x_fixed = glyph_infos[i].render_pen_x; // Stored 26.6 pen position + FT_UInt glyph_index = layout_infos[i].glyph_index; + S32 render_pen_x_fixed = layout_infos[i].render_pen_x; // Stored 26.6 pen position + + // Populate FP_GlyphMetrics for this glyph using layout_infos data + FP_GlyphMetrics *out_metric = &result.metrics[i]; + out_metric->bitmap_left = layout_infos[i].bitmap_left; + out_metric->bitmap_top = layout_infos[i].bitmap_top; + out_metric->advance_x = (F32)(layout_infos[i].advance_x / 64.0f); // Convert 26.6 advance to float pixels + out_metric->codepoint = string32.str[i]; + out_metric->string_index = i; // Load glyph again (might be necessary if FT_Load_Glyph doesn't cache everything) // or ensure first pass loaded everything needed. Reloading is safer. error = FT_Load_Glyph(face, glyph_index, load_flags); // <<< Use final load_flags if (error) { - // log_infof(...) + out_metric->src_rect_px = r2s16p(0,0,0,0); // Zero rect for errors continue; } @@ -356,7 +416,7 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S { error = FT_Render_Glyph(face->glyph, render_mode); // <<< Use final render_mode if (error) { - // log_infof(...) + out_metric->src_rect_px = r2s16p(0,0,0,0); // Zero rect for errors continue; } } @@ -364,9 +424,24 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S FT_GlyphSlot slot = face->glyph; FT_Bitmap *bitmap = &slot->bitmap; - // Calculate blit position using consistent metrics - S32 blit_x = (render_pen_x_fixed >> 6) + slot->bitmap_left; - S32 blit_y = max_ascent - slot->bitmap_top; // max_ascent also calculated consistently now + // REMOVED Assertions: Dimensions might differ slightly after rendering due to hinting. + // Assert((U16)bitmap->width == layout_infos[i].bitmap_width); + // Assert((U16)bitmap->rows == layout_infos[i].bitmap_rows); + // Assert((S16)slot->bitmap_left == layout_infos[i].bitmap_left); + // Assert((S16)slot->bitmap_top == layout_infos[i].bitmap_top); + + + // Calculate blit position using metrics stored from the FIRST pass (layout_infos) + S32 blit_x = (render_pen_x_fixed >> 6) + layout_infos[i].bitmap_left; + S32 blit_y = max_ascent - layout_infos[i].bitmap_top; + + // Set the source rect in the output metrics using the blit position + // and the ACTUAL rendered bitmap dimensions from the SECOND pass. + out_metric->src_rect_px.x0 = (S16)blit_x; + out_metric->src_rect_px.y0 = (S16)blit_y; + out_metric->src_rect_px.x1 = (S16)(blit_x + bitmap->width); // Use rendered width + out_metric->src_rect_px.y1 = (S16)(blit_y + bitmap->rows); // Use rendered height + // Blit the bitmap to the RGBA atlas U8 *in_row = bitmap->buffer; @@ -401,7 +476,7 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S else if (bitmap->pixel_mode == FT_PIXEL_MODE_LCD) { U8 *in_pixel_rgb = in_row; - for (unsigned int x = 0; x < bitmap->width; ++x) + for (unsigned int x = 0; x < bitmap->width / 3; ++x) // Iterate over RGB triples { S32 atlas_x = blit_x + (S32)x; if (atlas_x >= 0 && atlas_x < total_width_pixels) // Clip horizontally @@ -411,7 +486,9 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S U8 r = in_pixel_rgb[0]; U8 g = in_pixel_rgb[1]; U8 b = in_pixel_rgb[2]; - U32 alpha_u32 = (54*r + 183*g + 19*b) >> 8; + // Standard weights for luminance conversion + U32 alpha_u32 = (U32)(0.2126f * r + 0.7152f * g + 0.0722f * b); + // U32 alpha_u32 = (54*r + 183*g + 19*b) >> 8; // Alternative integer weights U8 alpha = (U8)(alpha_u32 > 255 ? 255 : alpha_u32); out_pixel[0] = 255; out_pixel[1] = 255; @@ -448,19 +525,24 @@ fp_raster(Arena *arena, FP_Handle font_handle, F32 size, FP_RasterFlags flags, S in_pixel_bgra += 4; } } + else if (bitmap->pixel_mode == FT_PIXEL_MODE_MONO) + { + // TODO: Handle mono bitmaps if necessary + } } in_row += bitmap->pitch; } } - //- dan: Finalize result - result.advance = (F32)(total_width_fixed >> 6); // Use final fixed-point width from first pass + //- dan: Finalize result (Removed advance field) + // result.advance = (F32)(total_width_fixed >> 6); // Use final fixed-point width from first pass // If nothing visible rendered, ensure atlas dimensions reflect that. if (non_empty_pixel_count == 0) { result.atlas_dim = v2s16(0, 0); - // Consider freeing result.atlas or ensuring it's handled correctly downstream. + // Atlas buffer is already allocated but will be effectively empty. + // Caller should ideally check atlas_dim. } scratch_end(scratch);