diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a1d18231..8ae39667b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,32 +75,35 @@ jobs: fail-fast: false matrix: # MacOS 13 runs on Intel, 14 runs on ARM - os: [ubuntu-latest, macos-13, macos-14] + os: [macos-13, macos-14, ubuntu-latest] runs-on: ${{ matrix.os }} - name: ${{ matrix.os == 'macos-14' && 'MacOS ARM' || (matrix.os == 'macos-13' && 'MacOS Intel' || 'Ubuntu') }} Build, Check, and Test + name: ${{ matrix.os == 'macos-14' && 'MacOS ARM' || (matrix.os == 'macos-13' && 'MacOS Intel') || (matrix.os == 'ubuntu-latest' && 'Ubuntu') }} Build, Check, and Test timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - name: Download LLVM (Linux) - if: matrix.os == 'ubuntu-latest' - run: | - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 20 - echo "/usr/lib/llvm-20/bin" >> $GITHUB_PATH + - uses: actions/checkout@v4 - name: Download LLVM (MacOS Intel) if: matrix.os == 'macos-13' run: | brew update brew install llvm@20 lua@5.4 lld + echo "$(brew --prefix llvm@20)/bin" >> $GITHUB_PATH - name: Download LLVM (MacOS ARM) if: matrix.os == 'macos-14' run: | brew update brew install llvm@20 wasmtime lua@5.4 lld + echo "$(brew --prefix llvm@20)/bin" >> $GITHUB_PATH + + - name: Download LLVM (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 20 + echo "/usr/lib/llvm-20/bin" >> $GITHUB_PATH - name: Build Odin run: ./build_odin.sh release @@ -124,53 +127,52 @@ jobs: - name: Odin check vendor/sdl3 run: ./odin check vendor/sdl3 -strict-style -vet -disallow-do -no-entry-point - name: Normal Core library tests - run: ./odin test tests/core/normal.odin -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + run: ./odin test tests/core/normal.odin -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Optimized Core library tests - run: ./odin test tests/core/speed.odin -o:speed -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + run: ./odin test tests/core/speed.odin -o:speed -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Vendor library tests - run: ./odin test tests/vendor -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + run: ./odin test tests/vendor -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Internals tests - run: ./odin test tests/internal -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + run: ./odin test tests/internal -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: GitHub Issue tests run: | cd tests/issues ./run.sh - - name: Check benchmarks - run: ./odin check tests/benchmark -vet -strict-style -no-entry-point - - name: Odin check examples/all for Linux i386 - run: ./odin check examples/all -vet -strict-style -disallow-do -target:linux_i386 - if: matrix.os == 'ubuntu-latest' - - name: Odin check examples/all for Linux arm64 - run: ./odin check examples/all -vet -strict-style -disallow-do -target:linux_arm64 - if: matrix.os == 'ubuntu-latest' - - name: Odin check examples/all for FreeBSD amd64 - run: ./odin check examples/all -vet -strict-style -disallow-do -target:freebsd_amd64 - if: matrix.os == 'ubuntu-latest' - - name: Odin check examples/all for OpenBSD amd64 - run: ./odin check examples/all -vet -strict-style -disallow-do -target:openbsd_amd64 - if: matrix.os == 'ubuntu-latest' - - - name: Odin check vendor/sdl3 for Linux i386 - run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:linux_i386 - if: matrix.os == 'ubuntu-latest' - - name: Odin check vendor/sdl3 for Linux arm64 - run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:linux_arm64 - if: matrix.os == 'ubuntu-latest' - - name: Odin check vendor/sdl3 for FreeBSD amd64 - run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:freebsd_amd64 - if: matrix.os == 'ubuntu-latest' - - name: Odin check vendor/sdl3 for OpenBSD amd64 - run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:openbsd_amd64 - if: matrix.os == 'ubuntu-latest' - - - name: Run demo on WASI WASM32 run: | ./odin build examples/demo -target:wasi_wasm32 -vet -strict-style -disallow-do -out:demo wasmtime ./demo.wasm if: matrix.os == 'macos-14' + - name: Check benchmarks + run: ./odin check tests/benchmark -vet -strict-style -no-entry-point + - name: Odin check examples/all for Linux i386 + if: matrix.os == 'ubuntu-latest' + run: ./odin check examples/all -vet -strict-style -disallow-do -target:linux_i386 + - name: Odin check examples/all for Linux arm64 + if: matrix.os == 'ubuntu-latest' + run: ./odin check examples/all -vet -strict-style -disallow-do -target:linux_arm64 + - name: Odin check examples/all for FreeBSD amd64 + if: matrix.os == 'ubuntu-latest' + run: ./odin check examples/all -vet -strict-style -disallow-do -target:freebsd_amd64 + - name: Odin check examples/all for OpenBSD amd64 + if: matrix.os == 'ubuntu-latest' + run: ./odin check examples/all -vet -strict-style -disallow-do -target:openbsd_amd64 + + - name: Odin check vendor/sdl3 for Linux i386 + if: matrix.os == 'ubuntu-latest' + run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:linux_i386 + - name: Odin check vendor/sdl3 for Linux arm64 + if: matrix.os == 'ubuntu-latest' + run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:linux_arm64 + - name: Odin check vendor/sdl3 for FreeBSD amd64 + if: matrix.os == 'ubuntu-latest' + run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:freebsd_amd64 + - name: Odin check vendor/sdl3 for OpenBSD amd64 + if: matrix.os == 'ubuntu-latest' + run: ./odin check vendor/sdl3 -vet -strict-style -disallow-do -no-entry-point -target:openbsd_amd64 + build_windows: name: Windows Build, Check, and Test runs-on: windows-2022 @@ -215,23 +217,23 @@ jobs: shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat - odin test tests/core/normal.odin -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + odin test tests/core/normal.odin -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Optimized core library tests shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat - odin test tests/core/speed.odin -o:speed -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + odin test tests/core/speed.odin -o:speed -file -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Vendor library tests shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat copy vendor\lua\5.4\windows\*.dll . - odin test tests/vendor -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + odin test tests/vendor -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Odin internals tests shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat - odin test tests/internal -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true + odin test tests/internal -all-packages -vet -strict-style -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address - name: Check issues shell: cmd run: | diff --git a/.gitignore b/.gitignore index 75bb61c43..fdb030309 100644 --- a/.gitignore +++ b/.gitignore @@ -293,6 +293,7 @@ build.sh # RAD debugger project file *.raddbg - +*.rdi +tests/issues/build/* misc/featuregen/featuregen codegen/build/gen_src.map diff --git a/README.md b/README.md index 7703c0a7d..7c39e273b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ There were additions made for quality of life reasons: - +

diff --git a/base/builtin/builtin.odin b/base/builtin/builtin.odin index 227ceeb49..14da9603d 100644 --- a/base/builtin/builtin.odin +++ b/base/builtin/builtin.odin @@ -119,7 +119,8 @@ jmag :: proc(value: Quaternion) -> Float --- kmag :: proc(value: Quaternion) -> Float --- conj :: proc(value: Complex_Or_Quaternion) -> Complex_Or_Quaternion --- -expand_values :: proc(value: Struct_Or_Array) -> (A, B, C, ...) --- +expand_values :: proc(value: Struct_Or_Array) -> (A, B, C, ...) --- +compress_values :: proc(values: ...) -> Struct_Or_Array_Like_Type --- min :: proc(values: ..T) -> T --- max :: proc(values: ..T) -> T --- diff --git a/base/intrinsics/intrinsics.odin b/base/intrinsics/intrinsics.odin index bec452007..46e39c8d1 100644 --- a/base/intrinsics/intrinsics.odin +++ b/base/intrinsics/intrinsics.odin @@ -221,6 +221,9 @@ type_map_cell_info :: proc($T: typeid) -> ^runtime.Map_Cell_Info --- type_convert_variants_to_pointers :: proc($T: typeid) -> typeid where type_is_union(T) --- type_merge :: proc($U, $V: typeid) -> typeid where type_is_union(U), type_is_union(V) --- +type_integer_to_unsigned :: proc($T: typeid) -> type where type_is_integer(T), !type_is_unsigned(T) --- +type_integer_to_signed :: proc($T: typeid) -> type where type_is_integer(T), type_is_unsigned(T) --- + type_has_shared_fields :: proc($U, $V: typeid) -> bool where type_is_struct(U), type_is_struct(V) --- constant_utf16_cstring :: proc($literal: string) -> [^]u16 --- @@ -274,8 +277,12 @@ simd_lanes_ge :: proc(a, b: #simd[N]T) -> #simd[N]Integer --- simd_extract :: proc(a: #simd[N]T, idx: uint) -> T --- simd_replace :: proc(a: #simd[N]T, idx: uint, elem: T) -> #simd[N]T --- +simd_reduce_add_bisect :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- +simd_reduce_mul_bisect :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- simd_reduce_add_ordered :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- simd_reduce_mul_ordered :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- +simd_reduce_add_pairs :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- +simd_reduce_mul_pairs :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- simd_reduce_min :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- simd_reduce_max :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- simd_reduce_and :: proc(a: #simd[N]T) -> T where type_is_integer(T) || type_is_float(T)--- @@ -298,7 +305,7 @@ simd_masked_store :: proc(ptr: rawptr, val: #simd[N]T, mask: #simd[N]U) simd_masked_expand_load :: proc(ptr: rawptr, val: #simd[N]T, mask: #simd[N]U) -> #simd[N]T where type_is_integer(U) || type_is_boolean(U) --- simd_masked_compress_store :: proc(ptr: rawptr, val: #simd[N]T, mask: #simd[N]U) where type_is_integer(U) || type_is_boolean(U) --- - +simd_indices :: proc($T: typeid/#simd[$N]$E) -> T where type_is_numeric(T) --- simd_shuffle :: proc(a, b: #simd[N]T, indices: ..int) -> #simd[len(indices)]T --- simd_select :: proc(cond: #simd[N]boolean_or_integer, true, false: #simd[N]T) -> #simd[N]T --- @@ -353,15 +360,18 @@ x86_xgetbv :: proc(cx: u32) -> (eax, edx: u32) --- objc_object :: struct{} objc_selector :: struct{} objc_class :: struct{} +objc_ivar :: struct{} + objc_id :: ^objc_object objc_SEL :: ^objc_selector objc_Class :: ^objc_class +objc_Ivar :: ^objc_ivar objc_find_selector :: proc($name: string) -> objc_SEL --- objc_register_selector :: proc($name: string) -> objc_SEL --- objc_find_class :: proc($name: string) -> objc_Class --- objc_register_class :: proc($name: string) -> objc_Class --- - +objc_ivar_get :: proc(self: ^$T) -> ^$U --- valgrind_client_request :: proc(default: uintptr, request: uintptr, a0, a1, a2, a3, a4: uintptr) -> uintptr --- diff --git a/base/runtime/core.odin b/base/runtime/core.odin index cd7d35540..1dadbbf6f 100644 --- a/base/runtime/core.odin +++ b/base/runtime/core.odin @@ -110,7 +110,6 @@ Type_Info_Parameters :: struct { // Only used for procedures parameters and resu types: []^Type_Info, names: []string, } -Type_Info_Tuple :: Type_Info_Parameters // Will be removed eventually Type_Info_Struct_Flags :: distinct bit_set[Type_Info_Struct_Flag; u8] Type_Info_Struct_Flag :: enum u8 { @@ -559,10 +558,14 @@ ALL_ODIN_OS_TYPES :: Odin_OS_Types{ Odin_Platform_Subtarget_Type :: enum int { Default, iOS, + Android, } */ Odin_Platform_Subtarget_Type :: type_of(ODIN_PLATFORM_SUBTARGET) +Odin_Platform_Subtarget_Types :: bit_set[Odin_Platform_Subtarget_Type] + + /* // Defined internally by the compiler Odin_Sanitizer_Flag :: enum u32 { diff --git a/base/runtime/default_temp_allocator_arena.odin b/base/runtime/default_temp_allocator_arena.odin index 15c27e316..5700fec44 100644 --- a/base/runtime/default_temp_allocator_arena.odin +++ b/base/runtime/default_temp_allocator_arena.odin @@ -1,6 +1,7 @@ package runtime import "base:intrinsics" +import "base:sanitizer" DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE :: uint(DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE) @@ -43,6 +44,8 @@ memory_block_alloc :: proc(allocator: Allocator, capacity: uint, alignment: uint block.base = ([^]byte)(uintptr(block) + base_offset) block.capacity = uint(end - uintptr(block.base)) + sanitizer.address_poison(block.base, block.capacity) + // Should be zeroed assert(block.used == 0) assert(block.prev == nil) @@ -52,6 +55,7 @@ memory_block_alloc :: proc(allocator: Allocator, capacity: uint, alignment: uint memory_block_dealloc :: proc(block_to_free: ^Memory_Block, loc := #caller_location) { if block_to_free != nil { allocator := block_to_free.allocator + sanitizer.address_unpoison(block_to_free.base, block_to_free.capacity) mem_free(block_to_free, allocator, loc) } } @@ -83,6 +87,7 @@ alloc_from_memory_block :: proc(block: ^Memory_Block, min_size, alignment: uint) return } data = block.base[block.used+alignment_offset:][:min_size] + sanitizer.address_unpoison(block.base[block.used:block.used+size]) block.used += size return } @@ -164,6 +169,7 @@ arena_free_all :: proc(arena: ^Arena, loc := #caller_location) { if arena.curr_block != nil { intrinsics.mem_zero(arena.curr_block.base, arena.curr_block.used) arena.curr_block.used = 0 + sanitizer.address_poison(arena.curr_block.base, arena.curr_block.capacity) } arena.total_used = 0 } @@ -228,6 +234,7 @@ arena_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode, // grow data in-place, adjusting next allocation block.used = uint(new_end) data = block.base[start:new_end] + sanitizer.address_unpoison(data) return } } @@ -301,6 +308,7 @@ arena_temp_end :: proc(temp: Arena_Temp, loc := #caller_location) { assert(block.used >= temp.used, "out of order use of arena_temp_end", loc) amount_to_zero := block.used-temp.used intrinsics.mem_zero(block.base[temp.used:], amount_to_zero) + sanitizer.address_poison(block.base[temp.used:block.capacity]) block.used = temp.used arena.total_used -= amount_to_zero } diff --git a/base/runtime/dynamic_map_internal.odin b/base/runtime/dynamic_map_internal.odin index ea3ecd986..d63c24355 100644 --- a/base/runtime/dynamic_map_internal.odin +++ b/base/runtime/dynamic_map_internal.odin @@ -1033,3 +1033,32 @@ default_hasher_cstring :: proc "contextless" (data: rawptr, seed: uintptr) -> ui h &= HASH_MASK return uintptr(h) | uintptr(uintptr(h) == 0) } + +default_hasher_f64 :: proc "contextless" (f: f64, seed: uintptr) -> uintptr { + f := f + buf: [size_of(f)]u8 + if f == 0 { + return default_hasher(&buf, seed, size_of(buf)) + } + if f != f { + // TODO(bill): What should the logic be for NaNs? + return default_hasher(&f, seed, size_of(f)) + } + return default_hasher(&f, seed, size_of(f)) +} + +default_hasher_complex128 :: proc "contextless" (x, y: f64, seed: uintptr) -> uintptr { + seed := seed + seed = default_hasher_f64(x, seed) + seed = default_hasher_f64(y, seed) + return seed +} + +default_hasher_quaternion256 :: proc "contextless" (x, y, z, w: f64, seed: uintptr) -> uintptr { + seed := seed + seed = default_hasher_f64(x, seed) + seed = default_hasher_f64(y, seed) + seed = default_hasher_f64(z, seed) + seed = default_hasher_f64(w, seed) + return seed +} \ No newline at end of file diff --git a/base/runtime/heap_allocator_windows.odin b/base/runtime/heap_allocator_windows.odin index e07df7559..04a6f149b 100644 --- a/base/runtime/heap_allocator_windows.odin +++ b/base/runtime/heap_allocator_windows.odin @@ -1,5 +1,7 @@ package runtime +import "../sanitizer" + foreign import kernel32 "system:Kernel32.lib" @(private="file") @@ -16,7 +18,10 @@ foreign kernel32 { _heap_alloc :: proc "contextless" (size: int, zero_memory := true) -> rawptr { HEAP_ZERO_MEMORY :: 0x00000008 - return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY if zero_memory else 0, uint(size)) + ptr := HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY if zero_memory else 0, uint(size)) + // NOTE(lucas): asan not guarunteed to unpoison win32 heap out of the box, do it ourselves + sanitizer.address_unpoison(ptr, size) + return ptr } _heap_resize :: proc "contextless" (ptr: rawptr, new_size: int) -> rawptr { if new_size == 0 { @@ -28,7 +33,10 @@ _heap_resize :: proc "contextless" (ptr: rawptr, new_size: int) -> rawptr { } HEAP_ZERO_MEMORY :: 0x00000008 - return HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, ptr, uint(new_size)) + new_ptr := HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, ptr, uint(new_size)) + // NOTE(lucas): asan not guarunteed to unpoison win32 heap out of the box, do it ourselves + sanitizer.address_unpoison(new_ptr, new_size) + return new_ptr } _heap_free :: proc "contextless" (ptr: rawptr) { if ptr == nil { diff --git a/base/runtime/internal.odin b/base/runtime/internal.odin index 59811a525..38b7f662c 100644 --- a/base/runtime/internal.odin +++ b/base/runtime/internal.odin @@ -1106,3 +1106,11 @@ __read_bits :: proc "contextless" (dst, src: [^]byte, offset: uintptr, size: uin dst[j>>3] |= the_bit<<(j&7) } } + +when .Address in ODIN_SANITIZER_FLAGS { + foreign { + @(require) + __asan_unpoison_memory_region :: proc "system" (address: rawptr, size: uint) --- + } +} + diff --git a/base/runtime/procs_darwin.odin b/base/runtime/procs_darwin.odin index c3fc46af1..0aec57e80 100644 --- a/base/runtime/procs_darwin.odin +++ b/base/runtime/procs_darwin.odin @@ -2,21 +2,34 @@ package runtime @(priority_index=-1e6) -foreign import "system:Foundation.framework" +foreign import ObjC "system:objc" import "base:intrinsics" -objc_id :: ^intrinsics.objc_object +objc_id :: ^intrinsics.objc_object objc_Class :: ^intrinsics.objc_class -objc_SEL :: ^intrinsics.objc_selector +objc_SEL :: ^intrinsics.objc_selector +objc_Ivar :: ^intrinsics.objc_ivar +objc_BOOL :: bool -foreign Foundation { - objc_lookUpClass :: proc "c" (name: cstring) -> objc_Class --- + +objc_IMP :: proc "c" (object: objc_id, sel: objc_SEL, #c_vararg args: ..any) -> objc_id + +foreign ObjC { sel_registerName :: proc "c" (name: cstring) -> objc_SEL --- - objc_allocateClassPair :: proc "c" (superclass: objc_Class, name: cstring, extraBytes: uint) -> objc_Class --- objc_msgSend :: proc "c" (self: objc_id, op: objc_SEL, #c_vararg args: ..any) --- objc_msgSend_fpret :: proc "c" (self: objc_id, op: objc_SEL, #c_vararg args: ..any) -> f64 --- objc_msgSend_fp2ret :: proc "c" (self: objc_id, op: objc_SEL, #c_vararg args: ..any) -> complex128 --- objc_msgSend_stret :: proc "c" (self: objc_id, op: objc_SEL, #c_vararg args: ..any) --- + + objc_lookUpClass :: proc "c" (name: cstring) -> objc_Class --- + objc_allocateClassPair :: proc "c" (superclass: objc_Class, name: cstring, extraBytes: uint) -> objc_Class --- + objc_registerClassPair :: proc "c" (cls : objc_Class) --- + class_addMethod :: proc "c" (cls: objc_Class, name: objc_SEL, imp: objc_IMP, types: cstring) -> objc_BOOL --- + class_addIvar :: proc "c" (cls: objc_Class, name: cstring, size: uint, alignment: u8, types: cstring) -> objc_BOOL --- + class_getInstanceVariable :: proc "c" (cls : objc_Class, name: cstring) -> objc_Ivar --- + class_getInstanceSize :: proc "c" (cls : objc_Class) -> uint --- + ivar_getOffset :: proc "c" (v: objc_Ivar) -> uintptr --- } + diff --git a/base/sanitizer/address.odin b/base/sanitizer/address.odin new file mode 100644 index 000000000..edfdfc172 --- /dev/null +++ b/base/sanitizer/address.odin @@ -0,0 +1,601 @@ +#+no-instrumentation +package sanitizer + +Address_Death_Callback :: #type proc "c" (pc: rawptr, bp: rawptr, sp: rawptr, addr: rawptr, is_write: i32, access_size: uint) + +@(private="file") +ASAN_ENABLED :: .Address in ODIN_SANITIZER_FLAGS + +@(private="file") +@(default_calling_convention="system") +foreign { + __asan_poison_memory_region :: proc(address: rawptr, size: uint) --- + __asan_unpoison_memory_region :: proc(address: rawptr, size: uint) --- + __sanitizer_set_death_callback :: proc(callback: Address_Death_Callback) --- + __asan_region_is_poisoned :: proc(begin: rawptr, size: uint) -> rawptr --- + __asan_address_is_poisoned :: proc(addr: rawptr) -> i32 --- + __asan_describe_address :: proc(addr: rawptr) --- + __asan_report_present :: proc() -> i32 --- + __asan_get_report_pc :: proc() -> rawptr --- + __asan_get_report_bp :: proc() -> rawptr --- + __asan_get_report_sp :: proc() -> rawptr --- + __asan_get_report_address :: proc() -> rawptr --- + __asan_get_report_access_type :: proc() -> i32 --- + __asan_get_report_access_size :: proc() -> uint --- + __asan_get_report_description :: proc() -> cstring --- + __asan_locate_address :: proc(addr: rawptr, name: rawptr, name_size: uint, region_address: ^rawptr, region_size: ^uint) -> cstring --- + __asan_get_alloc_stack :: proc(addr: rawptr, trace: rawptr, size: uint, thread_id: ^i32) -> uint --- + __asan_get_free_stack :: proc(addr: rawptr, trace: rawptr, size: uint, thread_id: ^i32) -> uint --- + __asan_get_shadow_mapping :: proc(shadow_scale: ^uint, shadow_offset: ^uint) --- + __asan_print_accumulated_stats :: proc() --- + __asan_get_current_fake_stack :: proc() -> rawptr --- + __asan_addr_is_in_fake_stack :: proc(fake_stack: rawptr, addr: rawptr, beg: ^rawptr, end: ^rawptr) -> rawptr --- + __asan_handle_no_return :: proc() --- + __asan_update_allocation_context :: proc(addr: rawptr) -> i32 --- +} + +Address_Access_Type :: enum { + none, + read, + write, +} + +Address_Located_Address :: struct { + category: string, + name: string, + region: []byte, +} + +Address_Shadow_Mapping :: struct { + scale: uint, + offset: uint, +} + +/* +Marks a slice as unaddressable + +Code instrumented with `-sanitize:address` is forbidden from accessing any address +within the slice. This procedure is not thread-safe because no two threads can +poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_poison_slice :: proc "contextless" (region: $T/[]$E) { + when ASAN_ENABLED { + __asan_poison_memory_region(raw_data(region), size_of(E) * len(region)) + } +} + +/* +Marks a slice as addressable + +Code instrumented with `-sanitize:address` is allowed to access any address +within the slice again. This procedure is not thread-safe because no two threads +can poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_unpoison_slice :: proc "contextless" (region: $T/[]$E) { + when ASAN_ENABLED { + __asan_unpoison_memory_region(raw_data(region), size_of(E) * len(region)) + } +} + +/* +Marks a pointer as unaddressable + +Code instrumented with `-sanitize:address` is forbidden from accessing any address +within the region the pointer points to. This procedure is not thread-safe because no +two threads can poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_poison_ptr :: proc "contextless" (ptr: ^$T) { + when ASAN_ENABLED { + __asan_poison_memory_region(ptr, size_of(T)) + } +} + +/* +Marks a pointer as addressable + +Code instrumented with `-sanitize:address` is allowed to access any address +within the region the pointer points to again. This procedure is not thread-safe +because no two threads can poison or unpoison memory in the same memory region +region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_unpoison_ptr :: proc "contextless" (ptr: ^$T) { + when ASAN_ENABLED { + __asan_unpoison_memory_region(ptr, size_of(T)) + } +} + +/* +Marks the region covering `[ptr, ptr+len)` as unaddressable + +Code instrumented with `-sanitize:address` is forbidden from accessing any address +within the region. This procedure is not thread-safe because no two threads can +poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_poison_rawptr :: proc "contextless" (ptr: rawptr, len: int) { + when ASAN_ENABLED { + assert_contextless(len >= 0) + __asan_poison_memory_region(ptr, uint(len)) + } +} + +/* +Marks the region covering `[ptr, ptr+len)` as unaddressable + +Code instrumented with `-sanitize:address` is forbidden from accessing any address +within the region. This procedure is not thread-safe because no two threads can +poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_poison_rawptr_uint :: proc "contextless" (ptr: rawptr, len: uint) { + when ASAN_ENABLED { + __asan_poison_memory_region(ptr, len) + } +} + +/* +Marks the region covering `[ptr, ptr+len)` as addressable + +Code instrumented with `-sanitize:address` is allowed to access any address +within the region again. This procedure is not thread-safe because no two +threads can poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_unpoison_rawptr :: proc "contextless" (ptr: rawptr, len: int) { + when ASAN_ENABLED { + assert_contextless(len >= 0) + __asan_unpoison_memory_region(ptr, uint(len)) + } +} + +/* +Marks the region covering `[ptr, ptr+len)` as addressable + +Code instrumented with `-sanitize:address` is allowed to access any address +within the region again. This procedure is not thread-safe because no two +threads can poison or unpoison memory in the same memory region region simultaneously. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_unpoison_rawptr_uint :: proc "contextless" (ptr: rawptr, len: uint) { + when ASAN_ENABLED { + __asan_unpoison_memory_region(ptr, len) + } +} + +address_poison :: proc { + address_poison_slice, + address_poison_ptr, + address_poison_rawptr, + address_poison_rawptr_uint, +} + +address_unpoison :: proc { + address_unpoison_slice, + address_unpoison_ptr, + address_unpoison_rawptr, + address_unpoison_rawptr_uint, +} + +/* +Registers a callback to be run when asan detects a memory error right before terminating +the process. + +This can be used for logging and/or debugging purposes. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_set_death_callback :: proc "contextless" (callback: Address_Death_Callback) { + when ASAN_ENABLED { + __sanitizer_set_death_callback(callback) + } +} + +/* +Checks if the memory region covered by the slice is poisoned. + +If it is poisoned this procedure returns the address which would result +in an asan error. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_region_is_poisoned_slice :: proc "contextless" (region: $T/[]$E) -> rawptr { + when ASAN_ENABLED { + return __asan_region_is_poisoned(raw_data(region), size_of(E) * len(region)) + } else { + return nil + } +} + +/* +Checks if the memory region pointed to by the pointer is poisoned. + +If it is poisoned this procedure returns the address which would result +in an asan error. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_region_is_poisoned_ptr :: proc "contextless" (ptr: ^$T) -> rawptr { + when ASAN_ENABLED { + return __asan_region_is_poisoned(ptr, size_of(T)) + } else { + return nil + } +} + +/* +Checks if the memory region covered by `[ptr, ptr+len)` is poisoned. + +If it is poisoned this procedure returns the address which would result +in an asan error. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_region_is_poisoned_rawptr :: proc "contextless" (region: rawptr, len: int) -> rawptr { + when ASAN_ENABLED { + assert_contextless(len >= 0) + return __asan_region_is_poisoned(region, uint(len)) + } else { + return nil + } +} + +/* +Checks if the memory region covered by `[ptr, ptr+len)` is poisoned. + +If it is poisoned this procedure returns the address which would result +in an asan error. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_region_is_poisoned_rawptr_uint :: proc "contextless" (region: rawptr, len: uint) -> rawptr { + when ASAN_ENABLED { + return __asan_region_is_poisoned(region, len) + } else { + return nil + } +} + + +address_region_is_poisoned :: proc { + address_region_is_poisoned_slice, + address_region_is_poisoned_ptr, + address_region_is_poisoned_rawptr, + address_region_is_poisoned_rawptr_uint, +} + +/* +Checks if the address is poisoned. + +If it is poisoned this procedure returns `true`, otherwise it returns +`false`. + +When asan is not enabled this procedure returns `false`. +*/ +@(no_sanitize_address) +address_is_poisoned :: proc "contextless" (address: rawptr) -> bool { + when ASAN_ENABLED { + return __asan_address_is_poisoned(address) != 0 + } else { + return false + } +} + +/* +Describes the sanitizer state for an address. + +This procedure prints the description out to `stdout`. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_describe_address :: proc "contextless" (address: rawptr) { + when ASAN_ENABLED { + __asan_describe_address(address) + } +} + +/* +Returns `true` if an asan error has occured, otherwise it returns +`false`. + +When asan is not enabled this procedure returns `false`. +*/ +@(no_sanitize_address) +address_report_present :: proc "contextless" () -> bool { + when ASAN_ENABLED { + return __asan_report_present() != 0 + } else { + return false + } +} + +/* +Returns the program counter register value of an asan error. + +If no asan error has occurd `nil` is returned. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_get_report_pc :: proc "contextless" () -> rawptr { + when ASAN_ENABLED { + return __asan_get_report_pc() + } else { + return nil + } +} + +/* +Returns the base pointer register value of an asan error. + +If no asan error has occurd `nil` is returned. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_get_report_bp :: proc "contextless" () -> rawptr { + when ASAN_ENABLED { + return __asan_get_report_bp() + } else { + return nil + } +} + +/* +Returns the stack pointer register value of an asan error. + +If no asan error has occurd `nil` is returned. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_get_report_sp :: proc "contextless" () -> rawptr { + when ASAN_ENABLED { + return __asan_get_report_sp() + } else { + return nil + } +} + +/* +Returns the report buffer address of an asan error. + +If no asan error has occurd `nil` is returned. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_get_report_address :: proc "contextless" () -> rawptr { + when ASAN_ENABLED { + return __asan_get_report_address() + } else { + return nil + } +} + +/* +Returns the address access type of an asan error. + +If no asan error has occurd `.none` is returned. + +When asan is not enabled this procedure returns `.none`. +*/ +@(no_sanitize_address) +address_get_report_access_type :: proc "contextless" () -> Address_Access_Type { + when ASAN_ENABLED { + if ! address_report_present() { + return .none + } + return __asan_get_report_access_type() == 0 ? .read : .write + } else { + return .none + } +} + +/* +Returns the access size of an asan error. + +If no asan error has occurd `0` is returned. + +When asan is not enabled this procedure returns `0`. +*/ +@(no_sanitize_address) +address_get_report_access_size :: proc "contextless" () -> uint { + when ASAN_ENABLED { + return __asan_get_report_access_size() + } else { + return 0 + } +} + +/* +Returns the bug description of an asan error. + +If no asan error has occurd an empty string is returned. + +When asan is not enabled this procedure returns an empty string. +*/ +@(no_sanitize_address) +address_get_report_description :: proc "contextless" () -> string { + when ASAN_ENABLED { + return string(__asan_get_report_description()) + } else { + return "" + } +} + +/* +Returns asan information about the address provided, writing the category into `data`. + +The information provided include: +* The category of the address, i.e. stack, global, heap, etc. +* The name of the variable this address belongs to +* The memory region of the address + +When asan is not enabled this procedure returns zero initialised values. +*/ +@(no_sanitize_address) +address_locate_address :: proc "contextless" (addr: rawptr, data: []byte) -> Address_Located_Address { + when ASAN_ENABLED { + out_addr: rawptr + out_size: uint + str := __asan_locate_address(addr, raw_data(data), len(data), &out_addr, &out_size) + return { string(str), string(cstring(raw_data(data))), (cast([^]byte)out_addr)[:out_size] }, + } else { + return { "", "", {} } + } +} + +/* +Returns the allocation stack trace and thread id for a heap address. + +The stack trace is filled into the `data` slice. + +When asan is not enabled this procedure returns a zero initialised value. +*/ +@(no_sanitize_address) +address_get_alloc_stack_trace :: proc "contextless" (addr: rawptr, data: []rawptr) -> ([]rawptr, int) { + when ASAN_ENABLED { + out_thread: i32 + __asan_get_alloc_stack(addr, raw_data(data), len(data), &out_thread) + return data, int(out_thread) + } else { + return {}, 0 + } +} + +/* +Returns the free stack trace and thread id for a heap address. + +The stack trace is filled into the `data` slice. + +When asan is not enabled this procedure returns zero initialised values. +*/ +@(no_sanitize_address) +address_get_free_stack_trace :: proc "contextless" (addr: rawptr, data: []rawptr) -> ([]rawptr, int) { + when ASAN_ENABLED { + out_thread: i32 + __asan_get_free_stack(addr, raw_data(data), len(data), &out_thread) + return data, int(out_thread) + } else { + return {}, 0 + } +} + +/* +Returns the current asan shadow memory mapping. + +When asan is not enabled this procedure returns a zero initialised value. +*/ +@(no_sanitize_address) +address_get_shadow_mapping :: proc "contextless" () -> Address_Shadow_Mapping { + when ASAN_ENABLED { + result: Address_Shadow_Mapping + __asan_get_shadow_mapping(&result.scale, &result.offset) + return result + } else { + return {} + } +} + +/* +Prints asan statistics to `stderr` + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_print_accumulated_stats :: proc "contextless" () { + when ASAN_ENABLED { + __asan_print_accumulated_stats() + } +} + +/* +Returns the address of the current fake stack used by asan. + +This pointer can be then used for `address_is_in_fake_stack`. + +When asan is not enabled this procedure returns `nil`. +*/ +@(no_sanitize_address) +address_get_current_fake_stack :: proc "contextless" () -> rawptr { + when ASAN_ENABLED { + return __asan_get_current_fake_stack() + } else { + return nil + } +} + +/* +Returns if an address belongs to a given fake stack and if so the region of the fake frame. + +When asan is not enabled this procedure returns zero initialised values. +*/ +@(no_sanitize_address) +address_is_in_fake_stack :: proc "contextless" (fake_stack: rawptr, addr: rawptr) -> ([]byte, bool) { + when ASAN_ENABLED { + begin: rawptr + end: rawptr + if __asan_addr_is_in_fake_stack(fake_stack, addr, &begin, &end) == nil { + return {}, false + } + return ((cast([^]byte)begin)[:uintptr(end)-uintptr(begin)]), true + } else { + return {}, false + } +} + +/* +Performs shadow memory cleanup for the current thread before a procedure with no return is called +i.e. a procedure such as `panic` and `os.exit`. + +When asan is not enabled this procedure does nothing. +*/ +@(no_sanitize_address) +address_handle_no_return :: proc "contextless" () { + when ASAN_ENABLED { + __asan_handle_no_return() + } +} + +/* +Updates the allocation stack trace for the given address. + +Returns `true` if successful, otherwise it returns `false`. + +When asan is not enabled this procedure returns `false`. +*/ +@(no_sanitize_address) +address_update_allocation_context :: proc "contextless" (addr: rawptr) -> bool { + when ASAN_ENABLED { + return __asan_update_allocation_context(addr) != 0 + } else { + return false + } +} + diff --git a/base/sanitizer/doc.odin b/base/sanitizer/doc.odin new file mode 100644 index 000000000..707f41ce0 --- /dev/null +++ b/base/sanitizer/doc.odin @@ -0,0 +1,38 @@ +/* +The `sanitizer` package implements various procedures for interacting with sanitizers +from user code. + +An odin project can be linked with various sanitizers to help identify various different +bugs. These sanitizers are: + +## Address + +Enabled with `-sanitize:address` when building an odin project. + +The address sanitizer (asan) is a runtime memory error detector used to help find common memory +related bugs. Typically asan interacts with libc but Odin code can be marked up to interact +with the asan runtime to extend the memory error detection outside of libc using this package. +For more information about asan see: https://clang.llvm.org/docs/AddressSanitizer.html + +Procedures can be made exempt from asan when marked up with @(no_sanitize_address) + +## Memory + +Enabled with `-sanitize:memory` when building an odin project. + +The memory sanitizer is another runtime memory error detector with the sole purpose to catch the +use of uninitialized memory. This is not a very common bug in Odin as by default everything is +set to zero when initialised (ZII). +For more information about the memory sanitizer see: https://clang.llvm.org/docs/MemorySanitizer.html + +## Thread + +Enabled with `-sanitize:thread` when building an odin project. + +The thread sanitizer is a runtime data race detector. It can be used to detect if multiple threads +are concurrently writing and accessing a memory location without proper syncronisation. +For more information about the thread sanitizer see: https://clang.llvm.org/docs/ThreadSanitizer.html + +*/ +package sanitizer + diff --git a/bin/llvm/windows/clang_rt.asan-x86_64.lib b/bin/llvm/windows/clang_rt.asan-x86_64.lib index d7dfcbb1c..0b209dc8d 100644 Binary files a/bin/llvm/windows/clang_rt.asan-x86_64.lib and b/bin/llvm/windows/clang_rt.asan-x86_64.lib differ diff --git a/build.bat b/build.bat index 4c015e133..b1ff9b173 100644 --- a/build.bat +++ b/build.bat @@ -19,16 +19,27 @@ if "%VSCMD_ARG_TGT_ARCH%" neq "x64" ( ) ) +where /Q git.exe || goto skip_git_hash +if not exist .git\ goto skip_git_hash +for /f "tokens=1,2" %%i IN ('git show "--pretty=%%cd %%h" "--date=format:%%Y-%%m-%%d" --no-patch --no-notes HEAD') do ( + set CURR_DATE_TIME=%%i + set GIT_SHA=%%j +) +if %ERRORLEVEL% equ 0 ( + goto have_git_hash_and_date +) +:skip_git_hash pushd misc cl /nologo get-date.c -popd - -for /f %%i in ('misc\get-date') do ( +for /f %%i in ('get-date') do ( set CURR_DATE_TIME=%%i + rem Don't set GIT_SHA ) +popd +:have_git_hash_and_date set curr_year=%CURR_DATE_TIME:~0,4% -set curr_month=%CURR_DATE_TIME:~4,2% -set curr_day=%CURR_DATE_TIME:~6,2% +set curr_month=%CURR_DATE_TIME:~5,2% +set curr_day=%CURR_DATE_TIME:~8,2% :: Make sure this is a decent name and not generic set exe_name=odin.exe @@ -61,31 +72,14 @@ if %release_mode% equ 0 ( set V4=0 set odin_version_full="%V1%.%V2%.%V3%.%V4%" set odin_version_raw="dev-%V1%-%V2%" - set compiler_flags= -nologo -Oi -TP -fp:precise -Gm- -MP -FC -EHsc- -GR- -GF rem Parse source code as utf-8 even on shift-jis and other codepages rem See https://learn.microsoft.com/en-us/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8?view=msvc-170 set compiler_flags= %compiler_flags% /utf-8 -set compiler_defines= -DODIN_VERSION_RAW=\"%odin_version_raw%\" +set compiler_defines= -DODIN_VERSION_RAW=\"%odin_version_raw%\" -DGIT_SHA=\"%GIT_SHA%\" rem fileversion is defined as {Major,Minor,Build,Private: u16} so a bit limited -set rc_flags=-nologo ^ --DV1=%V1% -DV2=%V2% -DV3=%V3% -DV4=%V4% ^ --DVF=%odin_version_full% -DNIGHTLY=%nightly% - -where /Q git.exe || goto skip_git_hash -if not exist .git\ goto skip_git_hash -for /f "tokens=1,2" %%i IN ('git show "--pretty=%%cd %%h" "--date=format:%%Y-%%m" --no-patch --no-notes HEAD') do ( - set odin_version_raw=dev-%%i - set GIT_SHA=%%j -) -if %ERRORLEVEL% equ 0 ( - set compiler_defines=%compiler_defines% -DGIT_SHA=\"%GIT_SHA%\" - set rc_flags=%rc_flags% -DGIT_SHA=%GIT_SHA% -DVP=%odin_version_raw%:%GIT_SHA% -) else ( - set rc_flags=%rc_flags% -DVP=%odin_version_raw% -) -:skip_git_hash +set rc_flags="-DGIT_SHA=%GIT_SHA% -DVP=dev-%V1%-%V2%:%GIT_SHA% nologo -DV1=%V1% -DV2=%V2% -DV3=%V3% -DV4=%V4% -DVF=%odin_version_full% -DNIGHTLY=%nightly%" if %nightly% equ 1 set compiler_defines=%compiler_defines% -DNIGHTLY @@ -138,6 +132,7 @@ del *.ilk > NUL 2> NUL rc %rc_flags% %odin_rc% cl %compiler_settings% "src\main.cpp" "src\libtommath.cpp" /link %linker_settings% -OUT:%exe_name% +if %errorlevel% neq 0 goto end_of_build mt -nologo -inputresource:%exe_name%;#1 -manifest misc\odin.manifest -outputresource:%exe_name%;#1 -validate_manifest -identity:"odin, processorArchitecture=amd64, version=%odin_version_full%, type=win32" if %errorlevel% neq 0 goto end_of_build @@ -152,4 +147,4 @@ if %release_mode% EQU 0 echo: & echo Debug compiler built. Note: run "build.bat del *.obj > NUL 2> NUL -:end_of_build +:end_of_build \ No newline at end of file diff --git a/build_odin.sh b/build_odin.sh index 19bb82a11..0d7e8a26e 100755 --- a/build_odin.sh +++ b/build_odin.sh @@ -6,7 +6,6 @@ set -eu : ${LDFLAGS=} : ${LLVM_CONFIG=} -CPPFLAGS="$CPPFLAGS -DODIN_VERSION_RAW=\"dev-$(date +"%Y-%m")\"" CXXFLAGS="$CXXFLAGS -std=c++14" DISABLED_WARNINGS="-Wno-switch -Wno-macro-redefined -Wno-unused-value" LDFLAGS="$LDFLAGS -pthread -lm" @@ -15,8 +14,12 @@ OS_NAME="$(uname -s)" if [ -d ".git" ] && [ -n "$(command -v git)" ]; then GIT_SHA=$(git show --pretty='%h' --no-patch --no-notes HEAD) + GIT_DATE=$(git show "--pretty=%cd" "--date=format:%Y-%m" --no-patch --no-notes HEAD) CPPFLAGS="$CPPFLAGS -DGIT_SHA=\"$GIT_SHA\"" +else + GIT_DATE=$(date +"%Y-%m") fi +CPPFLAGS="$CPPFLAGS -DODIN_VERSION_RAW=\"dev-$GIT_DATE\"" error() { printf "ERROR: %s\n" "$1" diff --git a/core/bufio/reader.odin b/core/bufio/reader.odin index a875c732d..b78cac6e1 100644 --- a/core/bufio/reader.odin +++ b/core/bufio/reader.odin @@ -257,7 +257,7 @@ reader_read_rune :: proc(b: ^Reader) -> (r: rune, size: int, err: io.Error) { for b.r+utf8.UTF_MAX > b.w && !utf8.full_rune(b.buf[b.r:b.w]) && b.err == nil && - b.w-b.w < len(b.buf) { + b.w-b.r < len(b.buf) { _reader_read_new_chunk(b) or_return } diff --git a/core/container/intrusive/list/intrusive_list.odin b/core/container/intrusive/list/intrusive_list.odin index 5b29efb22..1e116ef18 100644 --- a/core/container/intrusive/list/intrusive_list.odin +++ b/core/container/intrusive/list/intrusive_list.odin @@ -278,19 +278,19 @@ Example: iterate_next_example :: proc() { l: list.List - one := My_Struct{value=1} - two := My_Struct{value=2} + one := My_Next_Struct{value=1} + two := My_Next_Struct{value=2} list.push_back(&l, &one.node) list.push_back(&l, &two.node) - it := list.iterator_head(l, My_Struct, "node") + it := list.iterator_head(l, My_Next_Struct, "node") for num in list.iterate_next(&it) { fmt.println(num.value) } } - My_Struct :: struct { + My_Next_Struct :: struct { node : list.Node, value: int, } @@ -325,22 +325,22 @@ Example: import "core:fmt" import "core:container/intrusive/list" - iterate_next_example :: proc() { + iterate_prev_example :: proc() { l: list.List - one := My_Struct{value=1} - two := My_Struct{value=2} + one := My_Prev_Struct{value=1} + two := My_Prev_Struct{value=2} list.push_back(&l, &one.node) list.push_back(&l, &two.node) - it := list.iterator_tail(l, My_Struct, "node") + it := list.iterator_tail(l, My_Prev_Struct, "node") for num in list.iterate_prev(&it) { fmt.println(num.value) } } - My_Struct :: struct { + My_Prev_Struct :: struct { node : list.Node, value: int, } diff --git a/core/container/lru/lru_cache.odin b/core/container/lru/lru_cache.odin index f8aa55dc2..d95e8e1d2 100644 --- a/core/container/lru/lru_cache.odin +++ b/core/container/lru/lru_cache.odin @@ -129,7 +129,7 @@ remove :: proc(c: ^$C/Cache($Key, $Value), key: Key) -> bool { return false } _remove_node(c, e) - free(node, c.node_allocator) + free(e, c.node_allocator) c.count -= 1 return true } diff --git a/core/container/priority_queue/priority_queue.odin b/core/container/priority_queue/priority_queue.odin index 8a6d77288..c62a821f4 100644 --- a/core/container/priority_queue/priority_queue.odin +++ b/core/container/priority_queue/priority_queue.odin @@ -1,6 +1,7 @@ package container_priority_queue import "base:builtin" +import "base:runtime" Priority_Queue :: struct($T: typeid) { queue: [dynamic]T, @@ -17,13 +18,14 @@ default_swap_proc :: proc($T: typeid) -> proc(q: []T, i, j: int) { } } -init :: proc(pq: ^$Q/Priority_Queue($T), less: proc(a, b: T) -> bool, swap: proc(q: []T, i, j: int), capacity := DEFAULT_CAPACITY, allocator := context.allocator) { +init :: proc(pq: ^$Q/Priority_Queue($T), less: proc(a, b: T) -> bool, swap: proc(q: []T, i, j: int), capacity := DEFAULT_CAPACITY, allocator := context.allocator) -> (err: runtime.Allocator_Error) { if pq.queue.allocator.procedure == nil { pq.queue.allocator = allocator } - reserve(pq, capacity) + reserve(pq, capacity) or_return pq.less = less pq.swap = swap + return .None } init_from_dynamic_array :: proc(pq: ^$Q/Priority_Queue($T), queue: [dynamic]T, less: proc(a, b: T) -> bool, swap: proc(q: []T, i, j: int)) { @@ -41,8 +43,8 @@ destroy :: proc(pq: ^$Q/Priority_Queue($T)) { delete(pq.queue) } -reserve :: proc(pq: ^$Q/Priority_Queue($T), capacity: int) { - builtin.reserve(&pq.queue, capacity) +reserve :: proc(pq: ^$Q/Priority_Queue($T), capacity: int) -> (err: runtime.Allocator_Error) { + return builtin.reserve(&pq.queue, capacity) } clear :: proc(pq: ^$Q/Priority_Queue($T)) { builtin.clear(&pq.queue) @@ -103,9 +105,10 @@ fix :: proc(pq: ^$Q/Priority_Queue($T), i: int) { } } -push :: proc(pq: ^$Q/Priority_Queue($T), value: T) { - append(&pq.queue, value) +push :: proc(pq: ^$Q/Priority_Queue($T), value: T) -> (err: runtime.Allocator_Error) { + append(&pq.queue, value) or_return _shift_up(pq, builtin.len(pq.queue)-1) + return .None } pop :: proc(pq: ^$Q/Priority_Queue($T), loc := #caller_location) -> (value: T) { @@ -130,12 +133,10 @@ pop_safe :: proc(pq: ^$Q/Priority_Queue($T), loc := #caller_location) -> (value: remove :: proc(pq: ^$Q/Priority_Queue($T), i: int) -> (value: T, ok: bool) { n := builtin.len(pq.queue) if 0 <= i && i < n { - if n != i { - pq.swap(pq.queue[:], i, n) - _shift_down(pq, i, n) - _shift_up(pq, i) - } - value, ok = builtin.pop_safe(&pq.queue) + pq.swap(pq.queue[:], i, n-1) + _shift_down(pq, i, n-1) + _shift_up(pq, i) + value, ok = builtin.pop(&pq.queue), true } return } diff --git a/core/container/small_array/doc.odin b/core/container/small_array/doc.odin new file mode 100644 index 000000000..f3e9acd57 --- /dev/null +++ b/core/container/small_array/doc.odin @@ -0,0 +1,55 @@ +/* +Package small_array implements a dynamic array like +interface on a stack-allocated, fixed-size array. + +The Small_Array type is optimal for scenarios where you need +a container for a fixed number of elements of a specific type, +with the total number known at compile time but the exact +number to be used determined at runtime. + +Example: + import "core:fmt" + import "core:container/small_array" + + create :: proc() -> (result: small_array.Small_Array(10, rune)) { + // appending single elements + small_array.push(&result, 'e') + // pushing a bunch of elements at once + small_array.push(&result, 'l', 'i', 'x', '-', 'e') + // pre-pending + small_array.push_front(&result, 'H') + // removing elements + small_array.ordered_remove(&result, 4) + // resizing to the desired length (the capacity will stay unchanged) + small_array.resize(&result, 7) + // inserting elements + small_array.inject_at(&result, 'p', 5) + // updating elements + small_array.set(&result, 3, 'l') + // getting pointers to elements + o := small_array.get_ptr(&result, 4) + o^ = 'o' + // and much more .... + return + } + + // the Small_Array can be an ordinary parameter 'generic' over + // the actual length to be usable with different sizes + print_elements :: proc(arr: ^small_array.Small_Array($N, rune)) { + for r in small_array.slice(arr) { + fmt.print(r) + } + } + + main :: proc() { + arr := create() + // ... + print_elements(&arr) + } + +Output: + + Hellope + +*/ +package container_small_array diff --git a/core/container/small_array/small_array.odin b/core/container/small_array/small_array.odin index 77bb21cbc..49d441079 100644 --- a/core/container/small_array/small_array.odin +++ b/core/container/small_array/small_array.odin @@ -4,36 +4,171 @@ import "base:builtin" import "base:runtime" _ :: runtime +/* +A fixed-size stack-allocated array operated on in a dynamic fashion. + +Fields: +- `data`: The underlying array +- `len`: Amount of items that the `Small_Array` currently holds + +Example: + + import "core:container/small_array" + + example :: proc() { + a: small_array.Small_Array(100, int) + small_array.push_back(&a, 10) + } +*/ Small_Array :: struct($N: int, $T: typeid) where N >= 0 { data: [N]T, len: int, } +/* +Returns the amount of items in the small-array. +**Inputs** +- `a`: The small-array + +**Returns** +- the amount of items in the array +*/ len :: proc "contextless" (a: $A/Small_Array) -> int { return a.len } +/* +Returns the capacity of the small-array. + +**Inputs** +- `a`: The small-array + +**Returns** the capacity +*/ cap :: proc "contextless" (a: $A/Small_Array) -> int { return builtin.len(a.data) } +/* +Returns how many more items the small-array could fit. + +**Inputs** +- `a`: The small-array + +**Returns** +- the number of unused slots +*/ space :: proc "contextless" (a: $A/Small_Array) -> int { return builtin.len(a.data) - a.len } +/* +Returns a slice of the data. + +**Inputs** +- `a`: The pointer to the small-array + +**Returns** +- the slice + +Example: + + import "core:container/small_array" + import "core:fmt" + + slice_example :: proc() { + print :: proc(a: ^small_array.Small_Array($N, int)) { + for item in small_array.slice(a) { + fmt.println(item) + } + } + + a: small_array.Small_Array(5, int) + small_array.push_back(&a, 1) + small_array.push_back(&a, 2) + print(&a) + } + +Output: + + 1 + 2 +*/ slice :: proc "contextless" (a: ^$A/Small_Array($N, $T)) -> []T { return a.data[:a.len] } +/* +Get a copy of the item at the specified position. +This operation assumes that the small-array is large enough. +This will result in: + - the value if 0 <= index < len + - the zero value of the type if len < index < capacity + - 'crash' if capacity < index or index < 0 + +**Inputs** +- `a`: The small-array +- `index`: The position of the item to get + +**Returns** +- the element at the specified position +*/ get :: proc "contextless" (a: $A/Small_Array($N, $T), index: int) -> T { return a.data[index] } + +/* +Get a pointer to the item at the specified position. +This operation assumes that the small-array is large enough. + +This will result in: + - the pointer if 0 <= index < len + - the pointer to the zero value if len < index < capacity + - 'crash' if capacity < index or index < 0 + +**Inputs** +- `a`: A pointer to the small-array +- `index`: The position of the item to get + +**Returns** +- the pointer to the element at the specified position +*/ get_ptr :: proc "contextless" (a: ^$A/Small_Array($N, $T), index: int) -> ^T { return &a.data[index] } +/* +Attempt to get a copy of the item at the specified position. + +**Inputs** +- `a`: The small-array +- `index`: The position of the item to get + +**Returns** +- the element at the specified position +- true if element exists, false otherwise + +Example: + + import "core:container/small_array" + import "core:fmt" + + get_safe_example :: proc() { + a: small_array.Small_Array(5, rune) + small_array.push_back(&a, 'A') + + fmt.println(small_array.get_safe(a, 0) or_else 'x') + fmt.println(small_array.get_safe(a, 1) or_else 'x') + } + +Output: + + A + x + +*/ get_safe :: proc(a: $A/Small_Array($N, $T), index: int) -> (T, bool) #no_bounds_check { if index < 0 || index >= a.len { return {}, false @@ -41,6 +176,17 @@ get_safe :: proc(a: $A/Small_Array($N, $T), index: int) -> (T, bool) #no_bounds_ return a.data[index], true } +/* +Get a pointer to the item at the specified position. + +**Inputs** +- `a`: A pointer to the small-array +- `index`: The position of the item to get + +**Returns** +- the pointer to the element at the specified position +- true if element exists, false otherwise +*/ get_ptr_safe :: proc(a: ^$A/Small_Array($N, $T), index: int) -> (^T, bool) #no_bounds_check { if index < 0 || index >= a.len { return {}, false @@ -48,15 +194,128 @@ get_ptr_safe :: proc(a: ^$A/Small_Array($N, $T), index: int) -> (^T, bool) #no_b return &a.data[index], true } +/* +Set the element at the specified position to the given value. +This operation assumes that the small-array is large enough. + +This will result in: + - the value being set if 0 <= index < capacity + - 'crash' otherwise + +**Inputs** +- `a`: A pointer to the small-array +- `index`: The position of the item to set +- `value`: The value to set the element to + +Example: + + import "core:container/small_array" + import "core:fmt" + + set_example :: proc() { + a: small_array.Small_Array(5, rune) + small_array.push_back(&a, 'A') + small_array.push_back(&a, 'B') + fmt.println(small_array.slice(&a)) + + // updates index 0 + small_array.set(&a, 0, 'Z') + fmt.println(small_array.slice(&a)) + + // updates to a position x, where + // len <= x < cap are not visible since + // the length of the small-array remains unchanged + small_array.set(&a, 2, 'X') + small_array.set(&a, 3, 'Y') + small_array.set(&a, 4, 'Z') + fmt.println(small_array.slice(&a)) + + // resizing makes the change visible + small_array.resize(&a, 100) + fmt.println(small_array.slice(&a)) + } + +Output: + + [A, B] + [Z, B] + [Z, B] + [Z, B, X, Y, Z] + +*/ set :: proc "contextless" (a: ^$A/Small_Array($N, $T), index: int, item: T) { a.data[index] = item } +/* +Tries to resize the small-array to the specified length. + +The new length will be: + - `length` if `length` <= capacity + - capacity if length > capacity + +**Inputs** +- `a`: A pointer to the small-array +- `length`: The new desired length + +Example: + + import "core:container/small_array" + import "core:fmt" + + resize_example :: proc() { + a: small_array.Small_Array(5, int) + + small_array.push_back(&a, 1) + small_array.push_back(&a, 2) + fmt.println(small_array.slice(&a)) + + small_array.resize(&a, 1) + fmt.println(small_array.slice(&a)) + + small_array.resize(&a, 100) + fmt.println(small_array.slice(&a)) + } + +Output: + + [1, 2] + [1] + [1, 2, 0, 0, 0] +*/ resize :: proc "contextless" (a: ^$A/Small_Array, length: int) { a.len = min(length, builtin.len(a.data)) } +/* +Attempts to add the given element to the end. +**Inputs** +- `a`: A pointer to the small-array +- `item`: The item to append + +**Returns** +- true if there was enough space to fit the element, false otherwise + +Example: + + import "core:container/small_array" + import "core:fmt" + + push_back_example :: proc() { + a: small_array.Small_Array(2, int) + + assert(small_array.push_back(&a, 1), "this should fit") + assert(small_array.push_back(&a, 2), "this should fit") + assert(!small_array.push_back(&a, 3), "this should not fit") + + fmt.println(small_array.slice(&a)) + } + +Output: + + [1, 2] +*/ push_back :: proc "contextless" (a: ^$A/Small_Array($N, $T), item: T) -> bool { if a.len < cap(a^) { a.data[a.len] = item @@ -66,6 +325,39 @@ push_back :: proc "contextless" (a: ^$A/Small_Array($N, $T), item: T) -> bool { return false } +/* +Attempts to add the given element at the beginning. +This operation assumes that the small-array is not empty. + +Note: Performing this operation will cause pointers obtained +through get_ptr(_save) to reference incorrect elements. + +**Inputs** +- `a`: A pointer to the small-array +- `item`: The item to append + +**Returns** +- true if there was enough space to fit the element, false otherwise + +Example: + + import "core:container/small_array" + import "core:fmt" + + push_front_example :: proc() { + a: small_array.Small_Array(2, int) + + assert(small_array.push_front(&a, 2), "this should fit") + assert(small_array.push_front(&a, 1), "this should fit") + assert(!small_array.push_back(&a, 0), "this should not fit") + + fmt.println(small_array.slice(&a)) + } + +Output: + + [1, 2] +*/ push_front :: proc "contextless" (a: ^$A/Small_Array($N, $T), item: T) -> bool { if a.len < cap(a^) { a.len += 1 @@ -77,6 +369,35 @@ push_front :: proc "contextless" (a: ^$A/Small_Array($N, $T), item: T) -> bool { return false } +/* +Removes and returns the last element of the small-array. +This operation assumes that the small-array is not empty. + +**Inputs** +- `a`: A pointer to the small-array + +**Returns** +- a copy of the element removed from the end of the small-array + +Example: + + import "core:container/small_array" + import "core:fmt" + + pop_back_example :: proc() { + a: small_array.Small_Array(5, int) + small_array.push(&a, 0, 1, 2) + + fmt.println("BEFORE:", small_array.slice(&a)) + small_array.pop_back(&a) + fmt.println("AFTER: ", small_array.slice(&a)) + } + +Output: + + BEFORE: [0, 1, 2] + AFTER: [0, 1] +*/ pop_back :: proc "odin" (a: ^$A/Small_Array($N, $T), loc := #caller_location) -> T { assert(condition=(N > 0 && a.len > 0), loc=loc) item := a.data[a.len-1] @@ -84,6 +405,38 @@ pop_back :: proc "odin" (a: ^$A/Small_Array($N, $T), loc := #caller_location) -> return item } +/* +Removes and returns the first element of the small-array. +This operation assumes that the small-array is not empty. + +Note: Performing this operation will cause pointers obtained +through get_ptr(_save) to reference incorrect elements. + +**Inputs** +- `a`: A pointer to the small-array + +**Returns** +- a copy of the element removed from the beginning of the small-array + +Example: + + import "core:container/small_array" + import "core:fmt" + + pop_front_example :: proc() { + a: small_array.Small_Array(5, int) + small_array.push(&a, 0, 1, 2) + + fmt.println("BEFORE:", small_array.slice(&a)) + small_array.pop_front(&a) + fmt.println("AFTER: ", small_array.slice(&a)) + } + +Output: + + BEFORE: [0, 1, 2] + AFTER: [1, 2] +*/ pop_front :: proc "odin" (a: ^$A/Small_Array($N, $T), loc := #caller_location) -> T { assert(condition=(N > 0 && a.len > 0), loc=loc) item := a.data[0] @@ -93,6 +446,32 @@ pop_front :: proc "odin" (a: ^$A/Small_Array($N, $T), loc := #caller_location) - return item } +/* +Attempts to remove and return the last element of the small array. +Unlike `pop_back`, it does not assume that the array is non-empty. + +**Inputs** +- `a`: A pointer to the small-array + +**Returns** +- a copy of the element removed from the end of the small-array +- true if the small-array was not empty, false otherwise + +Example: + + import "core:container/small_array" + + pop_back_safe_example :: proc() { + a: small_array.Small_Array(3, int) + small_array.push(&a, 1) + + el, ok := small_array.pop_back_safe(&a) + assert(ok, "there was an element in the array") + + el, ok = small_array.pop_back_safe(&a) + assert(!ok, "there was NO element in the array") + } +*/ pop_back_safe :: proc "contextless" (a: ^$A/Small_Array($N, $T)) -> (item: T, ok: bool) { if N > 0 && a.len > 0 { item = a.data[a.len-1] @@ -102,6 +481,35 @@ pop_back_safe :: proc "contextless" (a: ^$A/Small_Array($N, $T)) -> (item: T, ok return } +/* +Attempts to remove and return the first element of the small array. +Unlike `pop_front`, it does not assume that the array is non-empty. + +Note: Performing this operation will cause pointers obtained +through get_ptr(_save) to reference incorrect elements. + +**Inputs** +- `a`: A pointer to the small-array + +**Returns** +- a copy of the element removed from the beginning of the small-array +- true if the small-array was not empty, false otherwise + +Example: + + import "core:container/small_array" + + pop_front_safe_example :: proc() { + a: small_array.Small_Array(3, int) + small_array.push(&a, 1) + + el, ok := small_array.pop_front_safe(&a) + assert(ok, "there was an element in the array") + + el, ok = small_array.pop_front_(&a) + assert(!ok, "there was NO element in the array") + } +*/ pop_front_safe :: proc "contextless" (a: ^$A/Small_Array($N, $T)) -> (item: T, ok: bool) { if N > 0 && a.len > 0 { item = a.data[0] @@ -113,11 +521,70 @@ pop_front_safe :: proc "contextless" (a: ^$A/Small_Array($N, $T)) -> (item: T, o return } +/* +Decreases the length of the small-array by the given amount. +The elements are therefore not really removed and can be +recovered by calling `resize`. + +Note: This procedure assumes that the array has a sufficient length. + +**Inputs** +- `a`: A pointer to the small-array +- `count`: The amount the length should be reduced by + +Example: + + import "core:container/small_array" + import "core:fmt" + + consume_example :: proc() { + a: small_array.Small_Array(3, int) + small_array.push(&a, 0, 1, 2) + + fmt.println("BEFORE:", small_array.slice(&a)) + small_array.consume(&a, 2) + fmt.println("AFTER :", small_array.slice(&a)) + } + +Output: + + BEFORE: [0, 1, 2] + AFTER : [0] +*/ consume :: proc "odin" (a: ^$A/Small_Array($N, $T), count: int, loc := #caller_location) { assert(condition=a.len >= count, loc=loc) a.len -= count } +/* +Removes the element at the specified index while retaining order. + +Note: Performing this operation will cause pointers obtained +through get_ptr(_save) to reference incorrect elements. + +**Inputs** +- `a`: A pointer to the small-array +- `index`: The position of the element to remove + +Example: + + import "core:container/small_array" + import "core:fmt" + + ordered_remove_example :: proc() { + a: small_array.Small_Array(4, int) + small_array.push(&a, 0, 1, 2, 3) + + fmt.println("BEFORE:", small_array.slice(&a)) + small_array.ordered_remove(&a, 1) + fmt.println("AFTER :", small_array.slice(&a)) + } + +Output: + + BEFORE: [0, 1, 2, 3] + AFTER : [0, 2, 3] +*/ ordered_remove :: proc "contextless" (a: ^$A/Small_Array($N, $T), index: int, loc := #caller_location) #no_bounds_check { runtime.bounds_check_error_loc(loc, index, a.len) if index+1 < a.len { @@ -126,6 +593,32 @@ ordered_remove :: proc "contextless" (a: ^$A/Small_Array($N, $T), index: int, lo a.len -= 1 } +/* +Removes the element at the specified index without retaining order. + +**Inputs** +- `a`: A pointer to the small-array +- `index`: The position of the element to remove + +Example: + + import "core:container/small_array" + import "core:fmt" + + unordered_remove_example :: proc() { + a: small_array.Small_Array(4, int) + small_array.push(&a, 0, 1, 2, 3) + + fmt.println("BEFORE:", small_array.slice(&a)) + small_array.unordered_remove(&a, 1) + fmt.println("AFTER :", small_array.slice(&a)) + } + +Output: + + BEFORE: [0, 1, 2, 3] + AFTER : [0, 3, 2] +*/ unordered_remove :: proc "contextless" (a: ^$A/Small_Array($N, $T), index: int, loc := #caller_location) #no_bounds_check { runtime.bounds_check_error_loc(loc, index, a.len) n := a.len-1 @@ -135,10 +628,63 @@ unordered_remove :: proc "contextless" (a: ^$A/Small_Array($N, $T), index: int, a.len -= 1 } +/* +Sets the length of the small-array to 0. + +**Inputs** +- `a`: A pointer to the small-array + +Example: + + import "core:container/small_array" + import "core:fmt" + + clear_example :: proc() { + a: small_array.Small_Array(4, int) + small_array.push(&a, 0, 1, 2, 3) + + fmt.println("BEFORE:", small_array.slice(&a)) + small_array.clear(&a) + fmt.println("AFTER :", small_array.slice(&a)) + } + +Output: + + BEFORE: [0, 1, 2, 3] + AFTER : [] + +*/ clear :: proc "contextless" (a: ^$A/Small_Array($N, $T)) { resize(a, 0) } +/* +Attempts to append all elements to the small-array returning +false if there is not enough space to fit all of them. + +**Inputs** +- `a`: A pointer to the small-array +- `item`: The item to append +- ..: + +**Returns** +- true if there was enough space to fit the element, false otherwise + +Example: + + import "core:container/small_array" + import "core:fmt" + + push_back_elems_example :: proc() { + a: small_array.Small_Array(100, int) + small_array.push_back_elems(&a, 0, 1, 2, 3, 4) + fmt.println(small_array.slice(&a)) + } + +Output: + + [0, 1, 2, 3, 4] +*/ push_back_elems :: proc "contextless" (a: ^$A/Small_Array($N, $T), items: ..T) -> bool { if a.len + builtin.len(items) <= cap(a^) { n := copy(a.data[a.len:], items[:]) @@ -148,6 +694,36 @@ push_back_elems :: proc "contextless" (a: ^$A/Small_Array($N, $T), items: ..T) - return false } +/* +Tries to insert an element at the specified position. + +Note: Performing this operation will cause pointers obtained +through get_ptr(_save) to reference incorrect elements. + +**Inputs** +- `a`: A pointer to the small-array +- `item`: The item to insert +- `index`: The index to insert the item at + +**Returns** +- true if there was enough space to fit the element, false otherwise + +Example: + + import "core:container/small_array" + import "core:fmt" + + inject_at_example :: proc() { + arr: small_array.Small_Array(100, rune) + small_array.push(&arr, 'A', 'C', 'D') + small_array.inject_at(&arr, 'B', 1) + fmt.println(small_array.slice(&arr)) + } + +Output: + + [A, B, C, D] +*/ inject_at :: proc "contextless" (a: ^$A/Small_Array($N, $T), item: T, index: int) -> bool #no_bounds_check { if a.len < cap(a^) && index >= 0 && index <= len(a^) { a.len += 1 @@ -160,7 +736,38 @@ inject_at :: proc "contextless" (a: ^$A/Small_Array($N, $T), item: T, index: int return false } +// Alias for `push_back` append_elem :: push_back +// Alias for `push_back_elems` append_elems :: push_back_elems + +/* +Tries to append the element(s) to the small-array. + +**Inputs** +- `a`: A pointer to the small-array +- `item`: The item to append +- ..: + +**Returns** +- true if there was enough space to fit the element, false otherwise + +Example: + + import "core:container/small_array" + import "core:fmt" + + push_example :: proc() { + a: small_array.Small_Array(100, int) + small_array.push(&a, 0) + small_array.push(&a, 1, 2, 3, 4) + fmt.println(small_array.slice(&a)) + } + +Output: + + [0, 1, 2, 3, 4] +*/ push :: proc{push_back, push_back_elems} +// Alias for `push` append :: proc{push_back, push_back_elems} diff --git a/core/encoding/cbor/unmarshal.odin b/core/encoding/cbor/unmarshal.odin index 4ea5c610e..b6fcfe5b6 100644 --- a/core/encoding/cbor/unmarshal.odin +++ b/core/encoding/cbor/unmarshal.odin @@ -29,6 +29,7 @@ an input. unmarshal :: proc { unmarshal_from_reader, unmarshal_from_string, + unmarshal_from_bytes, } unmarshal_from_reader :: proc(r: io.Reader, ptr: ^$T, flags := Decoder_Flags{}, allocator := context.allocator, temp_allocator := context.temp_allocator, loc := #caller_location) -> (err: Unmarshal_Error) { @@ -51,6 +52,11 @@ unmarshal_from_string :: proc(s: string, ptr: ^$T, flags := Decoder_Flags{}, all return } +// Unmarshals from a slice of bytes, see docs on the proc group `Unmarshal` for more info. +unmarshal_from_bytes :: proc(bytes: []byte, ptr: ^$T, flags := Decoder_Flags{}, allocator := context.allocator, temp_allocator := context.temp_allocator, loc := #caller_location) -> (err: Unmarshal_Error) { + return unmarshal_from_string(string(bytes), ptr, flags, allocator, temp_allocator, loc) +} + unmarshal_from_decoder :: proc(d: Decoder, ptr: ^$T, allocator := context.allocator, temp_allocator := context.temp_allocator, loc := #caller_location) -> (err: Unmarshal_Error) { d := d @@ -487,7 +493,7 @@ _unmarshal_array :: proc(d: Decoder, v: any, ti: ^reflect.Type_Info, hdr: Header data := mem.alloc_bytes_non_zeroed(t.elem.size * scap, t.elem.align, allocator=allocator, loc=loc) or_return defer if err != nil { mem.free_bytes(data, allocator=allocator, loc=loc) } - da := mem.Raw_Dynamic_Array{raw_data(data), 0, length, context.allocator } + da := mem.Raw_Dynamic_Array{raw_data(data), 0, scap, context.allocator } assign_array(d, &da, t.elem, length) or_return diff --git a/core/encoding/entity/entity.odin b/core/encoding/entity/entity.odin index d2f1d46b2..cb8fa8611 100644 --- a/core/encoding/entity/entity.odin +++ b/core/encoding/entity/entity.odin @@ -108,7 +108,7 @@ decode_xml :: proc(input: string, options := XML_Decode_Options{}, allocator := it couldn't have been part of an XML tag body to be decoded here. Keep in mind that we could already *be* inside a CDATA tag. - If so, write `>` as a literal and continue. + If so, write `<` as a literal and continue. */ if in_data { write_rune(&builder, '<') @@ -119,11 +119,9 @@ decode_xml :: proc(input: string, options := XML_Decode_Options{}, allocator := case ']': // If we're unboxing _and_ decoding CDATA, we'll have to check for the end tag. if in_data { - if t.read_offset + len(CDATA_END) < len(t.src) { - if string(t.src[t.offset:][:len(CDATA_END)]) == CDATA_END { - in_data = false - t.read_offset += len(CDATA_END) - 1 - } + if strings.has_prefix(t.src[t.offset:], CDATA_END) { + in_data = false + t.read_offset += len(CDATA_END) - 1 } continue } else { @@ -297,40 +295,40 @@ _handle_xml_special :: proc(t: ^Tokenizer, builder: ^strings.Builder, options: X assert(t != nil && t.r == '<') if t.read_offset + len(CDATA_START) >= len(t.src) { return false, .None } - if string(t.src[t.offset:][:len(CDATA_START)]) == CDATA_START { - t.read_offset += len(CDATA_START) - 1 - + s := string(t.src[t.offset:]) + if strings.has_prefix(s, CDATA_START) { if .Unbox_CDATA in options && .Decode_CDATA in options { // We're unboxing _and_ decoding CDATA + t.read_offset += len(CDATA_START) - 1 return true, .None } - // CDATA is passed through. - offset := t.offset - - // Scan until end of CDATA. + // CDATA is passed through. Scan until end of CDATA. + start_offset := t.offset + t.read_offset += len(CDATA_START) for { - advance(t) or_return - if t.r < 0 { return true, .CDATA_Not_Terminated } + advance(t) + if t.r < 0 { + // error(t, offset, "[scan_string] CDATA was not terminated\n") + return true, .CDATA_Not_Terminated + } - if t.read_offset + len(CDATA_END) < len(t.src) { - if string(t.src[t.offset:][:len(CDATA_END)]) == CDATA_END { - t.read_offset += len(CDATA_END) - 1 + // Scan until the end of a CDATA tag. + if s = string(t.src[t.read_offset:]); strings.has_prefix(s, CDATA_END) { + t.read_offset += len(CDATA_END) + cdata := string(t.src[start_offset:t.read_offset]) - cdata := string(t.src[offset : t.read_offset]) - - if .Unbox_CDATA in options { - cdata = cdata[len(CDATA_START):] - cdata = cdata[:len(cdata) - len(CDATA_END)] - } - - write_string(builder, cdata) - return false, .None + if .Unbox_CDATA in options { + cdata = cdata[len(CDATA_START):] + cdata = cdata[:len(cdata) - len(CDATA_END)] } + write_string(builder, cdata) + return false, .None } } - } else if string(t.src[t.offset:][:len(COMMENT_START)]) == COMMENT_START { + + } else if strings.has_prefix(s, COMMENT_START) { t.read_offset += len(COMMENT_START) // Comment is passed through by default. offset := t.offset diff --git a/core/encoding/hxa/read.odin b/core/encoding/hxa/read.odin index a679946f8..6dde16848 100644 --- a/core/encoding/hxa/read.odin +++ b/core/encoding/hxa/read.odin @@ -79,7 +79,6 @@ read :: proc(data: []byte, filename := "", print_error := false, allocato read_meta :: proc(r: ^Reader, capacity: u32le, allocator := context.allocator, loc := #caller_location) -> (meta_data: []Meta, err: Read_Error) { meta_data = make([]Meta, int(capacity), allocator=allocator) count := 0 - defer meta_data = meta_data[:count] for &m in meta_data { m.name = read_name(r) or_return @@ -105,6 +104,7 @@ read :: proc(data: []byte, filename := "", print_error := false, allocato count += 1 } + meta_data = meta_data[:count] return } @@ -112,7 +112,6 @@ read :: proc(data: []byte, filename := "", print_error := false, allocato stack_count := read_value(r, u32le) or_return layer_count := 0 layers = make(Layer_Stack, stack_count, allocator=allocator, loc=loc) - defer layers = layers[:layer_count] for &layer in layers { layer.name = read_name(r) or_return layer.components = read_value(r, u8) or_return @@ -136,6 +135,7 @@ read :: proc(data: []byte, filename := "", print_error := false, allocato layer_count += 1 } + layers = layers[:layer_count] return } diff --git a/core/encoding/json/tokenizer.odin b/core/encoding/json/tokenizer.odin index e46d879a7..ad928b7d9 100644 --- a/core/encoding/json/tokenizer.odin +++ b/core/encoding/json/tokenizer.odin @@ -101,7 +101,7 @@ get_token :: proc(t: ^Tokenizer) -> (token: Token, err: Error) { } } - scan_espace :: proc(t: ^Tokenizer) -> bool { + scan_escape :: proc(t: ^Tokenizer) -> bool { switch t.r { case '"', '\'', '\\', '/', 'b', 'n', 'r', 't', 'f': next_rune(t) @@ -310,7 +310,7 @@ get_token :: proc(t: ^Tokenizer) -> (token: Token, err: Error) { break } if r == '\\' { - scan_espace(t) + scan_escape(t) } } diff --git a/core/encoding/json/unmarshal.odin b/core/encoding/json/unmarshal.odin index f5862aca4..09f7b4f55 100644 --- a/core/encoding/json/unmarshal.odin +++ b/core/encoding/json/unmarshal.odin @@ -406,6 +406,9 @@ unmarshal_expect_token :: proc(p: ^Parser, kind: Token_Kind, loc := #caller_loca return prev } +// Struct tags can include not only the name of the JSON key, but also a tag such as `omitempty`. +// Example: `json:"key_name,omitempty"` +// This returns the first field as `json_name`, and the rest are returned as `extra`. @(private) json_name_from_tag_value :: proc(value: string) -> (json_name, extra: string) { json_name = value @@ -441,12 +444,6 @@ unmarshal_object :: proc(p: ^Parser, v: any, end_token: Token_Kind) -> (err: Unm defer delete(key, p.allocator) unmarshal_expect_token(p, .Colon) - - field_test :: #force_inline proc "contextless" (field_used: [^]byte, offset: uintptr) -> bool { - prev_set := field_used[offset/8] & byte(offset&7) != 0 - field_used[offset/8] |= byte(offset&7) - return prev_set - } field_used_bytes := (reflect.size_of_typeid(ti.id)+7)/8 field_used := intrinsics.alloca(field_used_bytes + 1, 1) // + 1 to not overflow on size_of 0 types. @@ -465,7 +462,9 @@ unmarshal_object :: proc(p: ^Parser, v: any, end_token: Token_Kind) -> (err: Unm if use_field_idx < 0 { for field, field_idx in fields { - if key == field.name { + tag_value := reflect.struct_tag_get(field.tag, "json") + json_name, _ := json_name_from_tag_value(tag_value) + if json_name == "" && key == field.name { use_field_idx = field_idx break } @@ -486,7 +485,9 @@ unmarshal_object :: proc(p: ^Parser, v: any, end_token: Token_Kind) -> (err: Unm } } - if field.name == key || (field.tag != "" && reflect.struct_tag_get(field.tag, "json") == key) { + tag_value := reflect.struct_tag_get(field.tag, "json") + json_name, _ := json_name_from_tag_value(tag_value) + if (json_name == "" && field.name == key) || json_name == key { offset = field.offset type = field.type found = true @@ -508,6 +509,11 @@ unmarshal_object :: proc(p: ^Parser, v: any, end_token: Token_Kind) -> (err: Unm } if field_found { + field_test :: #force_inline proc "contextless" (field_used: [^]byte, offset: uintptr) -> bool { + prev_set := field_used[offset/8] & byte(offset&7) != 0 + field_used[offset/8] |= byte(offset&7) + return prev_set + } if field_test(field_used, offset) { return .Multiple_Use_Field } diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 7c9d4b80c..b210f6a52 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -240,7 +240,7 @@ Example: import "core:encoding/uuid" import "core:fmt" - main :: proc() { + generate_v8_hash_bytes_example :: proc() { my_uuid := uuid.generate_v8_hash(uuid.Namespace_DNS, "www.odin-lang.org", .SHA256) my_uuid_string := uuid.to_string(my_uuid, context.temp_allocator) fmt.println(my_uuid_string) @@ -306,7 +306,7 @@ Example: import "core:encoding/uuid" import "core:fmt" - main :: proc() { + generate_v8_hash_string_example :: proc() { my_uuid := uuid.generate_v8_hash(uuid.Namespace_DNS, "www.odin-lang.org", .SHA256) my_uuid_string := uuid.to_string(my_uuid, context.temp_allocator) fmt.println(my_uuid_string) diff --git a/core/encoding/xml/tokenizer.odin b/core/encoding/xml/tokenizer.odin index a2bbaf28e..3ef9a6388 100644 --- a/core/encoding/xml/tokenizer.odin +++ b/core/encoding/xml/tokenizer.odin @@ -16,6 +16,7 @@ package encoding_xml import "core:fmt" import "core:unicode" import "core:unicode/utf8" +import "core:strings" Error_Handler :: #type proc(pos: Pos, fmt: string, args: ..any) @@ -121,7 +122,7 @@ default_error_handler :: proc(pos: Pos, msg: string, args: ..any) { error :: proc(t: ^Tokenizer, offset: int, msg: string, args: ..any) { pos := offset_to_pos(t, offset) if t.err != nil { - t.err(pos, msg, ..args) + t.err(pos=pos, fmt=msg, args=args) } t.error_count += 1 } @@ -268,32 +269,27 @@ scan_comment :: proc(t: ^Tokenizer) -> (comment: string, err: Error) { // Skip CDATA skip_cdata :: proc(t: ^Tokenizer) -> (err: Error) { - if t.read_offset + len(CDATA_START) >= len(t.src) { - // Can't be the start of a CDATA tag. + if s := string(t.src[t.offset:]); !strings.has_prefix(s, CDATA_START) { return .None } - if string(t.src[t.offset:][:len(CDATA_START)]) == CDATA_START { - t.read_offset += len(CDATA_START) - offset := t.offset + t.read_offset += len(CDATA_START) + offset := t.offset - cdata_scan: for { - advance_rune(t) - if t.ch < 0 { - error(t, offset, "[scan_string] CDATA was not terminated\n") - return .Premature_EOF - } + cdata_scan: for { + advance_rune(t) + if t.ch < 0 { + error(t, offset, "[scan_string] CDATA was not terminated\n") + return .Premature_EOF + } - // Scan until the end of a CDATA tag. - if t.read_offset + len(CDATA_END) < len(t.src) { - if string(t.src[t.offset:][:len(CDATA_END)]) == CDATA_END { - t.read_offset += len(CDATA_END) - break cdata_scan - } - } + // Scan until the end of a CDATA tag. + if s := string(t.src[t.read_offset:]); strings.has_prefix(s, CDATA_END) { + t.read_offset += len(CDATA_END) + break cdata_scan } } - return + return .None } @(optimization_mode="favor_size") @@ -393,6 +389,8 @@ scan :: proc(t: ^Tokenizer, multiline_string := false) -> Token { case '/': kind = .Slash case '-': kind = .Dash case ':': kind = .Colon + case '[': kind = .Open_Bracket + case ']': kind = .Close_Bracket case '"', '\'': kind = .Invalid diff --git a/core/encoding/xml/xml_reader.odin b/core/encoding/xml/xml_reader.odin index b8c8b13a4..d616be9dc 100644 --- a/core/encoding/xml/xml_reader.odin +++ b/core/encoding/xml/xml_reader.odin @@ -56,7 +56,7 @@ Option_Flag :: enum { Option_Flags :: bit_set[Option_Flag; u16] Document :: struct { - elements: [dynamic]Element, + elements: [dynamic]Element `fmt:"v,element_count"`, element_count: Element_ID, prologue: Attributes, @@ -70,15 +70,15 @@ Document :: struct { // If we encounter comments before the root node, and the option to intern comments is given, this is where they'll live. // Otherwise they'll be in the element tree. - comments: [dynamic]string, + comments: [dynamic]string `fmt:"-"`, // Internal - tokenizer: ^Tokenizer, - allocator: mem.Allocator, + tokenizer: ^Tokenizer `fmt:"-"`, + allocator: mem.Allocator `fmt:"-"`, // Input. Either the original buffer, or a copy if `.Input_May_Be_Modified` isn't specified. - input: []u8, - strings_to_free: [dynamic]string, + input: []u8 `fmt:"-"`, + strings_to_free: [dynamic]string `fmt:"-"`, } Element :: struct { @@ -195,7 +195,6 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha loop: for { skip_whitespace(t) - // NOTE(Jeroen): This is faster as a switch. switch t.ch { case '<': // Consume peeked `<` @@ -306,9 +305,17 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha } } + case .Open_Bracket: + // This could be a CDATA tag part of a tag's body. Unread the ` (attr: Attribute, offset: int, err: E t := doc.tokenizer key := expect(t, .Ident) or_return - offset = t.offset - len(key.text) - _ = expect(t, .Eq) or_return value := expect(t, .String, multiline_string=true) or_return @@ -591,6 +565,47 @@ parse_doctype :: proc(doc: ^Document) -> (err: Error) { return .None } +parse_body :: proc(doc: ^Document, element: Element_ID, opts: Options) -> (err: Error) { + assert(doc != nil) + context.allocator = doc.allocator + t := doc.tokenizer + + body_text := scan_string(t, t.offset) or_return + needs_processing := .Unbox_CDATA in opts.flags + needs_processing |= .Decode_SGML_Entities in opts.flags + + if !needs_processing { + append(&doc.elements[element].value, body_text) + return + } + + decode_opts := entity.XML_Decode_Options{} + if .Keep_Tag_Body_Comments not_in opts.flags { + decode_opts += { .Comment_Strip } + } + + if .Decode_SGML_Entities not_in opts.flags { + decode_opts += { .No_Entity_Decode } + } + + if .Unbox_CDATA in opts.flags { + decode_opts += { .Unbox_CDATA } + if .Decode_SGML_Entities in opts.flags { + decode_opts += { .Decode_CDATA } + } + } + + decoded, decode_err := entity.decode_xml(body_text, decode_opts) + if decode_err == .None { + append(&doc.elements[element].value, decoded) + append(&doc.strings_to_free, decoded) + } else { + append(&doc.elements[element].value, body_text) + } + + return +} + Element_ID :: u32 new_element :: proc(doc: ^Document) -> (id: Element_ID) { @@ -609,4 +624,4 @@ new_element :: proc(doc: ^Document) -> (id: Element_ID) { cur := doc.element_count doc.element_count += 1 return cur -} +} \ No newline at end of file diff --git a/core/flags/util.odin b/core/flags/util.odin index c182289be..ce7e2e36c 100644 --- a/core/flags/util.odin +++ b/core/flags/util.odin @@ -95,7 +95,7 @@ Example: import "core:flags" import "core:fmt" - subtag_example :: proc() { + get_subtag_example :: proc() { args_tag := "precision=3,signed" precision, has_precision := flags.get_subtag(args_tag, "precision") diff --git a/core/fmt/fmt.odin b/core/fmt/fmt.odin index 826a21ee9..9c07847dd 100644 --- a/core/fmt/fmt.odin +++ b/core/fmt/fmt.odin @@ -1449,9 +1449,12 @@ fmt_float :: proc(fi: ^Info, v: f64, bit_size: int, verb: rune) { _fmt_float_as(fi, v, bit_size, verb, 'g', -1) case 'f', 'F': _fmt_float_as(fi, v, bit_size, verb, 'f', 3) - case 'e', 'E': + case 'e': // BUG(): "%.3e" returns "3.000e+00" _fmt_float_as(fi, v, bit_size, verb, 'e', 6) + case 'E': + // BUG(): "%.3E" returns "3.000E+00" + _fmt_float_as(fi, v, bit_size, verb, 'E', 6) case 'h', 'H': prev_fi := fi^ @@ -1802,11 +1805,8 @@ fmt_bit_set :: proc(fi: ^Info, v: any, name: string = "", verb: rune = 'v') { e, is_enum := et.variant.(runtime.Type_Info_Enum) commas := 0 - loop: for i in 0 ..< bit_size { - if bits & (1< 0 { io.write_string(fi.writer, ", ", &fi.n) } @@ -1829,8 +1829,7 @@ fmt_bit_set :: proc(fi: ^Info, v: any, name: string = "", verb: rune = 'v') { } } } - v := i64(i) + info.lower - io.write_i64(fi.writer, v, 10, &fi.n) + io.write_i64(fi.writer, i, 10, &fi.n) commas += 1 } } diff --git a/core/log/file_console_logger.odin b/core/log/file_console_logger.odin index 6d93fb879..0fe5c3477 100644 --- a/core/log/file_console_logger.odin +++ b/core/log/file_console_logger.odin @@ -2,10 +2,12 @@ #+build !orca package log -import "core:encoding/ansi" +import "base:runtime" import "core:fmt" import "core:strings" import "core:os" +import "core:terminal" +import "core:terminal/ansi" import "core:time" Level_Headers := [?]string{ @@ -37,11 +39,36 @@ File_Console_Logger_Data :: struct { ident: string, } +@(private) global_subtract_stdout_options: Options +@(private) global_subtract_stderr_options: Options + +@(init, private) +init_standard_stream_status :: proc() { + // NOTE(Feoramund): While it is technically possible for these streams to + // be redirected during the runtime of the program, the cost of checking on + // every single log message is not worth it to support such an + // uncommonly-used feature. + if terminal.color_enabled { + // This is done this way because it's possible that only one of these + // streams could be redirected to a file. + if !terminal.is_terminal(os.stdout) { + global_subtract_stdout_options = {.Terminal_Color} + } + if !terminal.is_terminal(os.stderr) { + global_subtract_stderr_options = {.Terminal_Color} + } + } else { + // Override any terminal coloring. + global_subtract_stdout_options = {.Terminal_Color} + global_subtract_stderr_options = {.Terminal_Color} + } +} + create_file_logger :: proc(h: os.Handle, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger { data := new(File_Console_Logger_Data, allocator) data.file_handle = h data.ident = ident - return Logger{file_console_logger_proc, data, lowest, opt} + return Logger{file_logger_proc, data, lowest, opt} } destroy_file_logger :: proc(log: Logger, allocator := context.allocator) { @@ -56,19 +83,15 @@ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logg data := new(File_Console_Logger_Data, allocator) data.file_handle = os.INVALID_HANDLE data.ident = ident - return Logger{file_console_logger_proc, data, lowest, opt} + return Logger{console_logger_proc, data, lowest, opt} } destroy_console_logger :: proc(log: Logger, allocator := context.allocator) { free(log.data, allocator) } -file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) { - data := cast(^File_Console_Logger_Data)logger_data - h: os.Handle = os.stdout if level <= Level.Error else os.stderr - if data.file_handle != os.INVALID_HANDLE { - h = data.file_handle - } +@(private) +_file_console_logger_proc :: proc(h: os.Handle, ident: string, level: Level, text: string, options: Options, location: runtime.Source_Code_Location) { backing: [1024]byte //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths. buf := strings.builder_from_bytes(backing[:]) @@ -86,13 +109,32 @@ file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string fmt.sbprintf(&buf, "[{}] ", os.current_thread_id()) } - if data.ident != "" { - fmt.sbprintf(&buf, "[%s] ", data.ident) + if ident != "" { + fmt.sbprintf(&buf, "[%s] ", ident) } //TODO(Hoej): When we have better atomics and such, make this thread-safe fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text) } +file_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) { + data := cast(^File_Console_Logger_Data)logger_data + _file_console_logger_proc(data.file_handle, data.ident, level, text, options, location) +} + +console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) { + options := options + data := cast(^File_Console_Logger_Data)logger_data + h: os.Handle = --- + if level < Level.Error { + h = os.stdout + options -= global_subtract_stdout_options + } else { + h = os.stderr + options -= global_subtract_stderr_options + } + _file_console_logger_proc(h, data.ident, level, text, options, location) +} + do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) { RESET :: ansi.CSI + ansi.RESET + ansi.SGR diff --git a/core/math/math.odin b/core/math/math.odin index 934842318..b99a97bc1 100644 --- a/core/math/math.odin +++ b/core/math/math.odin @@ -2296,7 +2296,7 @@ nextafter_f16 :: proc "contextless" (x, y: f16) -> (r: f16) { case x == y: r = x case x == 0: - r = copy_sign_f16(1, y) + r = copy_sign_f16(transmute(f16)u16(1), y) case (y > x) == (x > 0): r = transmute(f16)(transmute(u16)x + 1) case: @@ -2312,7 +2312,7 @@ nextafter_f32 :: proc "contextless" (x, y: f32) -> (r: f32) { case x == y: r = x case x == 0: - r = copy_sign_f32(1, y) + r = copy_sign_f32(transmute(f32)u32(1), y) case (y > x) == (x > 0): r = transmute(f32)(transmute(u32)x + 1) case: @@ -2328,7 +2328,7 @@ nextafter_f64 :: proc "contextless" (x, y: f64) -> (r: f64) { case x == y: r = x case x == 0: - r = copy_sign_f64(1, y) + r = copy_sign_f64(transmute(f64)u64(1), y) case (y > x) == (x > 0): r = transmute(f64)(transmute(u64)x + 1) case: @@ -2349,32 +2349,6 @@ nextafter :: proc{ nextafter_f64, nextafter_f64le, nextafter_f64be, } -@(require_results) -signbit_f16 :: proc "contextless" (x: f16) -> bool { - return (transmute(u16)x)&(1<<15) != 0 -} -@(require_results) -signbit_f32 :: proc "contextless" (x: f32) -> bool { - return (transmute(u32)x)&(1<<31) != 0 -} -@(require_results) -signbit_f64 :: proc "contextless" (x: f64) -> bool { - return (transmute(u64)x)&(1<<63) != 0 -} -@(require_results) signbit_f16le :: proc "contextless" (x: f16le) -> bool { return signbit_f16(f16(x)) } -@(require_results) signbit_f32le :: proc "contextless" (x: f32le) -> bool { return signbit_f32(f32(x)) } -@(require_results) signbit_f64le :: proc "contextless" (x: f64le) -> bool { return signbit_f64(f64(x)) } -@(require_results) signbit_f16be :: proc "contextless" (x: f16be) -> bool { return signbit_f16(f16(x)) } -@(require_results) signbit_f32be :: proc "contextless" (x: f32be) -> bool { return signbit_f32(f32(x)) } -@(require_results) signbit_f64be :: proc "contextless" (x: f64be) -> bool { return signbit_f64(f64(x)) } - -signbit :: proc{ - signbit_f16, signbit_f16le, signbit_f16be, - signbit_f32, signbit_f32le, signbit_f32be, - signbit_f64, signbit_f64le, signbit_f64be, -} - - @(require_results) hypot_f16 :: proc "contextless" (x, y: f16) -> (r: f16) { p, q := abs(x), abs(y) diff --git a/core/math/math_gamma.odin b/core/math/math_gamma.odin index 9f5a364d3..9e8c2a909 100644 --- a/core/math/math_gamma.odin +++ b/core/math/math_gamma.odin @@ -132,7 +132,7 @@ gamma_f64 :: proc "contextless" (x: f64) -> f64 { case is_inf(x, 1): return inf_f64(1) case x == 0: - if signbit(x) { + if sign_bit(x) { return inf_f64(-1) } return inf_f64(1) diff --git a/core/math/rand/rand.odin b/core/math/rand/rand.odin index 537256d32..56208931f 100644 --- a/core/math/rand/rand.odin +++ b/core/math/rand/rand.odin @@ -33,7 +33,7 @@ Example: import "core:math/rand" import "core:fmt" - set_global_seed_example :: proc() { + reset_example :: proc() { rand.reset(1) fmt.println(rand.uint64()) } @@ -432,7 +432,7 @@ Example: Possible Output: 15.312 - 673.130 + 273.15 */ @(require_results) float64_range :: proc(low, high: f64, gen := context.random_generator) -> (val: f64) { @@ -467,7 +467,7 @@ Example: Possible Output: 15.312 - 273.130 + 273.15 */ @(require_results) float32_range :: proc(low, high: f32, gen := context.random_generator) -> (val: f32) { diff --git a/core/mem/allocators.odin b/core/mem/allocators.odin index 028be58e3..1bb887212 100644 --- a/core/mem/allocators.odin +++ b/core/mem/allocators.odin @@ -2,6 +2,7 @@ package mem import "base:intrinsics" import "base:runtime" +import "base:sanitizer" /* Nil allocator. @@ -138,6 +139,7 @@ arena_init :: proc(a: ^Arena, data: []byte) { a.offset = 0 a.peak_used = 0 a.temp_count = 0 + sanitizer.address_poison(a.data) } /* @@ -224,7 +226,9 @@ arena_alloc_bytes_non_zeroed :: proc( } a.offset += total_size a.peak_used = max(a.peak_used, a.offset) - return byte_slice(ptr, size), nil + result := byte_slice(ptr, size) + sanitizer.address_unpoison(result) + return result, nil } /* @@ -232,6 +236,7 @@ Free all memory to an arena. */ arena_free_all :: proc(a: ^Arena) { a.offset = 0 + sanitizer.address_poison(a.data) } arena_allocator_proc :: proc( @@ -309,6 +314,7 @@ allocations *inside* the temporary memory region will be freed to the arena. end_arena_temp_memory :: proc(tmp: Arena_Temp_Memory) { assert(tmp.arena.offset >= tmp.prev_offset) assert(tmp.arena.temp_count > 0) + sanitizer.address_poison(tmp.arena.data[tmp.prev_offset:tmp.arena.offset]) tmp.arena.offset = tmp.prev_offset tmp.arena.temp_count -= 1 } @@ -363,6 +369,7 @@ scratch_init :: proc(s: ^Scratch, size: int, backup_allocator := context.allocat s.prev_allocation = nil s.backup_allocator = backup_allocator s.leaked_allocations.allocator = backup_allocator + sanitizer.address_poison(s.data) return nil } @@ -377,6 +384,7 @@ scratch_destroy :: proc(s: ^Scratch) { free_bytes(ptr, s.backup_allocator) } delete(s.leaked_allocations) + sanitizer.address_unpoison(s.data) delete(s.data, s.backup_allocator) s^ = {} } @@ -472,7 +480,9 @@ scratch_alloc_bytes_non_zeroed :: proc( ptr := align_forward_uintptr(offset+start, uintptr(alignment)) s.prev_allocation = rawptr(ptr) s.curr_offset = int(offset) + size - return byte_slice(rawptr(ptr), size), nil + result := byte_slice(rawptr(ptr), size) + sanitizer.address_unpoison(result) + return result, nil } else { a := s.backup_allocator if a.procedure == nil { @@ -516,6 +526,7 @@ scratch_free :: proc(s: ^Scratch, ptr: rawptr, loc := #caller_location) -> Alloc old_ptr := uintptr(ptr) if s.prev_allocation == ptr { s.curr_offset = int(uintptr(s.prev_allocation) - start) + sanitizer.address_poison(s.data[s.curr_offset:]) s.prev_allocation = nil return nil } @@ -546,6 +557,7 @@ scratch_free_all :: proc(s: ^Scratch, loc := #caller_location) { free_bytes(ptr, s.backup_allocator, loc) } clear(&s.leaked_allocations) + sanitizer.address_poison(s.data) } /* @@ -675,7 +687,9 @@ scratch_resize_bytes_non_zeroed :: proc( old_ptr := uintptr(old_memory) if begin <= old_ptr && old_ptr < end && old_ptr+uintptr(size) < end { s.curr_offset = int(old_ptr-begin)+size - return byte_slice(old_memory, size), nil + result := byte_slice(old_memory, size) + sanitizer.address_unpoison(result) + return result, nil } data, err := scratch_alloc_bytes_non_zeroed(s, size, alignment, loc) if err != nil { @@ -776,6 +790,7 @@ stack_init :: proc(s: ^Stack, data: []byte) { s.prev_offset = 0 s.curr_offset = 0 s.peak_used = 0 + sanitizer.address_poison(data) } /* @@ -861,15 +876,19 @@ stack_alloc_bytes_non_zeroed :: proc( if s.curr_offset + padding + size > len(s.data) { return nil, .Out_Of_Memory } + old_offset := s.prev_offset s.prev_offset = s.curr_offset s.curr_offset += padding next_addr := curr_addr + uintptr(padding) header := (^Stack_Allocation_Header)(next_addr - size_of(Stack_Allocation_Header)) + sanitizer.address_unpoison(header) header.padding = padding - header.prev_offset = s.prev_offset + header.prev_offset = old_offset s.curr_offset += size s.peak_used = max(s.peak_used, s.curr_offset) - return byte_slice(rawptr(next_addr), size), nil + result := byte_slice(rawptr(next_addr), size) + sanitizer.address_unpoison(result) + return result, nil } /* @@ -902,12 +921,15 @@ stack_free :: proc( } header := (^Stack_Allocation_Header)(curr_addr - size_of(Stack_Allocation_Header)) old_offset := int(curr_addr - uintptr(header.padding) - uintptr(raw_data(s.data))) - if old_offset != header.prev_offset { + if old_offset != s.prev_offset { // panic("Out of order stack allocator free"); return .Invalid_Pointer } - s.curr_offset = old_offset + s.prev_offset = header.prev_offset + sanitizer.address_poison(s.data[old_offset:s.curr_offset]) + s.curr_offset = old_offset + return nil } @@ -917,6 +939,7 @@ Free all allocations to the stack. stack_free_all :: proc(s: ^Stack, loc := #caller_location) { s.prev_offset = 0 s.curr_offset = 0 + sanitizer.address_poison(s.data) } /* @@ -1076,7 +1099,9 @@ stack_resize_bytes_non_zeroed :: proc( if diff > 0 { zero(rawptr(curr_addr + uintptr(diff)), diff) } - return byte_slice(old_memory, size), nil + result := byte_slice(old_memory, size) + sanitizer.address_unpoison(result) + return result, nil } stack_allocator_proc :: proc( @@ -1144,6 +1169,7 @@ small_stack_init :: proc(s: ^Small_Stack, data: []byte) { s.data = data s.offset = 0 s.peak_used = 0 + sanitizer.address_poison(data) } /* @@ -1252,10 +1278,13 @@ small_stack_alloc_bytes_non_zeroed :: proc( s.offset += padding next_addr := curr_addr + uintptr(padding) header := (^Small_Stack_Allocation_Header)(next_addr - size_of(Small_Stack_Allocation_Header)) + sanitizer.address_unpoison(header) header.padding = auto_cast padding s.offset += size s.peak_used = max(s.peak_used, s.offset) - return byte_slice(rawptr(next_addr), size), nil + result := byte_slice(rawptr(next_addr), size) + sanitizer.address_unpoison(result) + return result, nil } /* @@ -1289,6 +1318,7 @@ small_stack_free :: proc( } header := (^Small_Stack_Allocation_Header)(curr_addr - size_of(Small_Stack_Allocation_Header)) old_offset := int(curr_addr - uintptr(header.padding) - uintptr(raw_data(s.data))) + sanitizer.address_poison(s.data[old_offset:s.offset]) s.offset = old_offset return nil } @@ -1298,6 +1328,7 @@ Free all memory to small stack. */ small_stack_free_all :: proc(s: ^Small_Stack) { s.offset = 0 + sanitizer.address_poison(s.data) } /* @@ -1442,7 +1473,9 @@ small_stack_resize_bytes_non_zeroed :: proc( return nil, nil } if old_size == size { - return byte_slice(old_memory, size), nil + result := byte_slice(old_memory, size) + sanitizer.address_unpoison(result) + return result, nil } data, err := small_stack_alloc_bytes_non_zeroed(s, size, alignment, loc) if err == nil { diff --git a/core/mem/rollback_stack_allocator.odin b/core/mem/rollback_stack_allocator.odin index 43ef10fe9..a00131b7f 100644 --- a/core/mem/rollback_stack_allocator.odin +++ b/core/mem/rollback_stack_allocator.odin @@ -1,6 +1,7 @@ package mem import "base:runtime" +import "base:sanitizer" /* Rollback stack default block size. @@ -47,14 +48,14 @@ Rollback_Stack :: struct { block_allocator: Allocator, } -@(private="file", require_results) +@(private="file", require_results, no_sanitize_address) rb_ptr_in_bounds :: proc(block: ^Rollback_Stack_Block, ptr: rawptr) -> bool { start := raw_data(block.buffer) end := start[block.offset:] return start < ptr && ptr <= end } -@(private="file", require_results) +@(private="file", require_results, no_sanitize_address) rb_find_ptr :: proc(stack: ^Rollback_Stack, ptr: rawptr) -> ( parent: ^Rollback_Stack_Block, block: ^Rollback_Stack_Block, @@ -71,7 +72,7 @@ rb_find_ptr :: proc(stack: ^Rollback_Stack, ptr: rawptr) -> ( return nil, nil, nil, .Invalid_Pointer } -@(private="file", require_results) +@(private="file", require_results, no_sanitize_address) rb_find_last_alloc :: proc(stack: ^Rollback_Stack, ptr: rawptr) -> ( block: ^Rollback_Stack_Block, header: ^Rollback_Stack_Header, @@ -86,9 +87,10 @@ rb_find_last_alloc :: proc(stack: ^Rollback_Stack, ptr: rawptr) -> ( return nil, nil, false } -@(private="file") +@(private="file", no_sanitize_address) rb_rollback_block :: proc(block: ^Rollback_Stack_Block, header: ^Rollback_Stack_Header) { header := header + for block.offset > 0 && header.is_free { block.offset = header.prev_offset block.last_alloc = raw_data(block.buffer)[header.prev_ptr:] @@ -99,9 +101,10 @@ rb_rollback_block :: proc(block: ^Rollback_Stack_Block, header: ^Rollback_Stack_ /* Free memory to a rollback stack allocator. */ -@(private="file", require_results) +@(private="file", require_results, no_sanitize_address) rb_free :: proc(stack: ^Rollback_Stack, ptr: rawptr) -> Allocator_Error { parent, block, header := rb_find_ptr(stack, ptr) or_return + if header.is_free { return .Invalid_Pointer } @@ -120,7 +123,7 @@ rb_free :: proc(stack: ^Rollback_Stack, ptr: rawptr) -> Allocator_Error { /* Free all memory owned by the rollback stack allocator. */ -@(private="file") +@(private="file", no_sanitize_address) rb_free_all :: proc(stack: ^Rollback_Stack) { for block := stack.head.next_block; block != nil; /**/ { next_block := block.next_block @@ -131,12 +134,13 @@ rb_free_all :: proc(stack: ^Rollback_Stack) { stack.head.next_block = nil stack.head.last_alloc = nil stack.head.offset = 0 + sanitizer.address_poison(stack.head.buffer) } /* Allocate memory using the rollback stack allocator. */ -@(require_results) +@(require_results, no_sanitize_address) rb_alloc :: proc( stack: ^Rollback_Stack, size: int, @@ -153,7 +157,7 @@ rb_alloc :: proc( /* Allocate memory using the rollback stack allocator. */ -@(require_results) +@(require_results, no_sanitize_address) rb_alloc_bytes :: proc( stack: ^Rollback_Stack, size: int, @@ -170,7 +174,7 @@ rb_alloc_bytes :: proc( /* Allocate non-initialized memory using the rollback stack allocator. */ -@(require_results) +@(require_results, no_sanitize_address) rb_alloc_non_zeroed :: proc( stack: ^Rollback_Stack, size: int, @@ -184,7 +188,7 @@ rb_alloc_non_zeroed :: proc( /* Allocate non-initialized memory using the rollback stack allocator. */ -@(require_results) +@(require_results, no_sanitize_address) rb_alloc_bytes_non_zeroed :: proc( stack: ^Rollback_Stack, size: int, @@ -194,6 +198,7 @@ rb_alloc_bytes_non_zeroed :: proc( assert(size >= 0, "Size must be positive or zero.", loc) assert(is_power_of_two(cast(uintptr)alignment), "Alignment must be a power of two.", loc) parent: ^Rollback_Stack_Block + for block := stack.head; /**/; block = block.next_block { when !ODIN_DISABLE_ASSERT { allocated_new_block: bool @@ -235,7 +240,9 @@ rb_alloc_bytes_non_zeroed :: proc( // Prevent any further allocations on it. block.offset = cast(uintptr)len(block.buffer) } - #no_bounds_check return ptr[:size], nil + res := ptr[:size] + sanitizer.address_unpoison(res) + return res, nil } return nil, .Out_Of_Memory } @@ -243,7 +250,7 @@ rb_alloc_bytes_non_zeroed :: proc( /* Resize an allocation owned by rollback stack allocator. */ -@(require_results) +@(require_results, no_sanitize_address) rb_resize :: proc( stack: ^Rollback_Stack, old_ptr: rawptr, @@ -266,7 +273,7 @@ rb_resize :: proc( /* Resize an allocation owned by rollback stack allocator. */ -@(require_results) +@(require_results, no_sanitize_address) rb_resize_bytes :: proc( stack: ^Rollback_Stack, old_memory: []byte, @@ -289,7 +296,7 @@ rb_resize_bytes :: proc( Resize an allocation owned by rollback stack allocator without explicit zero-initialization. */ -@(require_results) +@(require_results, no_sanitize_address) rb_resize_non_zeroed :: proc( stack: ^Rollback_Stack, old_ptr: rawptr, @@ -306,7 +313,7 @@ rb_resize_non_zeroed :: proc( Resize an allocation owned by rollback stack allocator without explicit zero-initialization. */ -@(require_results) +@(require_results, no_sanitize_address) rb_resize_bytes_non_zeroed :: proc( stack: ^Rollback_Stack, old_memory: []byte, @@ -330,7 +337,9 @@ rb_resize_bytes_non_zeroed :: proc( if len(block.buffer) <= stack.block_size { block.offset += cast(uintptr)size - cast(uintptr)old_size } - #no_bounds_check return (ptr)[:size], nil + res := (ptr)[:size] + sanitizer.address_unpoison(res) + #no_bounds_check return res, nil } } } @@ -340,7 +349,7 @@ rb_resize_bytes_non_zeroed :: proc( return } -@(private="file", require_results) +@(private="file", require_results, no_sanitize_address) rb_make_block :: proc(size: int, allocator: Allocator) -> (block: ^Rollback_Stack_Block, err: Allocator_Error) { buffer := runtime.mem_alloc(size_of(Rollback_Stack_Block) + size, align_of(Rollback_Stack_Block), allocator) or_return block = cast(^Rollback_Stack_Block)raw_data(buffer) @@ -351,6 +360,7 @@ rb_make_block :: proc(size: int, allocator: Allocator) -> (block: ^Rollback_Stac /* Initialize the rollback stack allocator using a fixed backing buffer. */ +@(no_sanitize_address) rollback_stack_init_buffered :: proc(stack: ^Rollback_Stack, buffer: []byte, location := #caller_location) { MIN_SIZE :: size_of(Rollback_Stack_Block) + size_of(Rollback_Stack_Header) + size_of(rawptr) assert(len(buffer) >= MIN_SIZE, "User-provided buffer to Rollback Stack Allocator is too small.", location) @@ -365,6 +375,7 @@ rollback_stack_init_buffered :: proc(stack: ^Rollback_Stack, buffer: []byte, loc /* Initialize the rollback stack alocator using a backing block allocator. */ +@(no_sanitize_address) rollback_stack_init_dynamic :: proc( stack: ^Rollback_Stack, block_size : int = ROLLBACK_STACK_DEFAULT_BLOCK_SIZE, @@ -396,6 +407,7 @@ rollback_stack_init :: proc { /* Destroy a rollback stack. */ +@(no_sanitize_address) rollback_stack_destroy :: proc(stack: ^Rollback_Stack) { if stack.block_allocator.procedure != nil { rb_free_all(stack) @@ -435,7 +447,7 @@ from the last allocation backwards. Each allocation has an overhead of 8 bytes and any extra bytes to satisfy the requested alignment. */ -@(require_results) +@(require_results, no_sanitize_address) rollback_stack_allocator :: proc(stack: ^Rollback_Stack) -> Allocator { return Allocator { data = stack, @@ -443,7 +455,7 @@ rollback_stack_allocator :: proc(stack: ^Rollback_Stack) -> Allocator { } } -@(require_results) +@(require_results, no_sanitize_address) rollback_stack_allocator_proc :: proc( allocator_data: rawptr, mode: Allocator_Mode, diff --git a/core/mem/tlsf/tlsf.odin b/core/mem/tlsf/tlsf.odin index 8ec5f52b7..0ae8c28e0 100644 --- a/core/mem/tlsf/tlsf.odin +++ b/core/mem/tlsf/tlsf.odin @@ -10,6 +10,7 @@ // package mem_tlsf implements a Two Level Segregated Fit memory allocator. package mem_tlsf +import "base:intrinsics" import "base:runtime" Error :: enum byte { @@ -21,7 +22,6 @@ Error :: enum byte { Backing_Allocator_Error = 5, } - Allocator :: struct { // Empty lists point at this block to indicate they are free. block_null: Block_Header, @@ -39,12 +39,13 @@ Allocator :: struct { // statistics like how much memory is still available, // fragmentation, etc. pool: Pool, + + // If we're expected to grow when we run out of memory, + // how much should we ask the backing allocator for? + new_pool_size: uint, } #assert(size_of(Allocator) % ALIGN_SIZE == 0) - - - @(require_results) allocator :: proc(t: ^Allocator) -> runtime.Allocator { return runtime.Allocator{ @@ -53,6 +54,21 @@ allocator :: proc(t: ^Allocator) -> runtime.Allocator { } } +// Tries to estimate a pool size sufficient for `count` allocations, each of `size` and with `alignment`. +estimate_pool_from_size_alignment :: proc(count: int, size: int, alignment: int) -> (pool_size: int) { + per_allocation := align_up(uint(size + alignment) + BLOCK_HEADER_OVERHEAD, ALIGN_SIZE) + return count * int(per_allocation) + int(INITIAL_POOL_OVERHEAD) +} + +// Tries to estimate a pool size sufficient for `count` allocations of `type`. +estimate_pool_from_typeid :: proc(count: int, type: typeid) -> (pool_size: int) { + ti := type_info_of(type) + return estimate_pool_size(count, ti.size, ti.align) +} + +estimate_pool_size :: proc{estimate_pool_from_size_alignment, estimate_pool_from_typeid} + + @(require_results) init_from_buffer :: proc(control: ^Allocator, buf: []byte) -> Error { assert(control != nil) @@ -60,21 +76,25 @@ init_from_buffer :: proc(control: ^Allocator, buf: []byte) -> Error { return .Invalid_Alignment } - pool_bytes := align_down(len(buf) - POOL_OVERHEAD, ALIGN_SIZE) + pool_bytes := align_down(len(buf) - INITIAL_POOL_OVERHEAD, ALIGN_SIZE) if pool_bytes < BLOCK_SIZE_MIN { return .Backing_Buffer_Too_Small } else if pool_bytes > BLOCK_SIZE_MAX { return .Backing_Buffer_Too_Large } - clear(control) - return pool_add(control, buf[:]) + control.pool = Pool{ + data = buf, + allocator = {}, + } + + return free_all(control) } @(require_results) init_from_allocator :: proc(control: ^Allocator, backing: runtime.Allocator, initial_pool_size: int, new_pool_size := 0) -> Error { assert(control != nil) - pool_bytes := align_up(uint(initial_pool_size) + POOL_OVERHEAD, ALIGN_SIZE) + pool_bytes := uint(estimate_pool_size(1, initial_pool_size, ALIGN_SIZE)) if pool_bytes < BLOCK_SIZE_MIN { return .Backing_Buffer_Too_Small } else if pool_bytes > BLOCK_SIZE_MAX { @@ -85,12 +105,15 @@ init_from_allocator :: proc(control: ^Allocator, backing: runtime.Allocator, ini if backing_err != nil { return .Backing_Allocator_Error } - err := init_from_buffer(control, buf) + control.pool = Pool{ data = buf, allocator = backing, } - return err + + control.new_pool_size = uint(new_pool_size) + + return free_all(control) } init :: proc{init_from_buffer, init_from_allocator} @@ -103,8 +126,6 @@ destroy :: proc(control: ^Allocator) { // No need to call `pool_remove` or anything, as they're they're embedded in the backing memory. // We do however need to free the `Pool` tracking entities and the backing memory itself. - // As `Allocator` is embedded in the first backing slice, the `control` pointer will be - // invalid after this call. for p := control.pool.next; p != nil; { next := p.next @@ -136,9 +157,8 @@ allocator_proc :: proc(allocator_data: rawptr, mode: runtime.Allocator_Mode, return nil, nil case .Free_All: - // NOTE: this doesn't work right at the moment, Jeroen has it on his to-do list :) - // clear(control) - return nil, .Mode_Not_Implemented + free_all(control) + return nil, nil case .Resize: return resize(control, old_memory, uint(old_size), uint(size), uint(alignment)) @@ -159,3 +179,23 @@ allocator_proc :: proc(allocator_data: rawptr, mode: runtime.Allocator_Mode, return nil, nil } + +// Exported solely to facilitate testing +@(require_results) +ffs :: proc "contextless" (word: u32) -> (bit: i32) { + return -1 if word == 0 else i32(intrinsics.count_trailing_zeros(word)) +} + +// Exported solely to facilitate testing +@(require_results) +fls :: proc "contextless" (word: u32) -> (bit: i32) { + N :: (size_of(u32) * 8) - 1 + return i32(N - intrinsics.count_leading_zeros(word)) +} + +// Exported solely to facilitate testing +@(require_results) +fls_uint :: proc "contextless" (size: uint) -> (bit: i32) { + N :: (size_of(uint) * 8) - 1 + return i32(N - intrinsics.count_leading_zeros(size)) +} diff --git a/core/mem/tlsf/tlsf_internal.odin b/core/mem/tlsf/tlsf_internal.odin index 66141fcbb..89b875679 100644 --- a/core/mem/tlsf/tlsf_internal.odin +++ b/core/mem/tlsf/tlsf_internal.odin @@ -7,12 +7,11 @@ Jeroen van Rijn: Source port */ - package mem_tlsf import "base:intrinsics" +import "base:sanitizer" import "base:runtime" -// import "core:fmt" // log2 of number of linear subdivisions of block sizes. // Larger values require more memory in the control structure. @@ -58,7 +57,6 @@ Pool :: struct { next: ^Pool, } - /* Block header structure. @@ -95,6 +93,7 @@ The `prev_phys_block` field is stored *inside* the previous free block. BLOCK_HEADER_OVERHEAD :: uint(size_of(uint)) POOL_OVERHEAD :: 2 * BLOCK_HEADER_OVERHEAD +INITIAL_POOL_OVERHEAD :: 48 // User data starts directly after the size field in a used block. BLOCK_START_OFFSET :: offset_of(Block_Header, size) + size_of(Block_Header{}.size) @@ -107,436 +106,12 @@ bits for `FL_INDEX`. BLOCK_SIZE_MIN :: uint(size_of(Block_Header) - size_of(^Block_Header)) BLOCK_SIZE_MAX :: uint(1) << FL_INDEX_MAX -/* - TLSF achieves O(1) cost for `alloc` and `free` operations by limiting - the search for a free block to a free list of guaranteed size - adequate to fulfill the request, combined with efficient free list - queries using bitmasks and architecture-specific bit-manipulation - routines. - - NOTE: TLSF spec relies on ffs/fls returning value 0..31. -*/ - -@(require_results) -ffs :: proc "contextless" (word: u32) -> (bit: i32) { - return -1 if word == 0 else i32(intrinsics.count_trailing_zeros(word)) -} - -@(require_results) -fls :: proc "contextless" (word: u32) -> (bit: i32) { - N :: (size_of(u32) * 8) - 1 - return i32(N - intrinsics.count_leading_zeros(word)) -} - -@(require_results) -fls_uint :: proc "contextless" (size: uint) -> (bit: i32) { - N :: (size_of(uint) * 8) - 1 - return i32(N - intrinsics.count_leading_zeros(size)) -} - -@(require_results) -block_size :: proc "contextless" (block: ^Block_Header) -> (size: uint) { - return block.size &~ (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE) -} - -block_set_size :: proc "contextless" (block: ^Block_Header, size: uint) { - old_size := block.size - block.size = size | (old_size & (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE)) -} - -@(require_results) -block_is_last :: proc "contextless" (block: ^Block_Header) -> (is_last: bool) { - return block_size(block) == 0 -} - -@(require_results) -block_is_free :: proc "contextless" (block: ^Block_Header) -> (is_free: bool) { - return (block.size & BLOCK_HEADER_FREE) == BLOCK_HEADER_FREE -} - -block_set_free :: proc "contextless" (block: ^Block_Header) { - block.size |= BLOCK_HEADER_FREE -} - -block_set_used :: proc "contextless" (block: ^Block_Header) { - block.size &~= BLOCK_HEADER_FREE -} - -@(require_results) -block_is_prev_free :: proc "contextless" (block: ^Block_Header) -> (is_prev_free: bool) { - return (block.size & BLOCK_HEADER_PREV_FREE) == BLOCK_HEADER_PREV_FREE -} - -block_set_prev_free :: proc "contextless" (block: ^Block_Header) { - block.size |= BLOCK_HEADER_PREV_FREE -} - -block_set_prev_used :: proc "contextless" (block: ^Block_Header) { - block.size &~= BLOCK_HEADER_PREV_FREE -} - -@(require_results) -block_from_ptr :: proc(ptr: rawptr) -> (block_ptr: ^Block_Header) { - return (^Block_Header)(uintptr(ptr) - BLOCK_START_OFFSET) -} - -@(require_results) -block_to_ptr :: proc(block: ^Block_Header) -> (ptr: rawptr) { - return rawptr(uintptr(block) + BLOCK_START_OFFSET) -} - -// Return location of next block after block of given size. -@(require_results) -offset_to_block :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) { - return (^Block_Header)(uintptr(ptr) + uintptr(size)) -} - -@(require_results) -offset_to_block_backwards :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) { - return (^Block_Header)(uintptr(ptr) - uintptr(size)) -} - -// Return location of previous block. -@(require_results) -block_prev :: proc(block: ^Block_Header) -> (prev: ^Block_Header) { - assert(block_is_prev_free(block), "previous block must be free") - return block.prev_phys_block -} - -// Return location of next existing block. -@(require_results) -block_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) { - return offset_to_block(block_to_ptr(block), block_size(block) - BLOCK_HEADER_OVERHEAD) -} - -// Link a new block with its physical neighbor, return the neighbor. -@(require_results) -block_link_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) { - next = block_next(block) - next.prev_phys_block = block - return -} - -block_mark_as_free :: proc(block: ^Block_Header) { - // Link the block to the next block, first. - next := block_link_next(block) - block_set_prev_free(next) - block_set_free(block) -} - -block_mark_as_used :: proc(block: ^Block_Header) { - next := block_next(block) - block_set_prev_used(next) - block_set_used(block) -} - -@(require_results) -align_up :: proc(x, align: uint) -> (aligned: uint) { - assert(0 == (align & (align - 1)), "must align to a power of two") - return (x + (align - 1)) &~ (align - 1) -} - -@(require_results) -align_down :: proc(x, align: uint) -> (aligned: uint) { - assert(0 == (align & (align - 1)), "must align to a power of two") - return x - (x & (align - 1)) -} - -@(require_results) -align_ptr :: proc(ptr: rawptr, align: uint) -> (aligned: rawptr) { - assert(0 == (align & (align - 1)), "must align to a power of two") - align_mask := uintptr(align) - 1 - _ptr := uintptr(ptr) - _aligned := (_ptr + align_mask) &~ (align_mask) - return rawptr(_aligned) -} - -// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum. -@(require_results) -adjust_request_size :: proc(size, align: uint) -> (adjusted: uint) { - if size == 0 { - return 0 - } - - // aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`. - if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX { - adjusted = max(aligned, BLOCK_SIZE_MIN) - } - return -} - -// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum. -@(require_results) -adjust_request_size_with_err :: proc(size, align: uint) -> (adjusted: uint, err: runtime.Allocator_Error) { - if size == 0 { - return 0, nil - } - - // aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`. - if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX { - adjusted = min(aligned, BLOCK_SIZE_MAX) - } else { - err = .Out_Of_Memory - } - return -} - -// TLSF utility functions. In most cases these are direct translations of -// the documentation in the research paper. - -@(optimization_mode="favor_size", require_results) -mapping_insert :: proc(size: uint) -> (fl, sl: i32) { - if size < SMALL_BLOCK_SIZE { - // Store small blocks in first list. - sl = i32(size) / (SMALL_BLOCK_SIZE / SL_INDEX_COUNT) - } else { - fl = fls_uint(size) - sl = i32(size >> (uint(fl) - TLSF_SL_INDEX_COUNT_LOG2)) ~ (1 << TLSF_SL_INDEX_COUNT_LOG2) - fl -= (FL_INDEX_SHIFT - 1) - } - return -} - -@(optimization_mode="favor_size", require_results) -mapping_round :: #force_inline proc(size: uint) -> (rounded: uint) { - rounded = size - if size >= SMALL_BLOCK_SIZE { - round := uint(1 << (uint(fls_uint(size) - TLSF_SL_INDEX_COUNT_LOG2))) - 1 - rounded += round - } - return -} - -// This version rounds up to the next block size (for allocations) -@(optimization_mode="favor_size", require_results) -mapping_search :: proc(size: uint) -> (fl, sl: i32) { - return mapping_insert(mapping_round(size)) -} - -@(require_results) -search_suitable_block :: proc(control: ^Allocator, fli, sli: ^i32) -> (block: ^Block_Header) { - // First, search for a block in the list associated with the given fl/sl index. - fl := fli^; sl := sli^ - - sl_map := control.sl_bitmap[fli^] & (~u32(0) << uint(sl)) - if sl_map == 0 { - // No block exists. Search in the next largest first-level list. - fl_map := control.fl_bitmap & (~u32(0) << uint(fl + 1)) - if fl_map == 0 { - // No free blocks available, memory has been exhausted. - return {} - } - - fl = ffs(fl_map) - fli^ = fl - sl_map = control.sl_bitmap[fl] - } - assert(sl_map != 0, "internal error - second level bitmap is null") - sl = ffs(sl_map) - sli^ = sl - - // Return the first block in the free list. - return control.blocks[fl][sl] -} - -// Remove a free block from the free list. -remove_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) { - prev := block.prev_free - next := block.next_free - assert(prev != nil, "prev_free can not be nil") - assert(next != nil, "next_free can not be nil") - next.prev_free = prev - prev.next_free = next - - // If this block is the head of the free list, set new head. - if control.blocks[fl][sl] == block { - control.blocks[fl][sl] = next - - // If the new head is nil, clear the bitmap - if next == &control.block_null { - control.sl_bitmap[fl] &~= (u32(1) << uint(sl)) - - // If the second bitmap is now empty, clear the fl bitmap - if control.sl_bitmap[fl] == 0 { - control.fl_bitmap &~= (u32(1) << uint(fl)) - } - } - } -} - -// Insert a free block into the free block list. -insert_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) { - current := control.blocks[fl][sl] - assert(current != nil, "free lists cannot have a nil entry") - assert(block != nil, "cannot insert a nil entry into the free list") - block.next_free = current - block.prev_free = &control.block_null - current.prev_free = block - - assert(block_to_ptr(block) == align_ptr(block_to_ptr(block), ALIGN_SIZE), "block not properly aligned") - - // Insert the new block at the head of the list, and mark the first- and second-level bitmaps appropriately. - control.blocks[fl][sl] = block - control.fl_bitmap |= (u32(1) << uint(fl)) - control.sl_bitmap[fl] |= (u32(1) << uint(sl)) -} - -// Remove a given block from the free list. -block_remove :: proc(control: ^Allocator, block: ^Block_Header) { - fl, sl := mapping_insert(block_size(block)) - remove_free_block(control, block, fl, sl) -} - -// Insert a given block into the free list. -block_insert :: proc(control: ^Allocator, block: ^Block_Header) { - fl, sl := mapping_insert(block_size(block)) - insert_free_block(control, block, fl, sl) -} - -@(require_results) -block_can_split :: proc(block: ^Block_Header, size: uint) -> (can_split: bool) { - return block_size(block) >= size_of(Block_Header) + size -} - -// Split a block into two, the second of which is free. -@(require_results) -block_split :: proc(block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) { - // Calculate the amount of space left in the remaining block. - remaining = offset_to_block(block_to_ptr(block), size - BLOCK_HEADER_OVERHEAD) - - remain_size := block_size(block) - (size + BLOCK_HEADER_OVERHEAD) - - assert(block_to_ptr(remaining) == align_ptr(block_to_ptr(remaining), ALIGN_SIZE), - "remaining block not aligned properly") - - assert(block_size(block) == remain_size + size + BLOCK_HEADER_OVERHEAD) - block_set_size(remaining, remain_size) - assert(block_size(remaining) >= BLOCK_SIZE_MIN, "block split with invalid size") - - block_set_size(block, size) - block_mark_as_free(remaining) - - return remaining -} - -// Absorb a free block's storage into an adjacent previous free block. -@(require_results) -block_absorb :: proc(prev: ^Block_Header, block: ^Block_Header) -> (absorbed: ^Block_Header) { - assert(!block_is_last(prev), "previous block can't be last") - // Note: Leaves flags untouched. - prev.size += block_size(block) + BLOCK_HEADER_OVERHEAD - _ = block_link_next(prev) - return prev -} - -// Merge a just-freed block with an adjacent previous free block. -@(require_results) -block_merge_prev :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) { - merged = block - if (block_is_prev_free(block)) { - prev := block_prev(block) - assert(prev != nil, "prev physical block can't be nil") - assert(block_is_free(prev), "prev block is not free though marked as such") - block_remove(control, prev) - merged = block_absorb(prev, block) - } - return merged -} - -// Merge a just-freed block with an adjacent free block. -@(require_results) -block_merge_next :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) { - merged = block - next := block_next(block) - assert(next != nil, "next physical block can't be nil") - - if (block_is_free(next)) { - assert(!block_is_last(block), "previous block can't be last") - block_remove(control, next) - merged = block_absorb(block, next) - } - return merged -} - -// Trim any trailing block space off the end of a free block, return to pool. -block_trim_free :: proc(control: ^Allocator, block: ^Block_Header, size: uint) { - assert(block_is_free(block), "block must be free") - if (block_can_split(block, size)) { - remaining_block := block_split(block, size) - _ = block_link_next(block) - block_set_prev_free(remaining_block) - block_insert(control, remaining_block) - } -} - -// Trim any trailing block space off the end of a used block, return to pool. -block_trim_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) { - assert(!block_is_free(block), "Block must be used") - if (block_can_split(block, size)) { - // If the next block is free, we must coalesce. - remaining_block := block_split(block, size) - block_set_prev_used(remaining_block) - - remaining_block = block_merge_next(control, remaining_block) - block_insert(control, remaining_block) - } -} - -// Trim leading block space, return to pool. -@(require_results) -block_trim_free_leading :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) { - remaining = block - if block_can_split(block, size) { - // We want the 2nd block. - remaining = block_split(block, size - BLOCK_HEADER_OVERHEAD) - block_set_prev_free(remaining) - - _ = block_link_next(block) - block_insert(control, block) - } - return remaining -} - -@(require_results) -block_locate_free :: proc(control: ^Allocator, size: uint) -> (block: ^Block_Header) { - fl, sl: i32 - if size != 0 { - fl, sl = mapping_search(size) - - /* - `mapping_search` can futz with the size, so for excessively large sizes it can sometimes wind up - with indices that are off the end of the block array. So, we protect against that here, - since this is the only call site of `mapping_search`. Note that we don't need to check `sl`, - as it comes from a modulo operation that guarantees it's always in range. - */ - if fl < FL_INDEX_COUNT { - block = search_suitable_block(control, &fl, &sl) - } - } - - if block != nil { - assert(block_size(block) >= size) - remove_free_block(control, block, fl, sl) - } - return block -} - -@(require_results) -block_prepare_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (res: []byte, err: runtime.Allocator_Error) { - if block != nil { - assert(size != 0, "Size must be non-zero") - block_trim_free(control, block, size) - block_mark_as_used(block) - res = ([^]byte)(block_to_ptr(block))[:size] - } - return -} - // Clear control structure and point all empty lists at the null block -clear :: proc(control: ^Allocator) { +@(private) +free_all :: proc(control: ^Allocator) -> (err: Error) { + // Clear internal structures control.block_null.next_free = &control.block_null control.block_null.prev_free = &control.block_null - control.fl_bitmap = 0 for i in 0.. (err: Error) { assert(uintptr(raw_data(pool)) % ALIGN_SIZE == 0, "Added memory must be aligned") @@ -574,9 +155,11 @@ pool_add :: proc(control: ^Allocator, pool: []u8) -> (err: Error) { block_set_size(next, 0) block_set_used(next) block_set_prev_free(next) + return } +@(private) pool_remove :: proc(control: ^Allocator, pool: []u8) { block := offset_to_block_backwards(raw_data(pool), BLOCK_HEADER_OVERHEAD) @@ -588,7 +171,7 @@ pool_remove :: proc(control: ^Allocator, pool: []u8) { remove_free_block(control, block, fl, sl) } -@(require_results) +@(private, require_results) alloc_bytes_non_zeroed :: proc(control: ^Allocator, size: uint, align: uint) -> (res: []byte, err: runtime.Allocator_Error) { assert(control != nil) adjust := adjust_request_size(size, ALIGN_SIZE) @@ -601,9 +184,56 @@ alloc_bytes_non_zeroed :: proc(control: ^Allocator, size: uint, align: uint) -> return nil, .Out_Of_Memory } - block := block_locate_free(control, aligned_size) + block := block_locate_free(control, aligned_size) if block == nil { - return nil, .Out_Of_Memory + // OOM: Couldn't find block of `aligned_size` bytes. + if control.new_pool_size > 0 && control.pool.allocator.procedure != nil { + // TLSF is configured to grow. + + /* + This implementation doesn't allow for out-of-band allocations to be passed through, as it's not designed to + track those. Nor is it able to signal those allocations then need to be freed on the backing allocator, + as opposed to regular allocations handled for you when you `destroy` the TLSF instance. + + So if we're asked for more than we're configured to grow by, we can fail with an OOM error early, without adding a new pool. + */ + if aligned_size > control.new_pool_size { + return nil, .Out_Of_Memory + } + + // Trying to allocate a new pool of `control.new_pool_size` bytes. + new_pool_buf := runtime.make_aligned([]byte, control.new_pool_size, ALIGN_SIZE, control.pool.allocator) or_return + + // Add new pool to control structure + if pool_add_err := pool_add(control, new_pool_buf); pool_add_err != .None { + delete(new_pool_buf, control.pool.allocator) + return nil, .Out_Of_Memory + } + + sanitizer.address_poison(new_pool_buf) + + // Allocate a new link in the `control.pool` tracking structure. + new_pool := new_clone(Pool{ + data = new_pool_buf, + allocator = control.pool.allocator, + next = nil, + }, control.pool.allocator) or_return + + p := &control.pool + for p.next != nil { + p = p.next + } + p.next = new_pool + + // Try again to find free block + block = block_locate_free(control, aligned_size) + if block == nil { + return nil, .Out_Of_Memory + } + } else { + // TLSF is non-growing. We're done. + return nil, .Out_Of_Memory + } } ptr := block_to_ptr(block) aligned := align_ptr(ptr, align) @@ -627,7 +257,7 @@ alloc_bytes_non_zeroed :: proc(control: ^Allocator, size: uint, align: uint) -> return block_prepare_used(control, block, adjust) } -@(require_results) +@(private, require_results, no_sanitize_address) alloc_bytes :: proc(control: ^Allocator, size: uint, align: uint) -> (res: []byte, err: runtime.Allocator_Error) { res, err = alloc_bytes_non_zeroed(control, size, align) if err == nil { @@ -646,6 +276,7 @@ free_with_size :: proc(control: ^Allocator, ptr: rawptr, size: uint) { block := block_from_ptr(ptr) assert(!block_is_free(block), "block already marked as free") // double free + sanitizer.address_poison(ptr, block.size) block_mark_as_free(block) block = block_merge_prev(control, block) block = block_merge_next(control, block) @@ -653,7 +284,7 @@ free_with_size :: proc(control: ^Allocator, ptr: rawptr, size: uint) { } -@(require_results) +@(private, require_results) resize :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, alignment: uint) -> (res: []byte, err: runtime.Allocator_Error) { assert(control != nil) if ptr != nil && new_size == 0 { @@ -689,6 +320,7 @@ resize :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, align block_trim_used(control, block, adjust) res = ([^]byte)(ptr)[:new_size] + sanitizer.address_unpoison(res) if min_size < new_size { to_zero := ([^]byte)(ptr)[min_size:new_size] @@ -697,7 +329,7 @@ resize :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, align return } -@(require_results) +@(private, require_results) resize_non_zeroed :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, alignment: uint) -> (res: []byte, err: runtime.Allocator_Error) { assert(control != nil) if ptr != nil && new_size == 0 { @@ -736,3 +368,427 @@ resize_non_zeroed :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: res = ([^]byte)(ptr)[:new_size] return } + +/* + TLSF achieves O(1) cost for `alloc` and `free` operations by limiting + the search for a free block to a free list of guaranteed size + adequate to fulfill the request, combined with efficient free list + queries using bitmasks and architecture-specific bit-manipulation + routines. + + NOTE: TLSF spec relies on ffs/fls returning a value in the range 0..31. +*/ + +@(private, require_results, no_sanitize_address) +block_size :: proc "contextless" (block: ^Block_Header) -> (size: uint) { + return block.size &~ (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE) +} + +@(private, no_sanitize_address) +block_set_size :: proc "contextless" (block: ^Block_Header, size: uint) { + old_size := block.size + block.size = size | (old_size & (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE)) +} + +@(private, require_results, no_sanitize_address) +block_is_last :: proc "contextless" (block: ^Block_Header) -> (is_last: bool) { + return block_size(block) == 0 +} + +@(private, require_results, no_sanitize_address) +block_is_free :: proc "contextless" (block: ^Block_Header) -> (is_free: bool) { + return (block.size & BLOCK_HEADER_FREE) == BLOCK_HEADER_FREE +} + +@(private, no_sanitize_address) +block_set_free :: proc "contextless" (block: ^Block_Header) { + block.size |= BLOCK_HEADER_FREE +} + +@(private, no_sanitize_address) +block_set_used :: proc "contextless" (block: ^Block_Header) { + block.size &~= BLOCK_HEADER_FREE +} + +@(private, require_results, no_sanitize_address) +block_is_prev_free :: proc "contextless" (block: ^Block_Header) -> (is_prev_free: bool) { + return (block.size & BLOCK_HEADER_PREV_FREE) == BLOCK_HEADER_PREV_FREE +} + +@(private, no_sanitize_address) +block_set_prev_free :: proc "contextless" (block: ^Block_Header) { + block.size |= BLOCK_HEADER_PREV_FREE +} + +@(private, no_sanitize_address) +block_set_prev_used :: proc "contextless" (block: ^Block_Header) { + block.size &~= BLOCK_HEADER_PREV_FREE +} + +@(private, require_results, no_sanitize_address) +block_from_ptr :: proc(ptr: rawptr) -> (block_ptr: ^Block_Header) { + return (^Block_Header)(uintptr(ptr) - BLOCK_START_OFFSET) +} + +@(private, require_results, no_sanitize_address) +block_to_ptr :: proc(block: ^Block_Header) -> (ptr: rawptr) { + return rawptr(uintptr(block) + BLOCK_START_OFFSET) +} + +// Return location of next block after block of given size. +@(private, require_results, no_sanitize_address) +offset_to_block :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) { + return (^Block_Header)(uintptr(ptr) + uintptr(size)) +} + +@(private, require_results, no_sanitize_address) +offset_to_block_backwards :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) { + return (^Block_Header)(uintptr(ptr) - uintptr(size)) +} + +// Return location of previous block. +@(private, require_results, no_sanitize_address) +block_prev :: proc(block: ^Block_Header) -> (prev: ^Block_Header) { + assert(block_is_prev_free(block), "previous block must be free") + + return block.prev_phys_block +} + +// Return location of next existing block. +@(private, require_results, no_sanitize_address) +block_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) { + return offset_to_block(block_to_ptr(block), block_size(block) - BLOCK_HEADER_OVERHEAD) +} + +// Link a new block with its physical neighbor, return the neighbor. +@(private, require_results, no_sanitize_address) +block_link_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) { + next = block_next(block) + next.prev_phys_block = block + return +} + +@(private, no_sanitize_address) +block_mark_as_free :: proc(block: ^Block_Header) { + // Link the block to the next block, first. + next := block_link_next(block) + block_set_prev_free(next) + block_set_free(block) +} + +@(private, no_sanitize_address) +block_mark_as_used :: proc(block: ^Block_Header, ) { + next := block_next(block) + block_set_prev_used(next) + block_set_used(block) +} + +@(private, require_results, no_sanitize_address) +align_up :: proc(x, align: uint) -> (aligned: uint) { + assert(0 == (align & (align - 1)), "must align to a power of two") + return (x + (align - 1)) &~ (align - 1) +} + +@(private, require_results, no_sanitize_address) +align_down :: proc(x, align: uint) -> (aligned: uint) { + assert(0 == (align & (align - 1)), "must align to a power of two") + return x - (x & (align - 1)) +} + +@(private, require_results, no_sanitize_address) +align_ptr :: proc(ptr: rawptr, align: uint) -> (aligned: rawptr) { + assert(0 == (align & (align - 1)), "must align to a power of two") + align_mask := uintptr(align) - 1 + _ptr := uintptr(ptr) + _aligned := (_ptr + align_mask) &~ (align_mask) + return rawptr(_aligned) +} + +// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum. +@(private, require_results, no_sanitize_address) +adjust_request_size :: proc(size, align: uint) -> (adjusted: uint) { + if size == 0 { + return 0 + } + + // aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`. + if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX { + adjusted = max(aligned, BLOCK_SIZE_MIN) + } + return +} + +// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum. +@(private, require_results, no_sanitize_address) +adjust_request_size_with_err :: proc(size, align: uint) -> (adjusted: uint, err: runtime.Allocator_Error) { + if size == 0 { + return 0, nil + } + + // aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`. + if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX { + adjusted = min(aligned, BLOCK_SIZE_MAX) + } else { + err = .Out_Of_Memory + } + return +} + +// TLSF utility functions. In most cases these are direct translations of +// the documentation in the research paper. + +@(optimization_mode="favor_size", private, require_results, no_sanitize_address) +mapping_insert :: proc(size: uint) -> (fl, sl: i32) { + if size < SMALL_BLOCK_SIZE { + // Store small blocks in first list. + sl = i32(size) / (SMALL_BLOCK_SIZE / SL_INDEX_COUNT) + } else { + fl = fls_uint(size) + sl = i32(size >> (uint(fl) - TLSF_SL_INDEX_COUNT_LOG2)) ~ (1 << TLSF_SL_INDEX_COUNT_LOG2) + fl -= (FL_INDEX_SHIFT - 1) + } + return +} + +@(optimization_mode="favor_size", private, require_results, no_sanitize_address) +mapping_round :: #force_inline proc(size: uint) -> (rounded: uint) { + rounded = size + if size >= SMALL_BLOCK_SIZE { + round := uint(1 << (uint(fls_uint(size) - TLSF_SL_INDEX_COUNT_LOG2))) - 1 + rounded += round + } + return +} + +// This version rounds up to the next block size (for allocations) +@(optimization_mode="favor_size", private, require_results, no_sanitize_address) +mapping_search :: proc(size: uint) -> (fl, sl: i32) { + return mapping_insert(mapping_round(size)) +} + +@(private, require_results, no_sanitize_address) +search_suitable_block :: proc(control: ^Allocator, fli, sli: ^i32) -> (block: ^Block_Header) { + // First, search for a block in the list associated with the given fl/sl index. + fl := fli^; sl := sli^ + + sl_map := control.sl_bitmap[fli^] & (~u32(0) << uint(sl)) + if sl_map == 0 { + // No block exists. Search in the next largest first-level list. + fl_map := control.fl_bitmap & (~u32(0) << uint(fl + 1)) + if fl_map == 0 { + // No free blocks available, memory has been exhausted. + return {} + } + + fl = ffs(fl_map) + fli^ = fl + sl_map = control.sl_bitmap[fl] + } + assert(sl_map != 0, "internal error - second level bitmap is null") + sl = ffs(sl_map) + sli^ = sl + + // Return the first block in the free list. + return control.blocks[fl][sl] +} + +// Remove a free block from the free list. +@(private, no_sanitize_address) +remove_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) { + prev := block.prev_free + next := block.next_free + assert(prev != nil, "prev_free can not be nil") + assert(next != nil, "next_free can not be nil") + next.prev_free = prev + prev.next_free = next + + // If this block is the head of the free list, set new head. + if control.blocks[fl][sl] == block { + control.blocks[fl][sl] = next + + // If the new head is nil, clear the bitmap + if next == &control.block_null { + control.sl_bitmap[fl] &~= (u32(1) << uint(sl)) + + // If the second bitmap is now empty, clear the fl bitmap + if control.sl_bitmap[fl] == 0 { + control.fl_bitmap &~= (u32(1) << uint(fl)) + } + } + } +} + +// Insert a free block into the free block list. +@(private, no_sanitize_address) +insert_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) { + current := control.blocks[fl][sl] + assert(current != nil, "free lists cannot have a nil entry") + assert(block != nil, "cannot insert a nil entry into the free list") + block.next_free = current + block.prev_free = &control.block_null + current.prev_free = block + + assert(block_to_ptr(block) == align_ptr(block_to_ptr(block), ALIGN_SIZE), "block not properly aligned") + + // Insert the new block at the head of the list, and mark the first- and second-level bitmaps appropriately. + control.blocks[fl][sl] = block + control.fl_bitmap |= (u32(1) << uint(fl)) + control.sl_bitmap[fl] |= (u32(1) << uint(sl)) +} + +// Remove a given block from the free list. +@(private, no_sanitize_address) +block_remove :: proc(control: ^Allocator, block: ^Block_Header) { + fl, sl := mapping_insert(block_size(block)) + remove_free_block(control, block, fl, sl) +} + +// Insert a given block into the free list. +@(private, no_sanitize_address) +block_insert :: proc(control: ^Allocator, block: ^Block_Header) { + fl, sl := mapping_insert(block_size(block)) + insert_free_block(control, block, fl, sl) +} + +@(private, require_results, no_sanitize_address) +block_can_split :: proc(block: ^Block_Header, size: uint) -> (can_split: bool) { + return block_size(block) >= size_of(Block_Header) + size +} + +// Split a block into two, the second of which is free. +@(private, require_results, no_sanitize_address) +block_split :: proc(block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) { + // Calculate the amount of space left in the remaining block. + remaining = offset_to_block(block_to_ptr(block), size - BLOCK_HEADER_OVERHEAD) + + remain_size := block_size(block) - (size + BLOCK_HEADER_OVERHEAD) + + assert(block_to_ptr(remaining) == align_ptr(block_to_ptr(remaining), ALIGN_SIZE), + "remaining block not aligned properly") + + assert(block_size(block) == remain_size + size + BLOCK_HEADER_OVERHEAD) + block_set_size(remaining, remain_size) + assert(block_size(remaining) >= BLOCK_SIZE_MIN, "block split with invalid size") + + block_set_size(block, size) + block_mark_as_free(remaining) + + return remaining +} + +// Absorb a free block's storage into an adjacent previous free block. +@(private, require_results, no_sanitize_address) +block_absorb :: proc(prev: ^Block_Header, block: ^Block_Header) -> (absorbed: ^Block_Header) { + assert(!block_is_last(prev), "previous block can't be last") + + // Note: Leaves flags untouched. + prev.size += block_size(block) + BLOCK_HEADER_OVERHEAD + _ = block_link_next(prev) + return prev +} + +// Merge a just-freed block with an adjacent previous free block. +@(private, require_results, no_sanitize_address) +block_merge_prev :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) { + merged = block + if (block_is_prev_free(block)) { + prev := block_prev(block) + assert(prev != nil, "prev physical block can't be nil") + assert(block_is_free(prev), "prev block is not free though marked as such") + block_remove(control, prev) + merged = block_absorb(prev, block) + } + return merged +} + +// Merge a just-freed block with an adjacent free block. +@(private, require_results, no_sanitize_address) +block_merge_next :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) { + merged = block + next := block_next(block) + assert(next != nil, "next physical block can't be nil") + + if (block_is_free(next)) { + assert(!block_is_last(block), "previous block can't be last") + block_remove(control, next) + merged = block_absorb(block, next) + } + return merged +} + +// Trim any trailing block space off the end of a free block, return to pool. +@(private, no_sanitize_address) +block_trim_free :: proc(control: ^Allocator, block: ^Block_Header, size: uint) { + assert(block_is_free(block), "block must be free") + if (block_can_split(block, size)) { + remaining_block := block_split(block, size) + _ = block_link_next(block) + block_set_prev_free(remaining_block) + block_insert(control, remaining_block) + } +} + +// Trim any trailing block space off the end of a used block, return to pool. +@(private, no_sanitize_address) +block_trim_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) { + assert(!block_is_free(block), "Block must be used") + if (block_can_split(block, size)) { + // If the next block is free, we must coalesce. + remaining_block := block_split(block, size) + block_set_prev_used(remaining_block) + + remaining_block = block_merge_next(control, remaining_block) + block_insert(control, remaining_block) + } +} + +// Trim leading block space, return to pool. +@(private, require_results, no_sanitize_address) +block_trim_free_leading :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) { + remaining = block + if block_can_split(block, size) { + // We want the 2nd block. + remaining = block_split(block, size - BLOCK_HEADER_OVERHEAD) + block_set_prev_free(remaining) + + _ = block_link_next(block) + block_insert(control, block) + } + return remaining +} + +@(private, require_results, no_sanitize_address) +block_locate_free :: proc(control: ^Allocator, size: uint) -> (block: ^Block_Header) { + fl, sl: i32 + if size != 0 { + fl, sl = mapping_search(size) + + /* + `mapping_search` can futz with the size, so for excessively large sizes it can sometimes wind up + with indices that are off the end of the block array. So, we protect against that here, + since this is the only call site of `mapping_search`. Note that we don't need to check `sl`, + as it comes from a modulo operation that guarantees it's always in range. + */ + if fl < FL_INDEX_COUNT { + block = search_suitable_block(control, &fl, &sl) + } + } + + if block != nil { + assert(block_size(block) >= size) + remove_free_block(control, block, fl, sl) + } + return block +} + +@(private, require_results, no_sanitize_address) +block_prepare_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (res: []byte, err: runtime.Allocator_Error) { + if block != nil { + assert(size != 0, "Size must be non-zero") + block_trim_free(control, block, size) + block_mark_as_used(block) + res = ([^]byte)(block_to_ptr(block))[:size] + sanitizer.address_unpoison(res) + } + return +} diff --git a/core/mem/tracking_allocator.odin b/core/mem/tracking_allocator.odin index 263a1ca63..2250b566a 100644 --- a/core/mem/tracking_allocator.odin +++ b/core/mem/tracking_allocator.odin @@ -64,6 +64,7 @@ This procedure initializes the tracking allocator `t` with a backing allocator specified with `backing_allocator`. The `internals_allocator` will used to allocate the tracked data. */ +@(no_sanitize_address) tracking_allocator_init :: proc(t: ^Tracking_Allocator, backing_allocator: Allocator, internals_allocator := context.allocator) { t.backing = backing_allocator t.allocation_map.allocator = internals_allocator @@ -77,6 +78,7 @@ tracking_allocator_init :: proc(t: ^Tracking_Allocator, backing_allocator: Alloc /* Destroy the tracking allocator. */ +@(no_sanitize_address) tracking_allocator_destroy :: proc(t: ^Tracking_Allocator) { delete(t.allocation_map) delete(t.bad_free_array) @@ -90,6 +92,7 @@ This procedure clears the tracked data from a tracking allocator. **Note**: This procedure clears only the current allocation data while keeping the totals intact. */ +@(no_sanitize_address) tracking_allocator_clear :: proc(t: ^Tracking_Allocator) { sync.mutex_lock(&t.mutex) clear(&t.allocation_map) @@ -103,6 +106,7 @@ Reset the tracking allocator. Reset all of a Tracking Allocator's allocation data back to zero. */ +@(no_sanitize_address) tracking_allocator_reset :: proc(t: ^Tracking_Allocator) { sync.mutex_lock(&t.mutex) clear(&t.allocation_map) @@ -124,6 +128,7 @@ Override Tracking_Allocator.bad_free_callback to have something else happen. For example, you can use tracking_allocator_bad_free_callback_add_to_array to return the tracking allocator to the old behavior, where the bad_free_array was used. */ +@(no_sanitize_address) tracking_allocator_bad_free_callback_panic :: proc(t: ^Tracking_Allocator, memory: rawptr, location: runtime.Source_Code_Location) { runtime.print_caller_location(location) runtime.print_string(" Tracking allocator error: Bad free of pointer ") @@ -136,6 +141,7 @@ tracking_allocator_bad_free_callback_panic :: proc(t: ^Tracking_Allocator, memor Alternative behavior for a bad free: Store in `bad_free_array`. If you use this, then you must make sure to check Tracking_Allocator.bad_free_array at some point. */ +@(no_sanitize_address) tracking_allocator_bad_free_callback_add_to_array :: proc(t: ^Tracking_Allocator, memory: rawptr, location: runtime.Source_Code_Location) { append(&t.bad_free_array, Tracking_Allocator_Bad_Free_Entry { memory = memory, @@ -175,7 +181,7 @@ Example: } } */ -@(require_results) +@(require_results, no_sanitize_address) tracking_allocator :: proc(data: ^Tracking_Allocator) -> Allocator { return Allocator{ data = data, @@ -183,6 +189,7 @@ tracking_allocator :: proc(data: ^Tracking_Allocator) -> Allocator { } } +@(no_sanitize_address) tracking_allocator_proc :: proc( allocator_data: rawptr, mode: Allocator_Mode, @@ -191,6 +198,7 @@ tracking_allocator_proc :: proc( old_size: int, loc := #caller_location, ) -> (result: []byte, err: Allocator_Error) { + @(no_sanitize_address) track_alloc :: proc(data: ^Tracking_Allocator, entry: ^Tracking_Allocator_Entry) { data.total_memory_allocated += i64(entry.size) data.total_allocation_count += 1 @@ -200,6 +208,7 @@ tracking_allocator_proc :: proc( } } + @(no_sanitize_address) track_free :: proc(data: ^Tracking_Allocator, entry: ^Tracking_Allocator_Entry) { data.total_memory_freed += i64(entry.size) data.total_free_count += 1 diff --git a/core/mem/virtual/arena.odin b/core/mem/virtual/arena.odin index 4fc2e0e35..4e1cc2466 100644 --- a/core/mem/virtual/arena.odin +++ b/core/mem/virtual/arena.odin @@ -3,6 +3,8 @@ package mem_virtual import "core:mem" import "core:sync" +import "base:sanitizer" + Arena_Kind :: enum uint { Growing = 0, // Chained memory blocks (singly linked list). Static = 1, // Fixed reservation sized. @@ -43,7 +45,7 @@ DEFAULT_ARENA_STATIC_RESERVE_SIZE :: mem.Gigabyte when size_of(uintptr) == 8 els // Initialization of an `Arena` to be a `.Growing` variant. // A growing arena is a linked list of `Memory_Block`s allocated with virtual memory. -@(require_results) +@(require_results, no_sanitize_address) arena_init_growing :: proc(arena: ^Arena, reserved: uint = DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE) -> (err: Allocator_Error) { arena.kind = .Growing arena.curr_block = memory_block_alloc(0, reserved, {}) or_return @@ -53,24 +55,26 @@ arena_init_growing :: proc(arena: ^Arena, reserved: uint = DEFAULT_ARENA_GROWING if arena.minimum_block_size == 0 { arena.minimum_block_size = reserved } + sanitizer.address_poison(arena.curr_block.base[:arena.curr_block.committed]) return } // Initialization of an `Arena` to be a `.Static` variant. // A static arena contains a single `Memory_Block` allocated with virtual memory. -@(require_results) +@(require_results, no_sanitize_address) arena_init_static :: proc(arena: ^Arena, reserved: uint = DEFAULT_ARENA_STATIC_RESERVE_SIZE, commit_size: uint = DEFAULT_ARENA_STATIC_COMMIT_SIZE) -> (err: Allocator_Error) { arena.kind = .Static arena.curr_block = memory_block_alloc(commit_size, reserved, {}) or_return arena.total_used = 0 arena.total_reserved = arena.curr_block.reserved + sanitizer.address_poison(arena.curr_block.base[:arena.curr_block.committed]) return } // Initialization of an `Arena` to be a `.Buffer` variant. // A buffer arena contains single `Memory_Block` created from a user provided []byte. -@(require_results) +@(require_results, no_sanitize_address) arena_init_buffer :: proc(arena: ^Arena, buffer: []byte) -> (err: Allocator_Error) { if len(buffer) < size_of(Memory_Block) { return .Out_Of_Memory @@ -78,7 +82,7 @@ arena_init_buffer :: proc(arena: ^Arena, buffer: []byte) -> (err: Allocator_Erro arena.kind = .Buffer - mem.zero_slice(buffer) + sanitizer.address_poison(buffer[:]) block_base := raw_data(buffer) block := (^Memory_Block)(block_base) @@ -94,7 +98,7 @@ arena_init_buffer :: proc(arena: ^Arena, buffer: []byte) -> (err: Allocator_Erro } // Allocates memory from the provided arena. -@(require_results) +@(require_results, no_sanitize_address) arena_alloc :: proc(arena: ^Arena, size: uint, alignment: uint, loc := #caller_location) -> (data: []byte, err: Allocator_Error) { assert(alignment & (alignment-1) == 0, "non-power of two alignment", loc) @@ -158,10 +162,13 @@ arena_alloc :: proc(arena: ^Arena, size: uint, alignment: uint, loc := #caller_l data, err = alloc_from_memory_block(arena.curr_block, size, alignment, default_commit_size=0) arena.total_used = arena.curr_block.used } + + sanitizer.address_unpoison(data) return } // Resets the memory of a Static or Buffer arena to a specific `position` (offset) and zeroes the previously used memory. +@(no_sanitize_address) arena_static_reset_to :: proc(arena: ^Arena, pos: uint, loc := #caller_location) -> bool { sync.mutex_guard(&arena.mutex) @@ -175,6 +182,7 @@ arena_static_reset_to :: proc(arena: ^Arena, pos: uint, loc := #caller_location) mem.zero_slice(arena.curr_block.base[arena.curr_block.used:][:prev_pos-pos]) } arena.total_used = arena.curr_block.used + sanitizer.address_poison(arena.curr_block.base[:arena.curr_block.committed]) return true } else if pos == 0 { arena.total_used = 0 @@ -184,6 +192,7 @@ arena_static_reset_to :: proc(arena: ^Arena, pos: uint, loc := #caller_location) } // Frees the last memory block of a Growing Arena +@(no_sanitize_address) arena_growing_free_last_memory_block :: proc(arena: ^Arena, loc := #caller_location) { if free_block := arena.curr_block; free_block != nil { assert(arena.kind == .Growing, "expected a .Growing arena", loc) @@ -191,11 +200,13 @@ arena_growing_free_last_memory_block :: proc(arena: ^Arena, loc := #caller_locat arena.total_reserved -= free_block.reserved arena.curr_block = free_block.prev + sanitizer.address_poison(free_block.base[:free_block.committed]) memory_block_dealloc(free_block) } } // Deallocates all but the first memory block of the arena and resets the allocator's usage to 0. +@(no_sanitize_address) arena_free_all :: proc(arena: ^Arena, loc := #caller_location) { switch arena.kind { case .Growing: @@ -208,7 +219,9 @@ arena_free_all :: proc(arena: ^Arena, loc := #caller_location) { if arena.curr_block != nil { curr_block_used := int(arena.curr_block.used) arena.curr_block.used = 0 + sanitizer.address_unpoison(arena.curr_block.base[:curr_block_used]) mem.zero(arena.curr_block.base, curr_block_used) + sanitizer.address_poison(arena.curr_block.base[:arena.curr_block.committed]) } arena.total_used = 0 case .Static, .Buffer: @@ -219,6 +232,7 @@ arena_free_all :: proc(arena: ^Arena, loc := #caller_location) { // Frees all of the memory allocated by the arena and zeros all of the values of an arena. // A buffer based arena does not `delete` the provided `[]byte` bufffer. +@(no_sanitize_address) arena_destroy :: proc(arena: ^Arena, loc := #caller_location) { sync.mutex_guard(&arena.mutex) switch arena.kind { @@ -250,7 +264,7 @@ arena_static_bootstrap_new :: proc{ } // Ability to bootstrap allocate a struct with an arena within the struct itself using the growing variant strategy. -@(require_results) +@(require_results, no_sanitize_address) arena_growing_bootstrap_new_by_offset :: proc($T: typeid, offset_to_arena: uintptr, minimum_block_size: uint = DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE) -> (ptr: ^T, err: Allocator_Error) { bootstrap: Arena bootstrap.kind = .Growing @@ -266,13 +280,13 @@ arena_growing_bootstrap_new_by_offset :: proc($T: typeid, offset_to_arena: uintp } // Ability to bootstrap allocate a struct with an arena within the struct itself using the growing variant strategy. -@(require_results) +@(require_results, no_sanitize_address) arena_growing_bootstrap_new_by_name :: proc($T: typeid, $field_name: string, minimum_block_size: uint = DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE) -> (ptr: ^T, err: Allocator_Error) { return arena_growing_bootstrap_new_by_offset(T, offset_of_by_string(T, field_name), minimum_block_size) } // Ability to bootstrap allocate a struct with an arena within the struct itself using the static variant strategy. -@(require_results) +@(require_results, no_sanitize_address) arena_static_bootstrap_new_by_offset :: proc($T: typeid, offset_to_arena: uintptr, reserved: uint) -> (ptr: ^T, err: Allocator_Error) { bootstrap: Arena bootstrap.kind = .Static @@ -288,19 +302,20 @@ arena_static_bootstrap_new_by_offset :: proc($T: typeid, offset_to_arena: uintpt } // Ability to bootstrap allocate a struct with an arena within the struct itself using the static variant strategy. -@(require_results) +@(require_results, no_sanitize_address) arena_static_bootstrap_new_by_name :: proc($T: typeid, $field_name: string, reserved: uint) -> (ptr: ^T, err: Allocator_Error) { return arena_static_bootstrap_new_by_offset(T, offset_of_by_string(T, field_name), reserved) } // Create an `Allocator` from the provided `Arena` -@(require_results) +@(require_results, no_sanitize_address) arena_allocator :: proc(arena: ^Arena) -> mem.Allocator { return mem.Allocator{arena_allocator_proc, arena} } // The allocator procedure used by an `Allocator` produced by `arena_allocator` +@(no_sanitize_address) arena_allocator_proc :: proc(allocator_data: rawptr, mode: mem.Allocator_Mode, size, alignment: int, old_memory: rawptr, old_size: int, @@ -334,6 +349,7 @@ arena_allocator_proc :: proc(allocator_data: rawptr, mode: mem.Allocator_Mode, if size < old_size { // shrink data in-place data = old_data[:size] + sanitizer.address_poison(old_data[size:old_size]) return } @@ -347,6 +363,7 @@ arena_allocator_proc :: proc(allocator_data: rawptr, mode: mem.Allocator_Mode, _ = alloc_from_memory_block(block, new_end - old_end, 1, default_commit_size=arena.default_commit_size) or_return arena.total_used += block.used - prev_used data = block.base[start:new_end] + sanitizer.address_unpoison(data) return } } @@ -357,6 +374,7 @@ arena_allocator_proc :: proc(allocator_data: rawptr, mode: mem.Allocator_Mode, return } copy(new_memory, old_data[:old_size]) + sanitizer.address_poison(old_data[:old_size]) return new_memory, nil case .Query_Features: set := (^mem.Allocator_Mode_Set)(old_memory) @@ -382,7 +400,7 @@ Arena_Temp :: struct { } // Begins the section of temporary arena memory. -@(require_results) +@(require_results, no_sanitize_address) arena_temp_begin :: proc(arena: ^Arena, loc := #caller_location) -> (temp: Arena_Temp) { assert(arena != nil, "nil arena", loc) sync.mutex_guard(&arena.mutex) @@ -397,6 +415,7 @@ arena_temp_begin :: proc(arena: ^Arena, loc := #caller_location) -> (temp: Arena } // Ends the section of temporary arena memory by resetting the memory to the stored position. +@(no_sanitize_address) arena_temp_end :: proc(temp: Arena_Temp, loc := #caller_location) { assert(temp.arena != nil, "nil arena", loc) arena := temp.arena @@ -432,6 +451,7 @@ arena_temp_end :: proc(temp: Arena_Temp, loc := #caller_location) { } // Ignore the use of a `arena_temp_begin` entirely by __not__ resetting to the stored position. +@(no_sanitize_address) arena_temp_ignore :: proc(temp: Arena_Temp, loc := #caller_location) { assert(temp.arena != nil, "nil arena", loc) arena := temp.arena @@ -442,6 +462,7 @@ arena_temp_ignore :: proc(temp: Arena_Temp, loc := #caller_location) { } // Asserts that all uses of `Arena_Temp` has been used by an `Arena` +@(no_sanitize_address) arena_check_temp :: proc(arena: ^Arena, loc := #caller_location) { assert(arena.temp_count == 0, "Arena_Temp not been ended", loc) } diff --git a/core/mem/virtual/virtual.odin b/core/mem/virtual/virtual.odin index 4afc33813..031fb721a 100644 --- a/core/mem/virtual/virtual.odin +++ b/core/mem/virtual/virtual.odin @@ -2,6 +2,7 @@ package mem_virtual import "core:mem" import "base:intrinsics" +import "base:sanitizer" import "base:runtime" _ :: runtime @@ -14,27 +15,33 @@ platform_memory_init :: proc() { Allocator_Error :: mem.Allocator_Error -@(require_results) +@(require_results, no_sanitize_address) reserve :: proc "contextless" (size: uint) -> (data: []byte, err: Allocator_Error) { return _reserve(size) } +@(no_sanitize_address) commit :: proc "contextless" (data: rawptr, size: uint) -> Allocator_Error { + sanitizer.address_unpoison(data, size) return _commit(data, size) } -@(require_results) +@(require_results, no_sanitize_address) reserve_and_commit :: proc "contextless" (size: uint) -> (data: []byte, err: Allocator_Error) { data = reserve(size) or_return commit(raw_data(data), size) or_return return } +@(no_sanitize_address) decommit :: proc "contextless" (data: rawptr, size: uint) { + sanitizer.address_poison(data, size) _decommit(data, size) } +@(no_sanitize_address) release :: proc "contextless" (data: rawptr, size: uint) { + sanitizer.address_unpoison(data, size) _release(data, size) } @@ -46,13 +53,11 @@ Protect_Flag :: enum u32 { Protect_Flags :: distinct bit_set[Protect_Flag; u32] Protect_No_Access :: Protect_Flags{} +@(no_sanitize_address) protect :: proc "contextless" (data: rawptr, size: uint, flags: Protect_Flags) -> bool { return _protect(data, size, flags) } - - - Memory_Block :: struct { prev: ^Memory_Block, base: [^]byte, @@ -66,13 +71,13 @@ Memory_Block_Flag :: enum u32 { Memory_Block_Flags :: distinct bit_set[Memory_Block_Flag; u32] -@(private="file", require_results) +@(private="file", require_results, no_sanitize_address) align_formula :: #force_inline proc "contextless" (size, align: uint) -> uint { result := size + align-1 return result - result%align } -@(require_results) +@(require_results, no_sanitize_address) memory_block_alloc :: proc(committed, reserved: uint, alignment: uint = 0, flags: Memory_Block_Flags = {}) -> (block: ^Memory_Block, err: Allocator_Error) { page_size := DEFAULT_PAGE_SIZE assert(mem.is_power_of_two(uintptr(page_size))) @@ -116,8 +121,9 @@ memory_block_alloc :: proc(committed, reserved: uint, alignment: uint = 0, flags return &pmblock.block, nil } -@(require_results) +@(require_results, no_sanitize_address) alloc_from_memory_block :: proc(block: ^Memory_Block, min_size, alignment: uint, default_commit_size: uint = 0) -> (data: []byte, err: Allocator_Error) { + @(no_sanitize_address) calc_alignment_offset :: proc "contextless" (block: ^Memory_Block, alignment: uintptr) -> uint { alignment_offset := uint(0) ptr := uintptr(block.base[block.used:]) @@ -128,6 +134,7 @@ alloc_from_memory_block :: proc(block: ^Memory_Block, min_size, alignment: uint, return alignment_offset } + @(no_sanitize_address) do_commit_if_necessary :: proc(block: ^Memory_Block, size: uint, default_commit_size: uint) -> (err: Allocator_Error) { if block.committed - block.used < size { pmblock := (^Platform_Memory_Block)(block) @@ -172,10 +179,12 @@ alloc_from_memory_block :: proc(block: ^Memory_Block, min_size, alignment: uint, data = block.base[block.used+alignment_offset:][:min_size] block.used += size + sanitizer.address_unpoison(data) return } +@(no_sanitize_address) memory_block_dealloc :: proc(block_to_free: ^Memory_Block) { if block := (^Platform_Memory_Block)(block_to_free); block != nil { platform_memory_free(block) diff --git a/core/mem/virtual/virtual_platform.odin b/core/mem/virtual/virtual_platform.odin index 31e9cfca8..c9dde4e9d 100644 --- a/core/mem/virtual/virtual_platform.odin +++ b/core/mem/virtual/virtual_platform.odin @@ -7,6 +7,7 @@ Platform_Memory_Block :: struct { reserved: uint, } +@(no_sanitize_address) platform_memory_alloc :: proc "contextless" (to_commit, to_reserve: uint) -> (block: ^Platform_Memory_Block, err: Allocator_Error) { to_commit, to_reserve := to_commit, to_reserve to_reserve = max(to_commit, to_reserve) @@ -26,12 +27,14 @@ platform_memory_alloc :: proc "contextless" (to_commit, to_reserve: uint) -> (bl } +@(no_sanitize_address) platform_memory_free :: proc "contextless" (block: ^Platform_Memory_Block) { if block != nil { release(block, block.reserved) } } +@(no_sanitize_address) platform_memory_commit :: proc "contextless" (block: ^Platform_Memory_Block, to_commit: uint) -> (err: Allocator_Error) { if to_commit < block.committed { return nil diff --git a/core/mem/virtual/virtual_windows.odin b/core/mem/virtual/virtual_windows.odin index acd30ae33..0da8498d5 100644 --- a/core/mem/virtual/virtual_windows.odin +++ b/core/mem/virtual/virtual_windows.odin @@ -83,6 +83,8 @@ foreign Kernel32 { dwNumberOfBytesToMap: uint, ) -> rawptr --- } + +@(no_sanitize_address) _reserve :: proc "contextless" (size: uint) -> (data: []byte, err: Allocator_Error) { result := VirtualAlloc(nil, size, MEM_RESERVE, PAGE_READWRITE) if result == nil { @@ -93,6 +95,7 @@ _reserve :: proc "contextless" (size: uint) -> (data: []byte, err: Allocator_Err return } +@(no_sanitize_address) _commit :: proc "contextless" (data: rawptr, size: uint) -> Allocator_Error { result := VirtualAlloc(data, size, MEM_COMMIT, PAGE_READWRITE) if result == nil { @@ -107,12 +110,18 @@ _commit :: proc "contextless" (data: rawptr, size: uint) -> Allocator_Error { } return nil } + +@(no_sanitize_address) _decommit :: proc "contextless" (data: rawptr, size: uint) { VirtualFree(data, size, MEM_DECOMMIT) } + +@(no_sanitize_address) _release :: proc "contextless" (data: rawptr, size: uint) { VirtualFree(data, 0, MEM_RELEASE) } + +@(no_sanitize_address) _protect :: proc "contextless" (data: rawptr, size: uint, flags: Protect_Flags) -> bool { pflags: u32 pflags = PAGE_NOACCESS @@ -136,7 +145,7 @@ _protect :: proc "contextless" (data: rawptr, size: uint, flags: Protect_Flags) } - +@(no_sanitize_address) _platform_memory_init :: proc() { sys_info: SYSTEM_INFO GetSystemInfo(&sys_info) @@ -147,6 +156,7 @@ _platform_memory_init :: proc() { } +@(no_sanitize_address) _map_file :: proc "contextless" (fd: uintptr, size: i64, flags: Map_File_Flags) -> (data: []byte, error: Map_File_Error) { page_flags: u32 if flags == {.Read} { diff --git a/core/net/common.odin b/core/net/common.odin index 12add8225..fb6b17511 100644 --- a/core/net/common.odin +++ b/core/net/common.odin @@ -53,8 +53,6 @@ ODIN_NET_TCP_NODELAY_DEFAULT :: #config(ODIN_NET_TCP_NODELAY_DEFAULT, true) Maybe :: runtime.Maybe Network_Error :: union #shared_nil { - General_Error, - Platform_Error, Create_Socket_Error, Dial_Error, Listen_Error, @@ -65,6 +63,7 @@ Network_Error :: union #shared_nil { TCP_Recv_Error, UDP_Recv_Error, Shutdown_Error, + Interfaces_Error, Socket_Option_Error, Set_Blocking_Error, Parse_Endpoint_Error, @@ -74,14 +73,13 @@ Network_Error :: union #shared_nil { #assert(size_of(Network_Error) == 8) -General_Error :: enum u32 { - None = 0, - Unable_To_Enumerate_Network_Interfaces = 1, +Interfaces_Error :: enum u32 { + None, + Unable_To_Enumerate_Network_Interfaces, + Allocation_Failure, + Unknown, } -// `Platform_Error` is used to wrap errors returned by the different platforms that don't fit a common error. -Platform_Error :: enum u32 {} - Parse_Endpoint_Error :: enum u32 { None = 0, Bad_Port = 1, @@ -109,7 +107,7 @@ TCP_Options :: struct { no_delay: bool, } -default_tcp_options := TCP_Options { +DEFAULT_TCP_OPTIONS :: TCP_Options { no_delay = ODIN_NET_TCP_NODELAY_DEFAULT, } diff --git a/core/net/errors.odin b/core/net/errors.odin new file mode 100644 index 000000000..53c936a66 --- /dev/null +++ b/core/net/errors.odin @@ -0,0 +1,275 @@ +package net + +/* +Retrieve a platform specific error code, for when the categorized cross-platform errors are not enough. + +Platforms specific returns: +- Darwin: `posix.Errno` (`core:sys/posix`) +- Linux: `linux.Errno` (`core:sys/linux`) +- FreeBSD: `freebsd.Errno` (`core:sys/freebsd`) +- Windows: `windows.System_Error` (`core:sys/windows`) +*/ +@(require_results) +last_platform_error :: proc() -> i32 { + return _last_platform_error() +} + +/* +Retrieve a stringified version of the last platform error. +*/ +@(require_results) +last_platform_error_string :: proc() -> string { + return _last_platform_error_string() +} + +set_last_platform_error :: proc(err: i32) { + _set_last_platform_error(err) +} + +Create_Socket_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid/unsupported family or protocol. + Invalid_Argument, + // The user has no permission to create a socket of this type and/or protocol. + Insufficient_Permissions, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Dial_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid endpoint and/or options. + Invalid_Argument, + // An attempt was made to connect to a broadcast socket on a socket that doesn't support it. + Broadcast_Not_Supported, + // The socket is already connected. + Already_Connected, + // The socket is already in the progress of making a connection. + Already_Connecting, + // The address is already in use. + Address_In_Use, + // Could not reach the remote host. + Host_Unreachable, + // The remote host refused the connection or isn't listening. + Refused, + // The connection was reset by the remote host. + Reset, + // Timed out before making a connection. + Timeout, + // Non-blocking socket that would need to block waiting to connect. + Would_Block, + // Interrupted by a signal or other method of cancellation like WSACancelBlockingCall on Windows. + Interrupted, + // Endpoint given without a port, which is required. + Port_Required, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Bind_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid socket or endpoint, or invalid combination of the two. + Invalid_Argument, + // The socket is already bound to an address. + Already_Bound, + // The address is protected and the current user has insufficient permissions to access it. + Insufficient_Permissions_For_Address, + // The address is already in use. + Address_In_Use, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Listen_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // The socket or backlog is invalid. + Invalid_Argument, + // The socket is valid, but does not support listening. + Unsupported_Socket, + // The socket is already connected. + Already_Connected, + // The address is already in use. + Address_In_Use, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Accept_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid socket, or options. + Invalid_Argument, + // The given socket does not support accepting connections. + Unsupported_Socket, + // accept called on a socket which is not listening. + Not_Listening, + // A connection arrived but was closed while in the listen queue. + Aborted, + // Timed out before being able to accept a connection. + Timeout, + // Non-blocking socket that would need to block waiting for a connection. + Would_Block, + // Interrupted by a signal or other method of cancellation like WSACancelBlockingCall on Windows. + Interrupted, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +TCP_Recv_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid socket or buffer given. + Invalid_Argument, + // The socket is not connected. + Not_Connected, + // Connection was closed/broken/shutdown while receiving data. + Connection_Closed, + // Timed out before being able to receive any data. + Timeout, + // Non-blocking socket that would need to block waiting on data. + Would_Block, + // Interrupted by a signal or other method of cancellation like WSACancelBlockingCall on Windows. + Interrupted, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +UDP_Recv_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid socket or buffer given. + Invalid_Argument, + // "Connection" was refused by remote, or closed/broken/shutdown while receiving data. + Connection_Refused, + // Timed out before being able to receive any data. + Timeout, + // Non-blocking socket that would need to block waiting on data. + Would_Block, + // Interrupted by a signal or other method of cancellation like WSACancelBlockingCall on Windows. + Interrupted, + // Linux and UDP only: indicates the buffer was too small to receive all data, and the excess is truncated and discarded. + Excess_Truncated, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +TCP_Send_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid socket or buffer given. + Invalid_Argument, + // Connection was closed/broken/shutdown while receiving data. + Connection_Closed, + // The socket is not connected. + Not_Connected, + // Could not reach the remote host. + Host_Unreachable, + // Timed out before being able to send any data. + Timeout, + // Non-blocking socket that would need to block waiting on the remote to be able to receive the data. + Would_Block, + // Interrupted by a signal or other method of cancellation like WSACancelBlockingCall on Windows. + Interrupted, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +UDP_Send_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Invalid socket or buffer given. + Invalid_Argument, + // Could not reach the remote host. + Host_Unreachable, + // "Connection" was refused by remote, or closed/broken/shutdown while sending data. + Connection_Refused, + // Timed out before being able to send any data. + Timeout, + // Non-blocking socket that would need to block waiting on the remote to be able to receive the data. + Would_Block, + // Interrupted by a signal or other method of cancellation like WSACancelBlockingCall on Windows. + Interrupted, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Shutdown_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Socket is invalid or not connected, or the manner given is invalid. + Invalid_Argument, + // Connection was closed/aborted/shutdown. + Connection_Closed, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Socket_Option_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Not enough space in internal tables/buffers to create a new socket, or an unsupported protocol is given. + Insufficient_Resources, + // Socket is invalid, not connected, or the connection has been closed/reset/shutdown. + Invalid_Socket, + // Unknown or unsupported option for the socket. + Invalid_Option, + // Invalid level or value. + Invalid_Value, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} + +Set_Blocking_Error :: enum i32 { + None, + // No network connection, or the network stack is not initialized. + Network_Unreachable, + // Socket is invalid. + Invalid_Argument, + + // An error unable to be categorized in above categories, `last_platform_error` may have more info. + Unknown, +} diff --git a/core/net/errors_darwin.odin b/core/net/errors_darwin.odin index ccf1e0f7f..7aaa220e9 100644 --- a/core/net/errors_darwin.odin +++ b/core/net/errors_darwin.odin @@ -20,192 +20,232 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" +import "core:reflect" import "core:sys/posix" -@(private) -ESHUTDOWN :: 58 - -Create_Socket_Error :: enum c.int { - None = 0, - Family_Not_Supported_For_This_Socket = c.int(posix.EAFNOSUPPORT), - No_Socket_Descriptors_Available = c.int(posix.EMFILE), - No_Buffer_Space_Available = c.int(posix.ENOBUFS), - No_Memory_Available = c.int(posix.ENOMEM), - Protocol_Unsupported_By_System = c.int(posix.EPROTONOSUPPORT), - Wrong_Protocol_For_Socket = c.int(posix.EPROTONOSUPPORT), - Family_And_Socket_Type_Mismatch = c.int(posix.EPROTONOSUPPORT), +_last_platform_error :: proc() -> i32 { + return i32(posix.errno()) } -Dial_Error :: enum c.int { - None = 0, - Port_Required = -1, // Attempted to dial an endpointing without a port being set. - - Address_In_Use = c.int(posix.EADDRINUSE), - In_Progress = c.int(posix.EINPROGRESS), - Cannot_Use_Any_Address = c.int(posix.EADDRNOTAVAIL), - Wrong_Family_For_Socket = c.int(posix.EAFNOSUPPORT), - Refused = c.int(posix.ECONNREFUSED), - Is_Listening_Socket = c.int(posix.EACCES), - Already_Connected = c.int(posix.EISCONN), - Network_Unreachable = c.int(posix.ENETUNREACH), // Device is offline - Host_Unreachable = c.int(posix.EHOSTUNREACH), // Remote host cannot be reached - No_Buffer_Space_Available = c.int(posix.ENOBUFS), - Not_Socket = c.int(posix.ENOTSOCK), - Timeout = c.int(posix.ETIMEDOUT), - - // TODO: we may need special handling for this; maybe make a socket a struct with metadata? - Would_Block = c.int(posix.EWOULDBLOCK), +_last_platform_error_string :: proc() -> string { + description, _ := reflect.enum_name_from_value(posix.errno()) + return description } -Bind_Error :: enum c.int { - None = 0, - Privileged_Port_Without_Root = -1, // Attempted to bind to a port less than 1024 without root access. - - Address_In_Use = c.int(posix.EADDRINUSE), // Another application is currently bound to this endpoint. - Given_Nonlocal_Address = c.int(posix.EADDRNOTAVAIL), // The address is not a local address on this machine. - Broadcast_Disabled = c.int(posix.EACCES), // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. - Address_Family_Mismatch = c.int(posix.EFAULT), // The address family of the address does not match that of the socket. - Already_Bound = c.int(posix.EINVAL), // The socket is already bound to an address. - No_Ports_Available = c.int(posix.ENOBUFS), // There are not enough ephemeral ports available. +_set_last_platform_error :: proc(err: i32) { + posix.errno(posix.Errno(err)) } -Listen_Error :: enum c.int { - None = 0, - Address_In_Use = c.int(posix.EADDRINUSE), - Already_Connected = c.int(posix.EISCONN), - No_Socket_Descriptors_Available = c.int(posix.EMFILE), - No_Buffer_Space_Available = c.int(posix.ENOBUFS), - Nonlocal_Address = c.int(posix.EADDRNOTAVAIL), - Not_Socket = c.int(posix.ENOTSOCK), - Listening_Not_Supported_For_This_Socket = c.int(posix.EOPNOTSUPP), +_create_socket_error :: proc() -> Create_Socket_Error { + #partial switch posix.errno() { + case .EMFILE, .ENOBUFS, .ENOMEM, .EPROTONOSUPPORT, .EISCONN, .ENFILE: + return .Insufficient_Resources + case .EAFNOSUPPORT, .EPROTOTYPE: + return .Invalid_Argument + case .EACCES: + return .Insufficient_Permissions + case: + return .Unknown + } } -Accept_Error :: enum c.int { - None = 0, - // TODO(tetra): Is this error actually possible here? Or is like Linux, in which case we can remove it. - Reset = c.int(posix.ECONNRESET), - Not_Listening = c.int(posix.EINVAL), - No_Socket_Descriptors_Available_For_Client_Socket = c.int(posix.EMFILE), - No_Buffer_Space_Available = c.int(posix.ENOBUFS), - Not_Socket = c.int(posix.ENOTSOCK), - Not_Connection_Oriented_Socket = c.int(posix.EOPNOTSUPP), - - // TODO: we may need special handling for this; maybe make a socket a struct with metadata? - Would_Block = c.int(posix.EWOULDBLOCK), +_dial_error :: proc() -> Dial_Error { + #partial switch posix.errno() { + case .ENOBUFS: + return .Insufficient_Resources + case .EAFNOSUPPORT, .EBADF, .EFAULT, .EINVAL, .ENOTSOCK, .EPROTOTYPE, .EADDRNOTAVAIL: + return .Invalid_Argument + case .EISCONN: + return .Already_Connected + case .EALREADY: + return .Already_Connecting + case .EADDRINUSE: + return .Address_In_Use + case .ENETDOWN: + return .Network_Unreachable + case .EHOSTUNREACH: + return .Host_Unreachable + case .ECONNREFUSED: + return .Refused + case .ECONNRESET: + return .Reset + case .ETIMEDOUT: + return .Timeout + case .EINPROGRESS: + return .Would_Block + case .EINTR: + return .Interrupted + case .EACCES: + return .Broadcast_Not_Supported + case: + return .Unknown + } } -TCP_Recv_Error :: enum c.int { - None = 0, - Shutdown = ESHUTDOWN, - Not_Connected = c.int(posix.ENOTCONN), - - // TODO(tetra): Is this error actually possible here? - Connection_Broken = c.int(posix.ENETRESET), - Not_Socket = c.int(posix.ENOTSOCK), - Aborted = c.int(posix.ECONNABORTED), - - // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? - Connection_Closed = c.int(posix.ECONNRESET), - Offline = c.int(posix.ENETDOWN), - Host_Unreachable = c.int(posix.EHOSTUNREACH), - Interrupted = c.int(posix.EINTR), - - // NOTE: No, really. Presumably this means something different for nonblocking sockets... - Timeout = c.int(posix.EWOULDBLOCK), +_bind_error :: proc() -> Bind_Error { + #partial switch posix.errno() { + case .EADDRNOTAVAIL, .EAFNOSUPPORT, .EBADF, .EDESTADDRREQ, .EFAULT, .ENOTSOCK, .EOPNOTSUPP: + return .Invalid_Argument + case .EINVAL: + return .Already_Bound + case .EACCES: + return .Insufficient_Permissions_For_Address + case .EADDRINUSE: + return .Address_In_Use + case: + return .Unknown + } } -UDP_Recv_Error :: enum c.int { - None = 0, - Buffer_Too_Small = c.int(posix.EMSGSIZE), // The buffer is too small to fit the entire message, and the message was truncated. When this happens, the rest of message is lost. - Not_Socket = c.int(posix.ENOTSOCK), // The so-called socket is not an open socket. - Not_Descriptor = c.int(posix.EBADF), // The so-called socket is, in fact, not even a valid descriptor. - Bad_Buffer = c.int(posix.EFAULT), // The buffer did not point to a valid location in memory. - Interrupted = c.int(posix.EINTR), // A signal occurred before any data was transmitted. See signal(7). - - // The send timeout duration passed before all data was sent. See Socket_Option.Send_Timeout. - // NOTE: No, really. Presumably this means something different for nonblocking sockets... - Timeout = c.int(posix.EWOULDBLOCK), - Socket_Not_Bound = c.int(posix.EINVAL), // The socket must be bound for this operation, but isn't. +_listen_error :: proc() -> Listen_Error { + #partial switch posix.errno() { + case .EBADF, .ENOTSOCK: + return .Invalid_Argument + case .EDESTADDRREQ, .EOPNOTSUPP: + return .Unsupported_Socket + case .EINVAL: + return .Already_Connected + case: + return .Unknown + } } -TCP_Send_Error :: enum c.int { - None = 0, - - Aborted = c.int(posix.ECONNABORTED), - Connection_Closed = c.int(posix.ECONNRESET), - Not_Connected = c.int(posix.ENOTCONN), - Shutdown = ESHUTDOWN, - - // The send queue was full. - // This is usually a transient issue. - // - // This also shouldn't normally happen on Linux, as data is dropped if it - // doesn't fit in the send queue. - No_Buffer_Space_Available = c.int(posix.ENOBUFS), - Offline = c.int(posix.ENETDOWN), - Host_Unreachable = c.int(posix.EHOSTUNREACH), - Interrupted = c.int(posix.EINTR), // A signal occurred before any data was transmitted. See signal(7). - - // NOTE: No, really. Presumably this means something different for nonblocking sockets... - // The send timeout duration passed before all data was sent. See Socket_Option.Send_Timeout. - Timeout = c.int(posix.EWOULDBLOCK), - Not_Socket = c.int(posix.ENOTSOCK), // The so-called socket is not an open socket. +_accept_error :: proc() -> Accept_Error { + #partial switch posix.errno() { + case .EMFILE, .ENFILE, .ENOMEM: + return .Insufficient_Resources + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .EOPNOTSUPP: + return .Unsupported_Socket + case .ECONNABORTED: + return .Aborted + case .EWOULDBLOCK: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -// TODO -UDP_Send_Error :: enum c.int { - None = 0, - Message_Too_Long = c.int(posix.EMSGSIZE), // The message is larger than the maximum UDP packet size. No data was sent. - - // TODO: not sure what the exact circumstances for this is yet - Network_Unreachable = c.int(posix.ENETUNREACH), - No_Outbound_Ports_Available = c.int(posix.EAGAIN), // There are no more emphemeral outbound ports available to bind the socket to, in order to send. - - // The send timeout duration passed before all data was sent. See Socket_Option.Send_Timeout. - // NOTE: No, really. Presumably this means something different for nonblocking sockets... - Timeout = c.int(posix.EWOULDBLOCK), - Not_Socket = c.int(posix.ENOTSOCK), // The so-called socket is not an open socket. - Not_Descriptor = c.int(posix.EBADF), // The so-called socket is, in fact, not even a valid descriptor. - Bad_Buffer = c.int(posix.EFAULT), // The buffer did not point to a valid location in memory. - Interrupted = c.int(posix.EINTR), // A signal occurred before any data was transmitted. See signal(7). - - // The send queue was full. - // This is usually a transient issue. - // - // This also shouldn't normally happen on Linux, as data is dropped if it - // doesn't fit in the send queue. - No_Buffer_Space_Available = c.int(posix.ENOBUFS), - No_Memory_Available = c.int(posix.ENOMEM), // No memory was available to properly manage the send queue. +_tcp_recv_error :: proc() -> TCP_Recv_Error { + #partial switch posix.errno() { + case .EBADF, .EFAULT, .EINVAL, .ENOTSOCK, .EOPNOTSUPP: + return .Invalid_Argument + case .ENOBUFS: + return .Insufficient_Resources + case .ENOTCONN: + return .Not_Connected + case .ECONNRESET: + return .Connection_Closed + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Shutdown_Manner :: enum c.int { - Receive = c.int(posix.SHUT_RD), - Send = c.int(posix.SHUT_WR), - Both = c.int(posix.SHUT_RDWR), +_udp_recv_error :: proc() -> UDP_Recv_Error { + #partial switch posix.errno() { + case .EBADF, .EFAULT, .EINVAL, .ENOTSOCK, .EOPNOTSUPP, .EMSGSIZE: + return .Invalid_Argument + case .ENOBUFS, .ENOMEM: + return .Insufficient_Resources + case .ECONNRESET, .ENOTCONN: + return .Connection_Refused + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Shutdown_Error :: enum c.int { - None = 0, - Aborted = c.int(posix.ECONNABORTED), - Reset = c.int(posix.ECONNRESET), - Offline = c.int(posix.ENETDOWN), - Not_Connected = c.int(posix.ENOTCONN), - Not_Socket = c.int(posix.ENOTSOCK), - Invalid_Manner = c.int(posix.EINVAL), +_tcp_send_error :: proc() -> TCP_Send_Error { + #partial switch posix.errno() { + case .EACCES, .EBADF, .EFAULT, .EMSGSIZE, .ENOTSOCK, .EOPNOTSUPP: + return .Invalid_Argument + case .ENOBUFS: + return .Insufficient_Resources + case .ECONNRESET, .EPIPE: + return .Connection_Closed + case .ENOTCONN: + return .Not_Connected + case .EHOSTUNREACH: + return .Host_Unreachable + case .ENETDOWN, .ENETUNREACH: + return .Network_Unreachable + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Socket_Option_Error :: enum c.int { - None = 0, - Offline = c.int(posix.ENETDOWN), - Timeout_When_Keepalive_Set = c.int(posix.ENETRESET), - Invalid_Option_For_Socket = c.int(posix.ENOPROTOOPT), - Reset_When_Keepalive_Set = c.int(posix.ENOTCONN), - Not_Socket = c.int(posix.ENOTSOCK), +_udp_send_error :: proc() -> UDP_Send_Error { + #partial switch posix.errno() { + case .EACCES, .EBADF, .EFAULT, .EMSGSIZE, .ENOTSOCK, .EOPNOTSUPP, .EAFNOSUPPORT, .EDESTADDRREQ: + return .Invalid_Argument + case .ENOBUFS, .ENOMEM: + return .Insufficient_Resources + case .ECONNRESET, .EPIPE: + return .Connection_Refused + case .EHOSTUNREACH: + return .Host_Unreachable + case .ENETDOWN, .ENETUNREACH: + return .Network_Unreachable + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Set_Blocking_Error :: enum c.int { - None = 0, - - // TODO: Add errors for `set_blocking` +_shutdown_error :: proc() -> Shutdown_Error { + #partial switch posix.errno() { + case .EBADF, .EINVAL, .ENOTSOCK, .ENOTCONN: + return .Invalid_Argument + case: + return .Unknown + } +} + +_socket_option_error :: proc() -> Socket_Option_Error { + #partial switch posix.errno() { + case .ENOBUFS: + return .Insufficient_Resources + case .EBADF, .ENOTSOCK, .EISCONN: + return .Invalid_Socket + case .EINVAL, .ENOPROTOOPT: + return .Invalid_Option + case .EFAULT, .EDOM: + return .Invalid_Value + case: + return .Unknown + } +} + +_set_blocking_error :: proc() -> Set_Blocking_Error { + #partial switch posix.errno() { + case .EBADF: + return .Invalid_Argument + case: + return .Unknown + } } diff --git a/core/net/errors_freebsd.odin b/core/net/errors_freebsd.odin index 486732a95..707ffd0dd 100644 --- a/core/net/errors_freebsd.odin +++ b/core/net/errors_freebsd.odin @@ -20,198 +20,267 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" +import "core:reflect" import "core:sys/freebsd" -Create_Socket_Error :: enum c.int { - None = 0, - Access_Denied = cast(c.int)freebsd.Errno.EACCES, - Family_Not_Supported_For_This_Socket = cast(c.int)freebsd.Errno.EAFNOSUPPORT, - Full_Per_Process_Descriptor_Table = cast(c.int)freebsd.Errno.EMFILE, - Full_System_File_Table = cast(c.int)freebsd.Errno.ENFILE, - No_Buffer_Space_Available = cast(c.int)freebsd.Errno.ENOBUFS, - Insufficient_Permission = cast(c.int)freebsd.Errno.EPERM, - Protocol_Unsupported_In_Family = cast(c.int)freebsd.Errno.EPROTONOSUPPORT, - Socket_Type_Unsupported_By_Protocol = cast(c.int)freebsd.Errno.EPROTOTYPE, +@(private="file", thread_local) +_last_error: freebsd.Errno + +_last_platform_error :: proc() -> i32 { + return i32(_last_error) } -Dial_Error :: enum c.int { - None = 0, - Port_Required = -1, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Invalid_Namelen = cast(c.int)freebsd.Errno.EINVAL, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, - Address_Unavailable = cast(c.int)freebsd.Errno.EADDRNOTAVAIL, - Wrong_Family_For_Socket = cast(c.int)freebsd.Errno.EAFNOSUPPORT, - Already_Connected = cast(c.int)freebsd.Errno.EISCONN, - Timeout = cast(c.int)freebsd.Errno.ETIMEDOUT, - Refused_By_Remote_Host = cast(c.int)freebsd.Errno.ECONNREFUSED, - // `Refused` alias for `core:net` tests. - // The above default name `Refused_By_Remote_Host` is more explicit. - Refused = Refused_By_Remote_Host, - Reset_By_Remote_Host = cast(c.int)freebsd.Errno.ECONNRESET, - Network_Unreachable = cast(c.int)freebsd.Errno.ENETUNREACH, - Host_Unreachable = cast(c.int)freebsd.Errno.EHOSTUNREACH, - Address_In_Use = cast(c.int)freebsd.Errno.EADDRINUSE, - Invalid_Address_Space = cast(c.int)freebsd.Errno.EFAULT, - In_Progress = cast(c.int)freebsd.Errno.EINPROGRESS, - Interrupted_By_Signal = cast(c.int)freebsd.Errno.EINTR, - Previous_Attempt_Incomplete = cast(c.int)freebsd.Errno.EALREADY, - Broadcast_Unavailable = cast(c.int)freebsd.Errno.EACCES, - Auto_Port_Unavailable = cast(c.int)freebsd.Errno.EAGAIN, - - // NOTE: There are additional connect() error possibilities, but they are - // strictly for addresses in the UNIX domain. +_last_platform_error_string :: proc() -> string { + description, _ := reflect.enum_name_from_value(_last_error) + return description } -Bind_Error :: enum c.int { - None = 0, - Kernel_Resources_Unavailable = cast(c.int)freebsd.Errno.EAGAIN, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - - // NOTE: bind() can also return EINVAL if the underlying `addrlen` is an - // invalid length for the address family. This shouldn't happen for the net - // package, but it's worth noting. - Already_Bound = cast(c.int)freebsd.Errno.EINVAL, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, - Given_Nonlocal_Address = cast(c.int)freebsd.Errno.EADDRNOTAVAIL, - Address_In_Use = cast(c.int)freebsd.Errno.EADDRINUSE, - Address_Family_Mismatch = cast(c.int)freebsd.Errno.EAFNOSUPPORT, - Protected_Address = cast(c.int)freebsd.Errno.EACCES, - Invalid_Address_Space = cast(c.int)freebsd.Errno.EFAULT, - - // NOTE: There are additional bind() error possibilities, but they are - // strictly for addresses in the UNIX domain. +_set_last_platform_error :: proc(err: i32) { + _last_error = freebsd.Errno(err) } -Listen_Error :: enum c.int { - None = 0, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Socket_Not_Bound = cast(c.int)freebsd.Errno.EDESTADDRREQ, - Already_Connected = cast(c.int)freebsd.Errno.EINVAL, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, - Listening_Not_Supported_For_This_Socket = cast(c.int)freebsd.Errno.EOPNOTSUPP, +_create_socket_error :: proc(errno: freebsd.Errno) -> Create_Socket_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EMFILE, .ENFILE, .ENOBUFS, .EPROTONOSUPPORT: + return .Insufficient_Resources + case .EAFNOSUPPORT, .EPROTOTYPE: + return .Invalid_Argument + case .EACCES, .EPERM: + return .Insufficient_Permissions + case: + return .Unknown + } } -Accept_Error :: enum c.int { - None = 0, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Interrupted = cast(c.int)freebsd.Errno.EINTR, - Full_Per_Process_Descriptor_Table = cast(c.int)freebsd.Errno.EMFILE, - Full_System_File_Table = cast(c.int)freebsd.Errno.ENFILE, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, - Listen_Not_Called_On_Socket_Yet = cast(c.int)freebsd.Errno.EINVAL, - Address_Not_Writable = cast(c.int)freebsd.Errno.EFAULT, +_dial_error :: proc(errno: freebsd.Errno) -> Dial_Error { + assert(errno != nil) + _last_error = errno - // NOTE: This is the same as EWOULDBLOCK. - No_Connections_Available = cast(c.int)freebsd.Errno.EAGAIN, - // `Would_Block` alias for `core:net` tests. - Would_Block = cast(c.int)freebsd.Errno.EAGAIN, - - New_Connection_Aborted = cast(c.int)freebsd.Errno.ECONNABORTED, + #partial switch errno { + case .EBADF, .EINVAL, .ENOTSOCK, .EADDRNOTAVAIL, .EAFNOSUPPORT, .EFAULT, .EAGAIN: + return .Invalid_Argument + case .EISCONN: + return .Already_Connected + case .EALREADY: + return .Already_Connecting + case .EADDRINUSE: + return .Address_In_Use + case .ENETUNREACH: + return .Network_Unreachable + case .EHOSTUNREACH: + return .Host_Unreachable + case .ECONNREFUSED: + return .Refused + case .ECONNRESET: + return .Reset + case .ETIMEDOUT: + return .Timeout + case .EINPROGRESS: + return .Would_Block + case .EINTR: + return .Interrupted + case .EACCES: + return .Broadcast_Not_Supported + case: + return .Unknown + } } -TCP_Recv_Error :: enum c.int { - None = 0, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Connection_Closed = cast(c.int)freebsd.Errno.ECONNRESET, - Not_Connected = cast(c.int)freebsd.Errno.ENOTCONN, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, +_bind_error :: proc(errno: freebsd.Errno) -> Bind_Error { + assert(errno != nil) + _last_error = errno - // NOTE(Feoramund): The next two errors are only relevant for recvmsg(), - // but I'm including them for completeness's sake. - Full_Table_And_Pending_Data = cast(c.int)freebsd.Errno.EMFILE, - Invalid_Message_Size = cast(c.int)freebsd.Errno.EMSGSIZE, - - Timeout = cast(c.int)freebsd.Errno.EAGAIN, - Interrupted_By_Signal = cast(c.int)freebsd.Errno.EINTR, - Buffer_Pointer_Outside_Address_Space = cast(c.int)freebsd.Errno.EFAULT, + #partial switch errno { + case .EAGAIN, .ENOTSOCK, .EADDRNOTAVAIL, .EAFNOSUPPORT, .EFAULT: + return .Insufficient_Resources + case .EBADF: + return .Invalid_Argument + case .EINVAL: + return .Already_Bound + case .EACCES: + return .Insufficient_Permissions_For_Address + case .EADDRINUSE: + return .Address_In_Use + case: + return .Unknown + } } -UDP_Recv_Error :: enum c.int { - None = 0, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Connection_Closed = cast(c.int)freebsd.Errno.ECONNRESET, - Not_Connected = cast(c.int)freebsd.Errno.ENOTCONN, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, +_listen_error :: proc(errno: freebsd.Errno) -> Listen_Error { + assert(errno != nil) + _last_error = errno - // NOTE(Feoramund): The next two errors are only relevant for recvmsg(), - // but I'm including them for completeness's sake. - Full_Table_And_Data_Discarded = cast(c.int)freebsd.Errno.EMFILE, - Invalid_Message_Size = cast(c.int)freebsd.Errno.EMSGSIZE, - - Timeout = cast(c.int)freebsd.Errno.EAGAIN, - Interrupted_By_Signal = cast(c.int)freebsd.Errno.EINTR, - Buffer_Pointer_Outside_Address_Space = cast(c.int)freebsd.Errno.EFAULT, + #partial switch errno { + case .EBADF, .ENOTSOCK: + return .Invalid_Argument + case .EDESTADDRREQ, .EOPNOTSUPP: + return .Unsupported_Socket + case .EINVAL: + return .Already_Connected + case: + return .Unknown + } } -TCP_Send_Error :: enum c.int { - None = 0, - Connection_Closed = cast(c.int)freebsd.Errno.ECONNRESET, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Broadcast_Status_Mismatch = cast(c.int)freebsd.Errno.EACCES, - Not_Connected = cast(c.int)freebsd.Errno.ENOTCONN, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, - Argument_In_Invalid_Address_Space = cast(c.int)freebsd.Errno.EFAULT, +_accept_error :: proc(errno: freebsd.Errno) -> Accept_Error { + assert(errno != nil) + _last_error = errno - Message_Size_Breaks_Atomicity = cast(c.int)freebsd.Errno.EMSGSIZE, - - /* The socket is marked non-blocking, or MSG_DONTWAIT is - specified, and the requested operation would block. */ - Would_Block = cast(c.int)freebsd.Errno.EAGAIN, - - /* NOTE: This error arises for two distinct reasons: - - 1. The system was unable to allocate an internal buffer. - The operation may succeed when buffers become available. - - 2. The output queue for a network interface was full. - This generally indicates that the interface has stopped - sending, but may be caused by transient congestion. - */ - No_Buffer_Space_Available = cast(c.int)freebsd.Errno.ENOBUFS, - - Host_Unreachable = cast(c.int)freebsd.Errno.EHOSTUNREACH, - Already_Connected = cast(c.int)freebsd.Errno.EISCONN, - ICMP_Unreachable = cast(c.int)freebsd.Errno.ECONNREFUSED, - Host_Down = cast(c.int)freebsd.Errno.EHOSTDOWN, - Network_Down = cast(c.int)freebsd.Errno.ENETDOWN, - Jailed_Socket_Tried_To_Escape = cast(c.int)freebsd.Errno.EADDRNOTAVAIL, - Cannot_Send_More_Data = cast(c.int)freebsd.Errno.EPIPE, + #partial switch errno { + case .EMFILE, .ENFILE: + return .Insufficient_Resources + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .EINVAL: + return .Not_Listening + case .ECONNABORTED: + return .Aborted + case .EWOULDBLOCK: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -// NOTE(Feoramund): The same as TCP errors go, as far as I'm aware. -UDP_Send_Error :: distinct TCP_Send_Error +_tcp_recv_error :: proc(errno: freebsd.Errno) -> TCP_Recv_Error { + assert(errno != nil) + _last_error = errno -Shutdown_Manner :: enum c.int { - Receive = cast(c.int)freebsd.Shutdown_Method.RD, - Send = cast(c.int)freebsd.Shutdown_Method.WR, - Both = cast(c.int)freebsd.Shutdown_Method.RDWR, + #partial switch errno { + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .ENOTCONN: + return .Not_Connected + case .ECONNRESET: + return .Connection_Closed + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Shutdown_Error :: enum c.int { - None = 0, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Invalid_Manner = cast(c.int)freebsd.Errno.EINVAL, - Not_Connected = cast(c.int)freebsd.Errno.ENOTCONN, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, +_udp_recv_error :: proc(errno: freebsd.Errno) -> UDP_Recv_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .ECONNRESET, .ENOTCONN: + return .Connection_Refused + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Socket_Option_Error :: enum c.int { - None = 0, - Value_Out_Of_Range = -1, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Not_Socket = cast(c.int)freebsd.Errno.ENOTSOCK, - Unknown_Option_For_Level = cast(c.int)freebsd.Errno.ENOPROTOOPT, - Argument_In_Invalid_Address_Space = cast(c.int)freebsd.Errno.EFAULT, - // This error can arise for many different reasons. - Invalid_Value = cast(c.int)freebsd.Errno.EINVAL, - System_Memory_Allocation_Failed = cast(c.int)freebsd.Errno.ENOMEM, - Insufficient_System_Resources = cast(c.int)freebsd.Errno.ENOBUFS, +_tcp_send_error :: proc(errno: freebsd.Errno) -> TCP_Send_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .EACCES, .ENOTSOCK, .EFAULT, .EMSGSIZE: + return .Invalid_Argument + case .ENOBUFS: + return .Insufficient_Resources + case .ECONNRESET, .EPIPE: + return .Connection_Closed + case .ENOTCONN: + return .Not_Connected + case .EHOSTUNREACH: + return .Host_Unreachable + case .EHOSTDOWN: + return .Host_Unreachable + case .ENETDOWN: + return .Network_Unreachable + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Set_Blocking_Error :: enum c.int { - None = 0, - Not_Descriptor = cast(c.int)freebsd.Errno.EBADF, - Wrong_Descriptor = cast(c.int)freebsd.Errno.ENOTTY, +_udp_send_error :: proc(errno: freebsd.Errno) -> UDP_Send_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .EACCES, .ENOTSOCK, .EFAULT, .EMSGSIZE: + return .Invalid_Argument + case .ENOBUFS: + return .Insufficient_Resources + case .ECONNRESET, .EPIPE: + return .Connection_Refused + case .EHOSTUNREACH: + return .Host_Unreachable + case .EHOSTDOWN: + return .Host_Unreachable + case .ENETDOWN: + return .Network_Unreachable + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } +} + +_shutdown_error :: proc(errno: freebsd.Errno) -> Shutdown_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .EINVAL, .ENOTSOCK, .ENOTCONN: + return .Invalid_Argument + case: + return .Unknown + } +} + +_socket_option_error :: proc(errno: freebsd.Errno) -> Socket_Option_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .ENOMEM, .ENOBUFS: + return .Insufficient_Resources + case .EBADF, .ENOTSOCK: + return .Invalid_Socket + case .ENOPROTOOPT: + return .Invalid_Option + case .EINVAL, .EFAULT: + return .Invalid_Value + case: + return .Unknown + } +} + +_set_blocking_error :: proc(errno: freebsd.Errno) -> Set_Blocking_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .ENOTTY: + return .Invalid_Argument + case: + return .Unknown + } } diff --git a/core/net/errors_linux.odin b/core/net/errors_linux.odin index 3cd51e6fd..237579f28 100644 --- a/core/net/errors_linux.odin +++ b/core/net/errors_linux.odin @@ -21,181 +21,269 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" +import "core:reflect" import "core:sys/linux" -Create_Socket_Error :: enum c.int { - None = 0, - Family_Not_Supported_For_This_Socket = c.int(linux.Errno.EAFNOSUPPORT), - No_Socket_Descriptors_Available = c.int(linux.Errno.EMFILE), - No_Buffer_Space_Available = c.int(linux.Errno.ENOBUFS), - No_Memory_Available_Available = c.int(linux.Errno.ENOMEM), - Protocol_Unsupported_By_System = c.int(linux.Errno.EPROTONOSUPPORT), - Wrong_Protocol_For_Socket = c.int(linux.Errno.EPROTONOSUPPORT), - Family_And_Socket_Type_Mismatch = c.int(linux.Errno.EPROTONOSUPPORT), +@(private="file", thread_local) +_last_error: linux.Errno + +_last_platform_error :: proc() -> i32 { + return i32(_last_error) } -Dial_Error :: enum c.int { - None = 0, - Port_Required = -1, - - Address_In_Use = c.int(linux.Errno.EADDRINUSE), - In_Progress = c.int(linux.Errno.EINPROGRESS), - Cannot_Use_Any_Address = c.int(linux.Errno.EADDRNOTAVAIL), - Wrong_Family_For_Socket = c.int(linux.Errno.EAFNOSUPPORT), - Refused = c.int(linux.Errno.ECONNREFUSED), - Is_Listening_Socket = c.int(linux.Errno.EACCES), - Already_Connected = c.int(linux.Errno.EISCONN), - Network_Unreachable = c.int(linux.Errno.ENETUNREACH), // Device is offline - Host_Unreachable = c.int(linux.Errno.EHOSTUNREACH), // Remote host cannot be reached - No_Buffer_Space_Available = c.int(linux.Errno.ENOBUFS), - Not_Socket = c.int(linux.Errno.ENOTSOCK), - Timeout = c.int(linux.Errno.ETIMEDOUT), - - // TODO: we may need special handling for this; maybe make a socket a struct with metadata? - Would_Block = c.int(linux.Errno.EWOULDBLOCK), +_last_platform_error_string :: proc() -> string { + description, _ := reflect.enum_name_from_value(_last_error) + return description } -Bind_Error :: enum c.int { - None = 0, - Address_In_Use = c.int(linux.Errno.EADDRINUSE), // Another application is currently bound to this endpoint. - Given_Nonlocal_Address = c.int(linux.Errno.EADDRNOTAVAIL), // The address is not a local address on this machine. - Broadcast_Disabled = c.int(linux.Errno.EACCES), // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. - Address_Family_Mismatch = c.int(linux.Errno.EFAULT), // The address family of the address does not match that of the socket. - Already_Bound = c.int(linux.Errno.EINVAL), // The socket is already bound to an address. - No_Ports_Available = c.int(linux.Errno.ENOBUFS), // There are not enough ephemeral ports available. +_set_last_platform_error :: proc(err: i32) { + _last_error = linux.Errno(err) } -Listen_Error :: enum c.int { - None = 0, - Address_In_Use = c.int(linux.Errno.EADDRINUSE), - Already_Connected = c.int(linux.Errno.EISCONN), - No_Socket_Descriptors_Available = c.int(linux.Errno.EMFILE), - No_Buffer_Space_Available = c.int(linux.Errno.ENOBUFS), - Nonlocal_Address = c.int(linux.Errno.EADDRNOTAVAIL), - Not_Socket = c.int(linux.Errno.ENOTSOCK), - Listening_Not_Supported_For_This_Socket = c.int(linux.Errno.EOPNOTSUPP), +_create_socket_error :: proc(errno: linux.Errno) -> Create_Socket_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EMFILE, .ENFILE, .ENOBUFS, .EPROTONOSUPPORT: + return .Insufficient_Resources + case .EAFNOSUPPORT, .EPROTOTYPE: + return .Invalid_Argument + case .EACCES, .EPERM: + return .Insufficient_Permissions + case: + return .Unknown + } } -Accept_Error :: enum c.int { - None = 0, - Not_Listening = c.int(linux.Errno.EINVAL), - No_Socket_Descriptors_Available_For_Client_Socket = c.int(linux.Errno.EMFILE), - No_Buffer_Space_Available = c.int(linux.Errno.ENOBUFS), - Not_Socket = c.int(linux.Errno.ENOTSOCK), - Not_Connection_Oriented_Socket = c.int(linux.Errno.EOPNOTSUPP), +_dial_error :: proc(errno: linux.Errno) -> Dial_Error { + assert(errno != nil) + _last_error = errno - // TODO: we may need special handling for this; maybe make a socket a struct with metadata? - Would_Block = c.int(linux.Errno.EWOULDBLOCK), + #partial switch errno { + case .EAGAIN: + return .Insufficient_Resources + case .EBADF, .EINVAL, .ENOTSOCK, .EADDRNOTAVAIL, .EAFNOSUPPORT, .EFAULT: + return .Invalid_Argument + case .EISCONN: + return .Already_Connected + case .EALREADY: + return .Already_Connecting + case .EADDRINUSE: + return .Address_In_Use + case .ENETUNREACH: + return .Network_Unreachable + case .EHOSTUNREACH: + return .Host_Unreachable + case .ECONNREFUSED: + return .Refused + case .ECONNRESET: + return .Reset + case .ETIMEDOUT: + return .Timeout + case .EINPROGRESS: + return .Would_Block + case .EINTR: + return .Interrupted + case .EACCES: + return .Broadcast_Not_Supported + case: + return .Unknown + } } -TCP_Recv_Error :: enum c.int { - None = 0, - Shutdown = c.int(linux.Errno.ESHUTDOWN), - Not_Connected = c.int(linux.Errno.ENOTCONN), - Connection_Broken = c.int(linux.Errno.ENETRESET), - Not_Socket = c.int(linux.Errno.ENOTSOCK), - Aborted = c.int(linux.Errno.ECONNABORTED), +_bind_error :: proc(errno: linux.Errno) -> Bind_Error { + assert(errno != nil) + _last_error = errno - // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? - Connection_Closed = c.int(linux.Errno.ECONNRESET), - Offline = c.int(linux.Errno.ENETDOWN), - Host_Unreachable = c.int(linux.Errno.EHOSTUNREACH), - Interrupted = c.int(linux.Errno.EINTR), - Timeout = c.int(linux.Errno.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + #partial switch errno { + case .EAGAIN, .ENOTSOCK, .EADDRNOTAVAIL, .EAFNOSUPPORT, .EFAULT: + return .Insufficient_Resources + case .EINVAL: + return .Already_Bound + case .EBADF: + return .Invalid_Argument + case .EACCES: + return .Insufficient_Permissions_For_Address + case .EADDRINUSE: + return .Address_In_Use + case: + return .Unknown + } } -UDP_Recv_Error :: enum c.int { - None = 0, +_listen_error :: proc(errno: linux.Errno) -> Listen_Error { + assert(errno != nil) + _last_error = errno - Buffer_Too_Small = c.int(linux.Errno.EMSGSIZE), // The buffer is too small to fit the entire message, and the message was truncated. When this happens, the rest of message is lost. - Not_Socket = c.int(linux.Errno.ENOTSOCK), // The so-called socket is not an open socket. - Not_Descriptor = c.int(linux.Errno.EBADF), // The so-called socket is, in fact, not even a valid descriptor. - Bad_Buffer = c.int(linux.Errno.EFAULT), // The buffer did not point to a valid location in memory. - Interrupted = c.int(linux.Errno.EINTR), // A signal occurred before any data was transmitted. See signal(7). - - // The send timeout duration passed before all data was received. See Socket_Option.Receive_Timeout. - // NOTE: No, really. Presumably this means something different for nonblocking sockets... - Timeout = c.int(linux.Errno.EWOULDBLOCK), - Socket_Not_Bound = c.int(linux.Errno.EINVAL), // The socket must be bound for this operation, but isn't. + #partial switch errno { + case .EBADF, .ENOTSOCK: + return .Invalid_Argument + case .EDESTADDRREQ, .EOPNOTSUPP: + return .Unsupported_Socket + case .EINVAL: + return .Already_Connected + case: + return .Unknown + } } -TCP_Send_Error :: enum c.int { - None = 0, - Aborted = c.int(linux.Errno.ECONNABORTED), - Connection_Closed = c.int(linux.Errno.ECONNRESET), - Not_Connected = c.int(linux.Errno.ENOTCONN), - Shutdown = c.int(linux.Errno.ESHUTDOWN), +_accept_error :: proc(errno: linux.Errno) -> Accept_Error { + assert(errno != nil) + _last_error = errno - // The send queue was full. - // This is usually a transient issue. - // - // This also shouldn't normally happen on Linux, as data is dropped if it - // doesn't fit in the send queue. - No_Buffer_Space_Available = c.int(linux.Errno.ENOBUFS), - Offline = c.int(linux.Errno.ENETDOWN), - Host_Unreachable = c.int(linux.Errno.EHOSTUNREACH), - Interrupted = c.int(linux.Errno.EINTR), // A signal occurred before any data was transmitted. See signal(7). - Timeout = c.int(linux.Errno.EWOULDBLOCK), // The send timeout duration passed before all data was sent. See Socket_Option.Send_Timeout. - Not_Socket = c.int(linux.Errno.ENOTSOCK), // The so-called socket is not an open socket. + #partial switch errno { + case .EMFILE, .ENFILE, .ENOBUFS, .ENOMEM: + return .Insufficient_Resources + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .EINVAL: + return .Not_Listening + case .ECONNABORTED: + return .Aborted + case .EWOULDBLOCK: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -// TODO -UDP_Send_Error :: enum c.int { - None = 0, - Message_Too_Long = c.int(linux.Errno.EMSGSIZE), // The message is larger than the maximum UDP packet size. No data was sent. +_tcp_recv_error :: proc(errno: linux.Errno) -> TCP_Recv_Error { + assert(errno != nil) + _last_error = errno - // TODO: not sure what the exact circumstances for this is yet - Network_Unreachable = c.int(linux.Errno.ENETUNREACH), - No_Outbound_Ports_Available = c.int(linux.Errno.EAGAIN), // There are no more emphemeral outbound ports available to bind the socket to, in order to send. - - // The send timeout duration passed before all data was sent. See Socket_Option.Send_Timeout. - // NOTE: No, really. Presumably this means something different for nonblocking sockets... - Timeout = c.int(linux.Errno.EWOULDBLOCK), - Not_Socket = c.int(linux.Errno.ENOTSOCK), // The so-called socket is not an open socket. - Not_Descriptor = c.int(linux.Errno.EBADF), // The so-called socket is, in fact, not even a valid descriptor. - Bad_Buffer = c.int(linux.Errno.EFAULT), // The buffer did not point to a valid location in memory. - Interrupted = c.int(linux.Errno.EINTR), // A signal occurred before any data was transmitted. See signal(7). - - // The send queue was full. - // This is usually a transient issue. - // - // This also shouldn't normally happen on Linux, as data is dropped if it - // doesn't fit in the send queue. - No_Buffer_Space_Available = c.int(linux.Errno.ENOBUFS), - No_Memory_Available = c.int(linux.Errno.ENOMEM), // No memory was available to properly manage the send queue. + #partial switch errno { + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .ENOTCONN: + return .Not_Connected + case .ECONNREFUSED, .ECONNRESET: + return .Connection_Closed + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -// TODO(flysand): slight regression -Shutdown_Manner :: enum c.int { - Receive = c.int(linux.Shutdown_How.RD), - Send = c.int(linux.Shutdown_How.WR), - Both = c.int(linux.Shutdown_How.RDWR), +_udp_recv_error :: proc(errno: linux.Errno) -> UDP_Recv_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .ENOTSOCK, .EFAULT: + return .Invalid_Argument + case .ECONNREFUSED, .ENOTCONN, .ECONNRESET: + return .Connection_Refused + case .ETIMEDOUT: + return .Timeout + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Shutdown_Error :: enum c.int { - None = 0, - Aborted = c.int(linux.Errno.ECONNABORTED), - Reset = c.int(linux.Errno.ECONNRESET), - Offline = c.int(linux.Errno.ENETDOWN), - Not_Connected = c.int(linux.Errno.ENOTCONN), - Not_Socket = c.int(linux.Errno.ENOTSOCK), - Invalid_Manner = c.int(linux.Errno.EINVAL), +_tcp_send_error :: proc(errno: linux.Errno) -> TCP_Send_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .EACCES, .ENOTSOCK, .EFAULT, .EMSGSIZE, .EDESTADDRREQ, .EINVAL, .EISCONN, .EOPNOTSUPP: + return .Invalid_Argument + case .ENOBUFS, .ENOMEM: + return .Insufficient_Resources + case .ECONNRESET, .EPIPE: + return .Connection_Closed + case .ENOTCONN: + return .Not_Connected + case .EHOSTUNREACH: + return .Host_Unreachable + case .EHOSTDOWN: + return .Host_Unreachable + case .ENETDOWN: + return .Network_Unreachable + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Socket_Option_Error :: enum c.int { - None = 0, - Offline = c.int(linux.Errno.ENETDOWN), - Timeout_When_Keepalive_Set = c.int(linux.Errno.ENETRESET), - Invalid_Option_For_Socket = c.int(linux.Errno.ENOPROTOOPT), - Reset_When_Keepalive_Set = c.int(linux.Errno.ENOTCONN), - Not_Socket = c.int(linux.Errno.ENOTSOCK), +_udp_send_error :: proc(errno: linux.Errno) -> UDP_Send_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF, .EACCES, .ENOTSOCK, .EFAULT, .EMSGSIZE, .EDESTADDRREQ, .EINVAL, .EISCONN, .EOPNOTSUPP: + return .Invalid_Argument + case .ENOBUFS, .ENOMEM: + return .Insufficient_Resources + case .ECONNRESET, .EPIPE: + return .Connection_Refused + case .EHOSTUNREACH: + return .Host_Unreachable + case .EHOSTDOWN: + return .Host_Unreachable + case .ENETDOWN: + return .Network_Unreachable + case .EAGAIN: + return .Would_Block + case .EINTR: + return .Interrupted + case: + return .Unknown + } } -Set_Blocking_Error :: enum c.int { - None = 0, +_shutdown_error :: proc(errno: linux.Errno) -> Shutdown_Error { + assert(errno != nil) + _last_error = errno - // TODO: add errors occuring on followig calls: - // flags, _ := linux.Errno.fcntl(sd, linux.Errno.F_GETFL, 0) - // linux.Errno.fcntl(sd, linux.Errno.F_SETFL, flags | int(linux.Errno.O_NONBLOCK)) -} \ No newline at end of file + #partial switch errno { + case .EBADF, .EINVAL, .ENOTSOCK, .ENOTCONN: + return .Invalid_Argument + case: + return .Unknown + } +} + +_socket_option_error :: proc(errno: linux.Errno) -> Socket_Option_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .ENOMEM, .ENOBUFS: + return .Insufficient_Resources + case .EBADF, .ENOTSOCK: + return .Invalid_Socket + case .ENOPROTOOPT, .EINVAL: + return .Invalid_Option + case .EFAULT, .EDOM: + return .Invalid_Value + case: + return .Unknown + } +} + +_set_blocking_error :: proc(errno: linux.Errno) -> Set_Blocking_Error { + assert(errno != nil) + _last_error = errno + + #partial switch errno { + case .EBADF: + return .Invalid_Argument + case: + return .Unknown + } +} diff --git a/core/net/errors_others.odin b/core/net/errors_others.odin new file mode 100644 index 000000000..bda0fd28f --- /dev/null +++ b/core/net/errors_others.odin @@ -0,0 +1,20 @@ +#+build !darwin +#+build !linux +#+build !freebsd +#+build !windows +package net + +@(private="file", thread_local) +_last_error: i32 + +_last_platform_error :: proc() -> i32 { + return _last_error +} + +_last_platform_error_string :: proc() -> string { + return "" +} + +_set_last_platform_error :: proc(err: i32) { + _last_error = err +} diff --git a/core/net/errors_windows.odin b/core/net/errors_windows.odin index f41bcf888..b30046a17 100644 --- a/core/net/errors_windows.odin +++ b/core/net/errors_windows.odin @@ -20,250 +20,242 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" +import "core:reflect" import win "core:sys/windows" -Create_Socket_Error :: enum c.int { - None = 0, - Network_Subsystem_Failure = win.WSAENETDOWN, - Family_Not_Supported_For_This_Socket = win.WSAEAFNOSUPPORT, - No_Socket_Descriptors_Available = win.WSAEMFILE, - No_Buffer_Space_Available = win.WSAENOBUFS, - Protocol_Unsupported_By_System = win.WSAEPROTONOSUPPORT, - Wrong_Protocol_For_Socket = win.WSAEPROTOTYPE, - Family_And_Socket_Type_Mismatch = win.WSAESOCKTNOSUPPORT, +_last_platform_error :: proc() -> i32 { + return i32(win.WSAGetLastError()) } -Dial_Error :: enum c.int { - None = 0, - Port_Required = -1, - Address_In_Use = win.WSAEADDRINUSE, - In_Progress = win.WSAEALREADY, - Cannot_Use_Any_Address = win.WSAEADDRNOTAVAIL, - Wrong_Family_For_Socket = win.WSAEAFNOSUPPORT, - Refused = win.WSAECONNREFUSED, - Is_Listening_Socket = win.WSAEINVAL, - Already_Connected = win.WSAEISCONN, - Network_Unreachable = win.WSAENETUNREACH, // Device is offline - Host_Unreachable = win.WSAEHOSTUNREACH, // Remote host cannot be reached - No_Buffer_Space_Available = win.WSAENOBUFS, - Not_Socket = win.WSAENOTSOCK, - Timeout = win.WSAETIMEDOUT, - Would_Block = win.WSAEWOULDBLOCK, // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +_last_platform_error_string :: proc() -> string { + description, _ := reflect.enum_name_from_value(win.System_Error(win.WSAGetLastError())) + return description } -Bind_Error :: enum c.int { - None = 0, - Address_In_Use = win.WSAEADDRINUSE, // Another application is currently bound to this endpoint. - Given_Nonlocal_Address = win.WSAEADDRNOTAVAIL, // The address is not a local address on this machine. - Broadcast_Disabled = win.WSAEACCES, // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. - Address_Family_Mismatch = win.WSAEFAULT, // The address family of the address does not match that of the socket. - Already_Bound = win.WSAEINVAL, // The socket is already bound to an address. - No_Ports_Available = win.WSAENOBUFS, // There are not enough ephemeral ports available. +_set_last_platform_error :: proc(err: i32) { + win.WSASetLastError(err) } -Listen_Error :: enum c.int { - None = 0, - Address_In_Use = win.WSAEADDRINUSE, - Already_Connected = win.WSAEISCONN, - No_Socket_Descriptors_Available = win.WSAEMFILE, - No_Buffer_Space_Available = win.WSAENOBUFS, - Nonlocal_Address = win.WSAEADDRNOTAVAIL, - Not_Socket = win.WSAENOTSOCK, - Listening_Not_Supported_For_This_Socket = win.WSAEOPNOTSUPP, +_create_socket_error :: proc() -> Create_Socket_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN, .WSAEINVALIDPROVIDER, .WSAEINVALIDPROCTABLE, .WSAEPROVIDERFAILEDINIT: + return .Network_Unreachable + case .WSAEAFNOSUPPORT, .WSAEINPROGRESS, .WSAEINVAL, .WSAEPROTOTYPE, .WSAESOCKTNOSUPPORT: + return .Invalid_Argument + case .WSAEMFILE, .WSAENOBUFS, .WSAEPROTONOSUPPORT: + return .Insufficient_Resources + case: + return .Unknown + } } -Accept_Error :: enum c.int { - None = 0, - Not_Listening = win.WSAEINVAL, - No_Socket_Descriptors_Available_For_Client_Socket = win.WSAEMFILE, - No_Buffer_Space_Available = win.WSAENOBUFS, - Not_Socket = win.WSAENOTSOCK, - Not_Connection_Oriented_Socket = win.WSAEOPNOTSUPP, - - // TODO: we may need special handling for this; maybe make a socket a struct with metadata? - Would_Block = win.WSAEWOULDBLOCK, +_dial_error :: proc() -> Dial_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAEADDRINUSE: + return .Address_In_Use + case .WSAEINTR: + return .Interrupted + case .WSAEWOULDBLOCK: + return .Would_Block + case .WSAEALREADY: + return .Already_Connecting + case .WSAEADDRNOTAVAIL, .WSAEAFNOSUPPORT, .WSAEFAULT, .WSAENOTSOCK, .WSAEINPROGRESS, .WSAEINVAL: + return .Invalid_Argument + case .WSAECONNREFUSED: + return .Refused + case .WSAEISCONN: + return .Already_Connected + case .WSAEHOSTUNREACH: + return .Host_Unreachable + case .WSAENOBUFS: + return .Insufficient_Resources + case .WSAETIMEDOUT: + return .Timeout + case .WSAEACCES: + return .Broadcast_Not_Supported + case: + return .Unknown + } } -TCP_Recv_Error :: enum c.int { - None = 0, - Network_Subsystem_Failure = win.WSAENETDOWN, - Not_Connected = win.WSAENOTCONN, - Bad_Buffer = win.WSAEFAULT, - Keepalive_Failure = win.WSAENETRESET, - Not_Socket = win.WSAENOTSOCK, - Shutdown = win.WSAESHUTDOWN, - Would_Block = win.WSAEWOULDBLOCK, - Aborted = win.WSAECONNABORTED, - Timeout = win.WSAETIMEDOUT, - - // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? - Connection_Closed = win.WSAECONNRESET, - - // TODO: verify can actually happen - Host_Unreachable = win.WSAEHOSTUNREACH, +_bind_error :: proc() -> Bind_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAEADDRINUSE: + return .Address_In_Use + case .WSAEADDRNOTAVAIL, .WSAEFAULT, .WSAEINPROGRESS, .WSAEACCES, .WSAEINVAL, .WSAENOTSOCK: + return .Invalid_Argument + case: + return .Unknown + } } -UDP_Recv_Error :: enum c.int { - None = 0, - Network_Subsystem_Failure = win.WSAENETDOWN, - Aborted = win.WSAECONNABORTED, - Buffer_Too_Small = win.WSAEMSGSIZE, // The buffer is too small to fit the entire message, and the message was truncated. When this happens, the rest of message is lost. - Remote_Not_Listening = win.WSAECONNRESET, // The machine at the remote endpoint doesn't have the given port open to receiving UDP data. - Shutdown = win.WSAESHUTDOWN, - Broadcast_Disabled = win.WSAEACCES, // A broadcast address was specified, but the .Broadcast socket option isn't set. - Bad_Buffer = win.WSAEFAULT, - No_Buffer_Space_Available = win.WSAENOBUFS, - Not_Socket = win.WSAENOTSOCK, // The socket is not valid socket handle. - Would_Block = win.WSAEWOULDBLOCK, - Host_Unreachable = win.WSAEHOSTUNREACH, // The remote host cannot be reached from this host at this time. - Offline = win.WSAENETUNREACH, // The network cannot be reached from this host at this time. - Timeout = win.WSAETIMEDOUT, - - // TODO: can this actually happen? The socket isn't bound; an unknown flag specified; or MSG_OOB specified with SO_OOBINLINE enabled. - Incorrectly_Configured = win.WSAEINVAL, - TTL_Expired = win.WSAENETRESET, // The message took more hops than was allowed (the Time To Live) to reach the remote endpoint. +_listen_error :: proc() -> Listen_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAEMFILE, .WSAENOBUFS: + return .Insufficient_Resources + case .WSAEADDRINUSE: + return .Address_In_Use + case .WSAEINPROGRESS, .WSAENOTSOCK: + return .Invalid_Argument + case .WSAEISCONN: + return .Already_Connected + case .WSAEOPNOTSUPP, .WSAEINVAL: + return .Unsupported_Socket + case: + return .Unknown + } } -// TODO: consider merging some errors to make handling them easier -// TODO: verify once more what errors to actually expose -TCP_Send_Error :: enum c.int { - None = 0, - - Aborted = win.WSAECONNABORTED, - Not_Connected = win.WSAENOTCONN, - Shutdown = win.WSAESHUTDOWN, - Connection_Closed = win.WSAECONNRESET, - No_Buffer_Space_Available = win.WSAENOBUFS, - Network_Subsystem_Failure = win.WSAENETDOWN, - Host_Unreachable = win.WSAEHOSTUNREACH, - Would_Block = win.WSAEWOULDBLOCK, - - // TODO: verify possible, as not mentioned in docs - Offline = win.WSAENETUNREACH, - Timeout = win.WSAETIMEDOUT, - - // A broadcast address was specified, but the .Broadcast socket option isn't set. - Broadcast_Disabled = win.WSAEACCES, - Bad_Buffer = win.WSAEFAULT, - - // Connection is broken due to keepalive activity detecting a failure during the operation. - Keepalive_Failure = win.WSAENETRESET, // TODO: not functionally different from Reset; merge? - Not_Socket = win.WSAENOTSOCK, // The so-called socket is not an open socket. +_accept_error :: proc() -> Accept_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAEMFILE, .WSAENOBUFS: + return .Insufficient_Resources + case .WSAECONNRESET: + return .Aborted + case .WSAEFAULT, .WSAEINPROGRESS, .WSAENOTSOCK: + return .Invalid_Argument + case .WSAEINTR: + return .Interrupted + case .WSAEINVAL: + return .Not_Listening + case .WSAEWOULDBLOCK: + return .Would_Block + case .WSAEOPNOTSUPP: + return .Unsupported_Socket + case: + return .Unknown + } } -UDP_Send_Error :: enum c.int { - None = 0, - Network_Subsystem_Failure = win.WSAENETDOWN, - - Aborted = win.WSAECONNABORTED, - Message_Too_Long = win.WSAEMSGSIZE, // The message is larger than the maximum UDP packet size. - Remote_Not_Listening = win.WSAECONNRESET, // The machine at the remote endpoint doesn't have the given port open to receiving UDP data. - Shutdown = win.WSAESHUTDOWN, // A broadcast address was specified, but the .Broadcast socket option isn't set. - Broadcast_Disabled = win.WSAEACCES, - Bad_Buffer = win.WSAEFAULT, // Connection is broken due to keepalive activity detecting a failure during the operation. - - // TODO: not functionally different from Reset; merge? - Keepalive_Failure = win.WSAENETRESET, - No_Buffer_Space_Available = win.WSAENOBUFS, - Not_Socket = win.WSAENOTSOCK, // The socket is not valid socket handle. - - // This socket is unidirectional and cannot be used to send any data. - // TODO: verify possible; decide whether to keep if not - Receive_Only = win.WSAEOPNOTSUPP, - Would_Block = win.WSAEWOULDBLOCK, - Host_Unreachable = win.WSAEHOSTUNREACH, // The remote host cannot be reached from this host at this time. - Cannot_Use_Any_Address = win.WSAEADDRNOTAVAIL, // Attempt to send to the Any address. - Family_Not_Supported_For_This_Socket = win.WSAEAFNOSUPPORT, // The address is of an incorrect address family for this socket. - Offline = win.WSAENETUNREACH, // The network cannot be reached from this host at this time. - Timeout = win.WSAETIMEDOUT, +_tcp_recv_error :: proc() -> TCP_Recv_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAEFAULT, .WSAEINPROGRESS, .WSAENOTSOCK, .WSAEMSGSIZE, .WSAEINVAL, .WSAEOPNOTSUPP: + return .Invalid_Argument + case .WSAENOTCONN: + return .Not_Connected + case .WSAEINTR: + return .Interrupted + case .WSAENETRESET, .WSAESHUTDOWN, .WSAECONNABORTED, .WSAECONNRESET: + return .Connection_Closed + case .WSAEWOULDBLOCK: + return .Would_Block + case .WSAETIMEDOUT: + return .Timeout + case: + return .Unknown + } } -Shutdown_Manner :: enum c.int { - Receive = win.SD_RECEIVE, - Send = win.SD_SEND, - Both = win.SD_BOTH, +_udp_recv_error :: proc() -> UDP_Recv_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAEFAULT, .WSAEINPROGRESS, .WSAEINVAL, .WSAEISCONN, .WSAENOTSOCK, .WSAEOPNOTSUPP, .WSAEMSGSIZE: + return .Invalid_Argument + case .WSAEINTR: + return .Interrupted + case .WSAENETRESET, .WSAESHUTDOWN, .WSAECONNRESET: + return .Connection_Refused + case .WSAEWOULDBLOCK: + return .Would_Block + case .WSAETIMEDOUT: + return .Timeout + case: + return .Unknown + } } -Shutdown_Error :: enum c.int { - None = 0, - Aborted = win.WSAECONNABORTED, - Reset = win.WSAECONNRESET, - Offline = win.WSAENETDOWN, - Not_Connected = win.WSAENOTCONN, - Not_Socket = win.WSAENOTSOCK, - Invalid_Manner = win.WSAEINVAL, +_tcp_send_error :: proc() -> TCP_Send_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN: + return .Network_Unreachable + case .WSAENOBUFS: + return .Insufficient_Resources + case .WSAEACCES, .WSAEINPROGRESS, .WSAEFAULT, .WSAENOTSOCK, .WSAEOPNOTSUPP, .WSAEMSGSIZE, .WSAEINVAL: + return .Invalid_Argument + case .WSAEINTR: + return .Interrupted + case .WSAENETRESET, .WSAESHUTDOWN, .WSAECONNABORTED, .WSAECONNRESET: + return .Connection_Closed + case .WSAENOTCONN: + return .Not_Connected + case .WSAEWOULDBLOCK: + return .Would_Block + case .WSAETIMEDOUT: + return .Timeout + case .WSAEHOSTUNREACH: + return .Host_Unreachable + case: + return .Unknown + } } -Socket_Option :: enum c.int { - // bool: Whether the address that this socket is bound to can be reused by other sockets. - // This allows you to bypass the cooldown period if a program dies while the socket is bound. - Reuse_Address = win.SO_REUSEADDR, - - // bool: Whether other programs will be inhibited from binding the same endpoint as this socket. - Exclusive_Addr_Use = win.SO_EXCLUSIVEADDRUSE, - - // bool: When true, keepalive packets will be automatically be sent for this connection. TODO: verify this understanding - Keep_Alive = win.SO_KEEPALIVE, - - // bool: When true, client connections will immediately be sent a TCP/IP RST response, rather than being accepted. - Conditional_Accept = win.SO_CONDITIONAL_ACCEPT, - - // bool: If true, when the socket is closed, but data is still waiting to be sent, discard that data. - Dont_Linger = win.SO_DONTLINGER, - - // bool: When true, 'out-of-band' data sent over the socket will be read by a normal net.recv() call, the same as normal 'in-band' data. - Out_Of_Bounds_Data_Inline = win.SO_OOBINLINE, - - // bool: When true, disables send-coalescing, therefore reducing latency. - TCP_Nodelay = win.TCP_NODELAY, - - // win.LINGER: Customizes how long (if at all) the socket will remain open when there - // is some remaining data waiting to be sent, and net.close() is called. - Linger = win.SO_LINGER, - - // win.DWORD: The size, in bytes, of the OS-managed receive-buffer for this socket. - Receive_Buffer_Size = win.SO_RCVBUF, - - // win.DWORD: The size, in bytes, of the OS-managed send-buffer for this socket. - Send_Buffer_Size = win.SO_SNDBUF, - - // win.DWORD: For blocking sockets, the time in milliseconds to wait for incoming data to be received, before giving up and returning .Timeout. - // For non-blocking sockets, ignored. - // Use a value of zero to potentially wait forever. - Receive_Timeout = win.SO_RCVTIMEO, - - // win.DWORD: For blocking sockets, the time in milliseconds to wait for outgoing data to be sent, before giving up and returning .Timeout. - // For non-blocking sockets, ignored. - // Use a value of zero to potentially wait forever. - Send_Timeout = win.SO_SNDTIMEO, - - // bool: Allow sending to, receiving from, and binding to, a broadcast address. - Broadcast = win.SO_BROADCAST, +_udp_send_error :: proc() -> UDP_Send_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSANOTINITIALISED, .WSAENETDOWN, .WSAENETUNREACH: + return .Network_Unreachable + case .WSAENOBUFS: + return .Insufficient_Resources + case .WSAEACCES, .WSAEINVAL, .WSAEINPROGRESS, .WSAEFAULT, .WSAENOTCONN, .WSAENOTSOCK, .WSAEOPNOTSUPP, .WSAEADDRNOTAVAIL, .WSAEAFNOSUPPORT, .WSAEDESTADDRREQ: + return .Invalid_Argument + case .WSAEINTR: + return .Interrupted + case .WSAENETRESET, .WSAESHUTDOWN, .WSAECONNRESET: + return .Connection_Refused + case .WSAEWOULDBLOCK: + return .Would_Block + case .WSAETIMEDOUT: + return .Timeout + case: + return .Unknown + } } -Socket_Option_Error :: enum c.int { - None = 0, - Linger_Only_Supports_Whole_Seconds = 1, - - // The given value is too big or small to be given to the OS. - Value_Out_Of_Range, - - Network_Subsystem_Failure = win.WSAENETDOWN, - Timeout_When_Keepalive_Set = win.WSAENETRESET, - Invalid_Option_For_Socket = win.WSAENOPROTOOPT, - Reset_When_Keepalive_Set = win.WSAENOTCONN, - Not_Socket = win.WSAENOTSOCK, +_shutdown_error :: proc() -> Shutdown_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSAENETDOWN, .WSANOTINITIALISED: + return .Network_Unreachable + case .WSAECONNABORTED, .WSAECONNRESET: + return .Connection_Closed + case .WSAEINPROGRESS, .WSAEINVAL, .WSAENOTCONN, .WSAENOTSOCK: + return .Invalid_Argument + case: + return .Unknown + } } -Set_Blocking_Error :: enum c.int { - None = 0, +_socket_option_error :: proc() -> Socket_Option_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSAENETDOWN, .WSANOTINITIALISED: + return .Network_Unreachable + case .WSAEFAULT, .WSAEINVAL: + return .Invalid_Value + case .WSAENETRESET, .WSAENOTCONN, .WSAENOTSOCK: + return .Invalid_Socket + case .WSAENOPROTOOPT: + return .Invalid_Option + case: + return .Unknown + } +} - Network_Subsystem_Failure = win.WSAENETDOWN, - Blocking_Call_In_Progress = win.WSAEINPROGRESS, - Not_Socket = win.WSAENOTSOCK, - - // TODO: are those errors possible? - Network_Subsystem_Not_Initialized = win.WSAENOTINITIALISED, - Invalid_Argument_Pointer = win.WSAEFAULT, -} \ No newline at end of file +_set_blocking_error :: proc() -> Set_Blocking_Error { + #partial switch win.System_Error(win.WSAGetLastError()) { + case .WSAENETDOWN, .WSANOTINITIALISED: + return .Network_Unreachable + case .WSAEINPROGRESS, .WSAENOTSOCK, .WSAEFAULT: + return .Invalid_Argument + case: + return .Unknown + } +} diff --git a/core/net/interface.odin b/core/net/interface.odin index 775a812f3..4d499a008 100644 --- a/core/net/interface.odin +++ b/core/net/interface.odin @@ -27,7 +27,7 @@ MAX_INTERFACE_ENUMERATION_TRIES :: 3 /* `enumerate_interfaces` retrieves a list of network interfaces with their associated properties. */ -enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { +enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Interfaces_Error) { return _enumerate_interfaces(allocator) } diff --git a/core/net/interface_darwin.odin b/core/net/interface_darwin.odin index 9aa6cbd1a..f189e5844 100644 --- a/core/net/interface_darwin.odin +++ b/core/net/interface_darwin.odin @@ -26,7 +26,7 @@ import "core:sys/posix" foreign import lib "system:System.framework" @(private) -_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { +_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Interfaces_Error) { context.allocator = allocator head: ^ifaddrs @@ -47,7 +47,7 @@ _enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: [] iface.adapter_name = key_ptr^ } if mem_err != nil { - return {}, .Unable_To_Enumerate_Network_Interfaces + return {}, .Allocation_Failure } address: Address diff --git a/core/net/interface_freebsd.odin b/core/net/interface_freebsd.odin index 50e2d1a96..90a538a04 100644 --- a/core/net/interface_freebsd.odin +++ b/core/net/interface_freebsd.odin @@ -25,7 +25,7 @@ import "core:strings" import "core:sys/freebsd" @(private) -_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { +_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Interfaces_Error) { // This is a simplified implementation of `getifaddrs` from the FreeBSD // libc using only Odin and syscalls. context.allocator = allocator @@ -50,7 +50,7 @@ _enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: [] // Allocate and get the entries. buf, alloc_err := make([]byte, needed) if alloc_err != nil { - return nil, .Unable_To_Enumerate_Network_Interfaces + return nil, .Allocation_Failure } defer delete(buf) diff --git a/core/net/interface_linux.odin b/core/net/interface_linux.odin index 28724735b..e329803c5 100644 --- a/core/net/interface_linux.odin +++ b/core/net/interface_linux.odin @@ -30,7 +30,7 @@ package net // NOTE(flysand): https://man7.org/linux/man-pages/man7/netlink.7.html // apparently musl libc uses this to enumerate network interfaces @(private) -_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { +_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Interfaces_Error) { context.allocator = allocator // head: ^os.ifaddrs @@ -143,4 +143,4 @@ _enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: [] // } // return _interfaces[:], {} return nil, {} -} \ No newline at end of file +} diff --git a/core/net/interface_windows.odin b/core/net/interface_windows.odin index a6eb72846..571fb322f 100644 --- a/core/net/interface_windows.odin +++ b/core/net/interface_windows.odin @@ -23,7 +23,7 @@ package net import sys "core:sys/windows" import strings "core:strings" -_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { +_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Interfaces_Error) { context.allocator = allocator buf: []u8 @@ -52,7 +52,8 @@ _enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: [] case 0: break gaa case: - return {}, Platform_Error(res) + set_last_platform_error(i32(res)) + return {}, .Unknown } } @@ -63,13 +64,13 @@ _enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: [] _interfaces := make([dynamic]Network_Interface, 0, allocator) for adapter := (^sys.IP_Adapter_Addresses)(raw_data(buf)); adapter != nil; adapter = adapter.Next { friendly_name, err1 := sys.wstring_to_utf8(sys.wstring(adapter.FriendlyName), 256, allocator) - if err1 != nil { return {}, Platform_Error(err1) } + if err1 != nil { return {}, .Allocation_Failure } description, err2 := sys.wstring_to_utf8(sys.wstring(adapter.Description), 256, allocator) - if err2 != nil { return {}, Platform_Error(err2) } + if err2 != nil { return {}, .Allocation_Failure } dns_suffix, err3 := sys.wstring_to_utf8(sys.wstring(adapter.DnsSuffix), 256, allocator) - if err3 != nil { return {}, Platform_Error(err3) } + if err3 != nil { return {}, .Allocation_Failure } interface := Network_Interface{ adapter_name = strings.clone(string(adapter.AdapterName)), @@ -176,4 +177,4 @@ parse_socket_address :: proc(addr_in: sys.SOCKET_ADDRESS) -> (addr: Endpoint) { case: return // Empty or invalid address type } unreachable() -} \ No newline at end of file +} diff --git a/core/net/socket.odin b/core/net/socket.odin index c5ea11e11..801693962 100644 --- a/core/net/socket.odin +++ b/core/net/socket.odin @@ -35,8 +35,10 @@ any_socket_to_socket :: proc "contextless" (socket: Any_Socket) -> Socket { `a.host.name:9999`, or as `1.2.3.4:9999`, or IP6 equivalent. Calls `parse_hostname_or_endpoint` and `dial_tcp_from_host_or_endpoint`. + + Errors that can be returned: `Parse_Endpoint_Error`, `Resolve_Error`, `DNS_Error`, `Create_Socket_Error`, or `Dial_Error` */ -dial_tcp_from_hostname_and_port_string :: proc(hostname_and_port: string, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +dial_tcp_from_hostname_and_port_string :: proc(hostname_and_port: string, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { target := parse_hostname_or_endpoint(hostname_and_port) or_return return dial_tcp_from_host_or_endpoint(target, options) @@ -47,8 +49,10 @@ dial_tcp_from_hostname_and_port_string :: proc(hostname_and_port: string, option `parse_hostname_or_endpoint` is called and the `hostname` will be resolved into an IP. If a `hostname` of form `a.host.name:9999` is given, the port will be ignored in favor of the explicit `port` param. + + Errors that can be returned: `Parse_Endpoint_Error`, `Resolve_Error`, `DNS_Error`, `Create_Socket_Error`, or `Dial_Error` */ -dial_tcp_from_hostname_with_port_override :: proc(hostname: string, port: int, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +dial_tcp_from_hostname_with_port_override :: proc(hostname: string, port: int, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { target := parse_hostname_or_endpoint(hostname) or_return switch &t in target { case Endpoint: @@ -62,8 +66,10 @@ dial_tcp_from_hostname_with_port_override :: proc(hostname: string, port: int, o /* Expects the `host` as Host. + + Errors that can be returned: `Resolve_Error`, `DNS_Error`, `Create_Socket_Error`, or `Dial_Error` */ -dial_tcp_from_host :: proc(host: Host, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +dial_tcp_from_host :: proc(host: Host, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { if host.port == 0 { return 0, .Port_Required } @@ -76,8 +82,10 @@ dial_tcp_from_host :: proc(host: Host, options := default_tcp_options) -> (socke /* Expects the `target` as a Host_OrEndpoint. Unwraps the underlying type and calls `dial_tcp_from_host` or `dial_tcp_from_endpoint`. + + Errors that can be returned: `Parse_Endpoint_Error`, `Resolve_Error`, `DNS_Error`, `Create_Socket_Error`, or `Dial_Error` */ -dial_tcp_from_host_or_endpoint :: proc(target: Host_Or_Endpoint, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +dial_tcp_from_host_or_endpoint :: proc(target: Host_Or_Endpoint, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { switch t in target { case Endpoint: return dial_tcp_from_endpoint(t, options) @@ -87,12 +95,21 @@ dial_tcp_from_host_or_endpoint :: proc(target: Host_Or_Endpoint, options := defa unreachable() } -// Dial from an Address -dial_tcp_from_address_and_port :: proc(address: Address, port: int, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +/* + Dial from an Address. + + Errors that can be returned: `Create_Socket_Error`, or `Dial_Error` +*/ +dial_tcp_from_address_and_port :: proc(address: Address, port: int, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { return dial_tcp_from_endpoint({address, port}, options) } -dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +/* + Dial from an Endpoint. + + Errors that can be returned: `Create_Socket_Error`, or `Dial_Error` +*/ +dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { return _dial_tcp_from_endpoint(endpoint, options) } @@ -105,11 +122,11 @@ dial_tcp :: proc{ dial_tcp_from_host_or_endpoint, } -create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { +create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { return _create_socket(family, protocol) } -bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Network_Error) { +bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Bind_Error) { return _bind(socket, ep) } @@ -119,7 +136,7 @@ bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Network_Error) { This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first. */ -make_unbound_udp_socket :: proc(family: Address_Family) -> (socket: UDP_Socket, err: Network_Error) { +make_unbound_udp_socket :: proc(family: Address_Family) -> (socket: UDP_Socket, err: Create_Socket_Error) { sock := create_socket(family, .UDP) or_return socket = sock.(UDP_Socket) return @@ -131,6 +148,8 @@ make_unbound_udp_socket :: proc(family: Address_Family) -> (socket: UDP_Socket, This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first. The `bound_address` is the address of the network interface that you want to use, or a loopback address if you don't care which to use. + + Errors that can be returned: `Parse_Endpoint_Error`, `Create_Socket_Error`, or `Bind_Error` */ make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (socket: UDP_Socket, err: Network_Error) { if bound_address == nil { @@ -141,6 +160,11 @@ make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (socket: UDP return } +/* + Creates a TCP socket and starts listening on the given endpoint. + + Errors that can be returned: `Create_Socket_Error`, `Bind_Error`, or `Listen_Error` +*/ listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (socket: TCP_Socket, err: Network_Error) { assert(backlog > 0 && backlog < int(max(i32))) @@ -150,11 +174,11 @@ listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (socket: TC /* Returns the endpoint that the given socket is listening / bound on. */ -bound_endpoint :: proc(socket: Any_Socket) -> (endpoint: Endpoint, err: Network_Error) { +bound_endpoint :: proc(socket: Any_Socket) -> (endpoint: Endpoint, err: Listen_Error) { return _bound_endpoint(socket) } -accept_tcp :: proc(socket: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { +accept_tcp :: proc(socket: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (client: TCP_Socket, source: Endpoint, err: Accept_Error) { return _accept_tcp(socket, options) } @@ -162,11 +186,11 @@ close :: proc(socket: Any_Socket) { _close(socket) } -recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { +recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: TCP_Recv_Error) { return _recv_tcp(socket, buf) } -recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { +recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: UDP_Recv_Error) { return _recv_udp(socket, buf) } @@ -175,6 +199,8 @@ recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_en Note: `remote_endpoint` parameter is non-nil only if the socket type is UDP. On TCP sockets it will always return `nil`. + + Errors that can be returned: `TCP_Recv_Error`, or `UDP_Recv_Error` */ recv_any :: proc(socket: Any_Socket, buf: []byte) -> ( bytes_read: int, @@ -197,7 +223,7 @@ recv :: proc{recv_tcp, recv_udp, recv_any} Repeatedly sends data until the entire buffer is sent. If a send fails before all data is sent, returns the amount sent up to that point. */ -send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { +send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: TCP_Send_Error) { return _send_tcp(socket, buf) } @@ -207,10 +233,15 @@ send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: N Datagrams are limited in size; attempting to send more than this limit at once will result in a Message_Too_Long error. UDP packets are not guarenteed to be received in order. */ -send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { +send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: UDP_Send_Error) { return _send_udp(socket, buf, to) } +/* + Sends data over the socket. + + Errors that can be returned: `TCP_Send_Error`, or `UDP_Send_Error` +*/ send_any :: proc(socket: Any_Socket, buf: []byte, to: Maybe(Endpoint) = nil) -> ( bytes_written: int, err: Network_Error, @@ -226,14 +257,14 @@ send_any :: proc(socket: Any_Socket, buf: []byte, to: Maybe(Endpoint) = nil) -> send :: proc{send_tcp, send_udp, send_any} -shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { +shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) { return _shutdown(socket, manner) } -set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { +set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error { return _set_option(socket, option, value, loc) } -set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Network_Error) { +set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) { return _set_blocking(socket, should_block) } diff --git a/core/net/socket_darwin.odin b/core/net/socket_darwin.odin index a132a6a95..e63f1844a 100644 --- a/core/net/socket_darwin.odin +++ b/core/net/socket_darwin.odin @@ -37,8 +37,14 @@ Socket_Option :: enum c.int { Send_Timeout = c.int(posix.Sock_Option.SNDTIMEO), } +Shutdown_Manner :: enum c.int { + Receive = c.int(posix.SHUT_RD), + Send = c.int(posix.SHUT_WR), + Both = c.int(posix.SHUT_RDWR), +} + @(private) -_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { +_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { c_type: posix.Sock c_protocol: posix.Protocol c_family: posix.AF @@ -59,7 +65,7 @@ _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (so sock := posix.socket(c_family, c_type, c_protocol) if sock < 0 { - err = Create_Socket_Error(posix.errno()) + err = _create_socket_error() return } @@ -72,7 +78,7 @@ _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (so } @(private) -_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { +_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (skt: TCP_Socket, err: Network_Error) { if endpoint.port == 0 { return 0, .Port_Required } @@ -88,28 +94,19 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio sockaddr := _endpoint_to_sockaddr(endpoint) if posix.connect(posix.FD(skt), (^posix.sockaddr)(&sockaddr), posix.socklen_t(sockaddr.ss_len)) != .OK { - errno := posix.errno() + err = _dial_error() close(skt) - return {}, Dial_Error(errno) } return } -// On Darwin, any port below 1024 is 'privileged' - which means that you need root access in order to use it. -MAX_PRIVILEGED_PORT :: 1023 - @(private) -_bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) { +_bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Bind_Error) { sockaddr := _endpoint_to_sockaddr(ep) s := any_socket_to_socket(skt) if posix.bind(posix.FD(s), (^posix.sockaddr)(&sockaddr), posix.socklen_t(sockaddr.ss_len)) != .OK { - errno := posix.errno() - if errno == .EACCES && ep.port <= MAX_PRIVILEGED_PORT { - err = .Privileged_Port_Without_Root - } else { - err = Bind_Error(errno) - } + err = _bind_error() } return @@ -128,25 +125,23 @@ _listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_ // bypass the cooldown period, and allow the next run of the program to // use the same address immediately. // - // TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address! - set_option(sock, .Reuse_Address, true) or_return + _ = set_option(sock, .Reuse_Address, true) bind(sock, interface_endpoint) or_return if posix.listen(posix.FD(skt), i32(backlog)) != .OK { - err = Listen_Error(posix.errno()) - return + err = _listen_error() } return } @(private) -_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) { +_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Listen_Error) { addr: posix.sockaddr_storage addr_len := posix.socklen_t(size_of(addr)) if posix.getsockname(posix.FD(any_socket_to_socket(sock)), (^posix.sockaddr)(&addr), &addr_len) != .OK { - err = Listen_Error(posix.errno()) + err = _listen_error() return } @@ -155,12 +150,12 @@ _bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) } @(private) -_accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { +_accept_tcp :: proc(sock: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (client: TCP_Socket, source: Endpoint, err: Accept_Error) { addr: posix.sockaddr_storage addr_len := posix.socklen_t(size_of(addr)) client_sock := posix.accept(posix.FD(sock), (^posix.sockaddr)(&addr), &addr_len) if client_sock < 0 { - err = Accept_Error(posix.errno()) + err = _accept_error() return } @@ -176,14 +171,14 @@ _close :: proc(skt: Any_Socket) { } @(private) -_recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { +_recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: TCP_Recv_Error) { if len(buf) <= 0 { return } res := posix.recv(posix.FD(skt), raw_data(buf), len(buf), {}) if res < 0 { - err = TCP_Recv_Error(posix.errno()) + err = _tcp_recv_error() return } @@ -191,7 +186,7 @@ _recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Networ } @(private) -_recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { +_recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: UDP_Recv_Error) { if len(buf) <= 0 { return } @@ -200,7 +195,7 @@ _recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endp fromsize := posix.socklen_t(size_of(from)) res := posix.recvfrom(posix.FD(skt), raw_data(buf), len(buf), {}, (^posix.sockaddr)(&from), &fromsize) if res < 0 { - err = UDP_Recv_Error(posix.errno()) + err = _udp_recv_error() return } @@ -210,20 +205,13 @@ _recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endp } @(private) -_send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { +_send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: TCP_Send_Error) { for bytes_written < len(buf) { limit := min(int(max(i32)), len(buf) - bytes_written) remaining := buf[bytes_written:][:limit] res := posix.send(posix.FD(skt), raw_data(remaining), len(remaining), {.NOSIGNAL}) if res < 0 { - errno := posix.errno() - if errno == .EPIPE { - // EPIPE arises if the socket has been closed remotely. - err = TCP_Send_Error.Connection_Closed - return - } - - err = TCP_Send_Error(errno) + err = _tcp_send_error() return } @@ -233,21 +221,14 @@ _send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Net } @(private) -_send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { +_send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: UDP_Send_Error) { toaddr := _endpoint_to_sockaddr(to) for bytes_written < len(buf) { limit := min(1<<31, len(buf) - bytes_written) remaining := buf[bytes_written:][:limit] res := posix.sendto(posix.FD(skt), raw_data(remaining), len(remaining), {.NOSIGNAL}, (^posix.sockaddr)(&toaddr), posix.socklen_t(toaddr.ss_len)) if res < 0 { - errno := posix.errno() - if errno == .EPIPE { - // EPIPE arises if the socket has been closed remotely. - err = UDP_Send_Error.Not_Socket - return - } - - err = UDP_Send_Error(errno) + err = _udp_send_error() return } @@ -257,16 +238,16 @@ _send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: } @(private) -_shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { +_shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) { s := any_socket_to_socket(skt) if posix.shutdown(posix.FD(s), posix.Shut(manner)) != .OK { - err = Shutdown_Error(posix.errno()) + err = _shutdown_error() } return } @(private) -_set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { +_set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error { level := posix.SOL_SOCKET if option != .TCP_Nodelay else posix.IPPROTO_TCP // NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool; @@ -337,19 +318,19 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca skt := any_socket_to_socket(s) if posix.setsockopt(posix.FD(skt), i32(level), posix.Sock_Option(option), ptr, len) != .OK { - return Socket_Option_Error(posix.errno()) + return _socket_option_error() } return nil } @(private) -_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Network_Error) { +_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) { socket := any_socket_to_socket(socket) flags_ := posix.fcntl(posix.FD(socket), .GETFL, 0) if flags_ < 0 { - return Set_Blocking_Error(posix.errno()) + return _set_blocking_error() } flags := transmute(posix.O_Flags)flags_ @@ -360,7 +341,7 @@ _set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Network_E } if posix.fcntl(posix.FD(socket), .SETFL, flags) < 0 { - return Set_Blocking_Error(posix.errno()) + return _set_blocking_error() } return nil diff --git a/core/net/socket_freebsd.odin b/core/net/socket_freebsd.odin index 3a3774007..b510346ba 100644 --- a/core/net/socket_freebsd.odin +++ b/core/net/socket_freebsd.odin @@ -54,8 +54,14 @@ Socket_Option :: enum c.int { Receive_Timeout = cast(c.int)freebsd.Socket_Option.RCVTIMEO, } +Shutdown_Manner :: enum c.int { + Receive = cast(c.int)freebsd.Shutdown_Method.RD, + Send = cast(c.int)freebsd.Shutdown_Method.WR, + Both = cast(c.int)freebsd.Shutdown_Method.RDWR, +} + @(private) -_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { +_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { sys_family: freebsd.Protocol_Family = --- sys_protocol: freebsd.Protocol = --- sys_socket_type: freebsd.Socket_Type = --- @@ -72,24 +78,7 @@ _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (so new_socket, errno := freebsd.socket(sys_family, sys_socket_type, sys_protocol) if errno != nil { - err = cast(Create_Socket_Error)errno - return - } - - // NOTE(Feoramund): By default, FreeBSD will generate SIGPIPE if an EPIPE - // error is raised during the writing of a socket that may be closed. - // This behavior is unlikely to be expected by general users. - // - // There are two workarounds. One is to apply the .NOSIGNAL flag when using - // the `sendto` syscall. However, that would prevent users of this library - // from re-enabling the SIGPIPE-raising functionality, if they really - // wanted it. - // - // So I have disabled it here with this socket option for all sockets. - truth: b32 = true - errno = freebsd.setsockopt(new_socket, .SOCKET, .NOSIGPIPE, &truth, size_of(truth)) - if errno != nil { - err = cast(Socket_Option_Error)errno + err = _create_socket_error(errno) return } @@ -102,7 +91,7 @@ _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (so } @(private) -_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { if endpoint.port == 0 { return 0, .Port_Required } @@ -115,19 +104,19 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio errno := freebsd.connect(cast(Fd)socket, &sockaddr, cast(freebsd.socklen_t)sockaddr.len) if errno != nil { close(socket) - return {}, cast(Dial_Error)errno + return {}, _dial_error(errno) } return } @(private) -_bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Network_Error) { +_bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Bind_Error) { sockaddr := _endpoint_to_sockaddr(ep) real_socket := any_socket_to_socket(socket) errno := freebsd.bind(cast(Fd)real_socket, &sockaddr, cast(freebsd.socklen_t)sockaddr.len) if errno != nil { - err = cast(Bind_Error)errno + err = _bind_error(errno) } return } @@ -143,7 +132,7 @@ _listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (socket: T errno := freebsd.listen(cast(Fd)socket, backlog) if errno != nil { - err = cast(Listen_Error)errno + err = _listen_error(errno) return } @@ -151,12 +140,12 @@ _listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (socket: T } @(private) -_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) { +_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Listen_Error) { sockaddr: freebsd.Socket_Address_Storage errno := freebsd.getsockname(cast(Fd)any_socket_to_socket(sock), &sockaddr) if errno != nil { - err = cast(Listen_Error)errno + err = _listen_error(errno) return } @@ -165,12 +154,12 @@ _bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) } @(private) -_accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { +_accept_tcp :: proc(sock: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (client: TCP_Socket, source: Endpoint, err: Accept_Error) { sockaddr: freebsd.Socket_Address_Storage result, errno := freebsd.accept(cast(Fd)sock, &sockaddr) if errno != nil { - err = cast(Accept_Error)errno + err = _accept_error(errno) return } @@ -187,20 +176,20 @@ _close :: proc(socket: Any_Socket) { } @(private) -_recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { +_recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: TCP_Recv_Error) { if len(buf) == 0 { return } result, errno := freebsd.recv(cast(Fd)socket, buf, .NONE) if errno != nil { - err = cast(TCP_Recv_Error)errno + err = _tcp_recv_error(errno) return } return result, nil } @(private) -_recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { +_recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: UDP_Recv_Error) { if len(buf) == 0 { return } @@ -208,21 +197,21 @@ _recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_e result, errno := freebsd.recvfrom(cast(Fd)socket, buf, .NONE, &from) if errno != nil { - err = cast(UDP_Recv_Error)errno + err = _udp_recv_error(errno) return } return result, _sockaddr_to_endpoint(&from), nil } @(private) -_send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { +_send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: TCP_Send_Error) { for bytes_written < len(buf) { limit := min(int(max(i32)), len(buf) - bytes_written) remaining := buf[bytes_written:][:limit] - result, errno := freebsd.send(cast(Fd)socket, remaining, .NONE) + result, errno := freebsd.send(cast(Fd)socket, remaining, .NOSIGNAL) if errno != nil { - err = cast(TCP_Send_Error)errno + err = _tcp_send_error(errno) return } bytes_written += result @@ -231,15 +220,15 @@ _send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: } @(private) -_send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { +_send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: UDP_Send_Error) { toaddr := _endpoint_to_sockaddr(to) for bytes_written < len(buf) { limit := min(int(max(i32)), len(buf) - bytes_written) remaining := buf[bytes_written:][:limit] - result, errno := freebsd.sendto(cast(Fd)socket, remaining, .NONE, &toaddr) + result, errno := freebsd.sendto(cast(Fd)socket, remaining, .NOSIGNAL, &toaddr) if errno != nil { - err = cast(UDP_Send_Error)errno + err = _udp_send_error(errno) return } bytes_written += result @@ -248,17 +237,17 @@ _send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_writt } @(private) -_shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { +_shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) { real_socket := cast(Fd)any_socket_to_socket(socket) errno := freebsd.shutdown(real_socket, cast(freebsd.Shutdown_Method)manner) if errno != nil { - return cast(Shutdown_Error)errno + return _shutdown_error(errno) } return } @(private) -_set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { +_set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error { // NOTE(Feoramund): I found that FreeBSD, like Linux, requires at least 32 // bits for a boolean socket option value. Nothing less will work. bool_value: b32 @@ -315,25 +304,25 @@ _set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc : case u16: int_value = cast(i32)real case i32: int_value = real case u32: - if real > u32(max(i32)) { return .Value_Out_Of_Range } + if real > u32(max(i32)) { return .Invalid_Value } int_value = cast(i32)real case i64: - if real > i64(max(i32)) || real < i64(min(i32)) { return .Value_Out_Of_Range } + if real > i64(max(i32)) || real < i64(min(i32)) { return .Invalid_Value } int_value = cast(i32)real case u64: - if real > u64(max(i32)) { return .Value_Out_Of_Range } + if real > u64(max(i32)) { return .Invalid_Value } int_value = cast(i32)real case i128: - if real > i128(max(i32)) || real < i128(min(i32)) { return .Value_Out_Of_Range } + if real > i128(max(i32)) || real < i128(min(i32)) { return .Invalid_Value } int_value = cast(i32)real case u128: - if real > u128(max(i32)) { return .Value_Out_Of_Range } + if real > u128(max(i32)) { return .Invalid_Value } int_value = cast(i32)real case int: - if real > int(max(i32)) || real < int(min(i32)) { return .Value_Out_Of_Range } + if real > int(max(i32)) || real < int(min(i32)) { return .Invalid_Value } int_value = cast(i32)real case uint: - if real > uint(max(i32)) { return .Value_Out_Of_Range } + if real > uint(max(i32)) { return .Invalid_Value } int_value = cast(i32)real case: panic("set_option() value must be an integer here", loc) @@ -347,19 +336,19 @@ _set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc : real_socket := any_socket_to_socket(socket) errno := freebsd.setsockopt(cast(Fd)real_socket, .SOCKET, cast(freebsd.Socket_Option)option, ptr, len) if errno != nil { - return cast(Socket_Option_Error)errno + return _socket_option_error(errno) } return nil } @(private) -_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Network_Error) { +_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) { real_socket := any_socket_to_socket(socket) flags, errno := freebsd.fcntl_getfl(cast(freebsd.Fd)real_socket) if errno != nil { - return cast(Set_Blocking_Error)errno + return _set_blocking_error(errno) } if should_block { @@ -370,7 +359,7 @@ _set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Network_E errno = freebsd.fcntl_setfl(cast(freebsd.Fd)real_socket, flags) if errno != nil { - return cast(Set_Blocking_Error)errno + return _set_blocking_error(errno) } return diff --git a/core/net/socket_linux.odin b/core/net/socket_linux.odin index cafec747d..3ec3521f0 100644 --- a/core/net/socket_linux.odin +++ b/core/net/socket_linux.odin @@ -38,15 +38,21 @@ Socket_Option :: enum c.int { Broadcast = c.int(linux.Socket_Option.BROADCAST), } +Shutdown_Manner :: enum c.int { + Receive = c.int(linux.Shutdown_How.RD), + Send = c.int(linux.Shutdown_How.WR), + Both = c.int(linux.Shutdown_How.RDWR), +} + // Wrappers and unwrappers for system-native types @(private="file") -_unwrap_os_socket :: proc "contextless" (sock: Any_Socket)->linux.Fd { +_unwrap_os_socket :: proc "contextless" (sock: Any_Socket) -> linux.Fd { return linux.Fd(any_socket_to_socket(sock)) } @(private="file") -_wrap_os_socket :: proc "contextless" (sock: linux.Fd, protocol: Socket_Protocol)->Any_Socket { +_wrap_os_socket :: proc "contextless" (sock: linux.Fd, protocol: Socket_Protocol) -> Any_Socket { switch protocol { case .TCP: return TCP_Socket(Socket(sock)) case .UDP: return UDP_Socket(Socket(sock)) @@ -56,7 +62,7 @@ _wrap_os_socket :: proc "contextless" (sock: linux.Fd, protocol: Socket_Protocol } @(private="file") -_unwrap_os_family :: proc "contextless" (family: Address_Family)->linux.Address_Family { +_unwrap_os_family :: proc "contextless" (family: Address_Family) -> linux.Address_Family { switch family { case .IP4: return .INET case .IP6: return .INET6 @@ -66,7 +72,7 @@ _unwrap_os_family :: proc "contextless" (family: Address_Family)->linux.Address_ } @(private="file") -_unwrap_os_proto_socktype :: proc "contextless" (protocol: Socket_Protocol)->(linux.Protocol, linux.Socket_Type) { +_unwrap_os_proto_socktype :: proc "contextless" (protocol: Socket_Protocol) -> (linux.Protocol, linux.Socket_Type) { switch protocol { case .TCP: return .TCP, .STREAM case .UDP: return .UDP, .DGRAM @@ -76,7 +82,7 @@ _unwrap_os_proto_socktype :: proc "contextless" (protocol: Socket_Protocol)->(li } @(private="file") -_unwrap_os_addr :: proc "contextless" (endpoint: Endpoint)->(linux.Sock_Addr_Any) { +_unwrap_os_addr :: proc "contextless" (endpoint: Endpoint) -> linux.Sock_Addr_Any { switch address in endpoint.address { case IP4_Address: return { @@ -100,7 +106,7 @@ _unwrap_os_addr :: proc "contextless" (endpoint: Endpoint)->(linux.Sock_Addr_Any } @(private="file") -_wrap_os_addr :: proc "contextless" (addr: linux.Sock_Addr_Any)->(Endpoint) { +_wrap_os_addr :: proc "contextless" (addr: linux.Sock_Addr_Any) -> Endpoint { #partial switch addr.family { case .INET: return { @@ -117,18 +123,18 @@ _wrap_os_addr :: proc "contextless" (addr: linux.Sock_Addr_Any)->(Endpoint) { } } -_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (Any_Socket, Network_Error) { +_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (Any_Socket, Create_Socket_Error) { family := _unwrap_os_family(family) proto, socktype := _unwrap_os_proto_socktype(protocol) sock, errno := linux.socket(family, socktype, {.CLOEXEC}, proto) if errno != .NONE { - return {}, Create_Socket_Error(errno) + return {}, _create_socket_error(errno) } return _wrap_os_socket(sock, protocol), nil } @(private) -_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (TCP_Socket, Network_Error) { +_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (TCP_Socket, Network_Error) { errno: linux.Errno if endpoint.port == 0 { return 0, .Port_Required @@ -138,7 +144,7 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio os_sock, errno = linux.socket(_unwrap_os_family(family_from_endpoint(endpoint)), .STREAM, {.CLOEXEC}, .TCP) if errno != .NONE { // TODO(flysand): should return invalid file descriptor here casted as TCP_Socket - return {}, Create_Socket_Error(errno) + return {}, _create_socket_error(errno) } // NOTE(tetra): This is so that if we crash while the socket is open, we can // bypass the cooldown period, and allow the next run of the program to @@ -149,7 +155,7 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio errno = linux.connect(linux.Fd(os_sock), &addr) if errno != .NONE { close(cast(TCP_Socket) os_sock) - return {}, Dial_Error(errno) + return {}, _dial_error(errno) } // NOTE(tetra): Not vital to succeed; error ignored no_delay: b32 = cast(b32) options.no_delay @@ -158,11 +164,11 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio } @(private) -_bind :: proc(sock: Any_Socket, endpoint: Endpoint) -> (Network_Error) { +_bind :: proc(sock: Any_Socket, endpoint: Endpoint) -> (Bind_Error) { addr := _unwrap_os_addr(endpoint) errno := linux.bind(_unwrap_os_socket(sock), &addr) if errno != .NONE { - return Bind_Error(errno) + return _bind_error(errno) } return nil } @@ -180,7 +186,7 @@ _listen_tcp :: proc(endpoint: Endpoint, backlog := 1000) -> (socket: TCP_Socket, os_sock: linux.Fd os_sock, errno = linux.socket(ep_family, .STREAM, {.CLOEXEC}, .TCP) if errno != .NONE { - err = Create_Socket_Error(errno) + err = _create_socket_error(errno) return } socket = cast(TCP_Socket)os_sock @@ -193,31 +199,30 @@ _listen_tcp :: proc(endpoint: Endpoint, backlog := 1000) -> (socket: TCP_Socket, // TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address! do_reuse_addr: b32 = true if errno = linux.setsockopt(os_sock, linux.SOL_SOCKET, linux.Socket_Option.REUSEADDR, &do_reuse_addr); errno != .NONE { - err = Listen_Error(errno) + err = _listen_error(errno) return } // Bind the socket to endpoint address if errno = linux.bind(os_sock, &ep_address); errno != .NONE { - err = Bind_Error(errno) + err = _bind_error(errno) return } // Listen on bound socket if errno = linux.listen(os_sock, cast(i32) backlog); errno != .NONE { - err = Listen_Error(errno) - return + err = _listen_error(errno) } return } @(private) -_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) { +_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Listen_Error) { addr: linux.Sock_Addr_Any errno := linux.getsockname(_unwrap_os_socket(sock), &addr) if errno != .NONE { - err = Listen_Error(errno) + err = _listen_error(errno) return } @@ -226,11 +231,11 @@ _bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) } @(private) -_accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (tcp_client: TCP_Socket, endpoint: Endpoint, err: Network_Error) { +_accept_tcp :: proc(sock: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (tcp_client: TCP_Socket, endpoint: Endpoint, err: Accept_Error) { addr: linux.Sock_Addr_Any client_sock, errno := linux.accept(linux.Fd(sock), &addr) if errno != .NONE { - return {}, {}, Accept_Error(errno) + return {}, {}, _accept_error(errno) } // NOTE(tetra): Not vital to succeed; error ignored val: b32 = cast(b32) options.no_delay @@ -244,19 +249,19 @@ _close :: proc(sock: Any_Socket) { } @(private) -_recv_tcp :: proc(tcp_sock: TCP_Socket, buf: []byte) -> (int, Network_Error) { +_recv_tcp :: proc(tcp_sock: TCP_Socket, buf: []byte) -> (int, TCP_Recv_Error) { if len(buf) <= 0 { return 0, nil } bytes_read, errno := linux.recv(linux.Fd(tcp_sock), buf, {}) if errno != .NONE { - return 0, TCP_Recv_Error(errno) + return 0, _tcp_recv_error(errno) } return int(bytes_read), nil } @(private) -_recv_udp :: proc(udp_sock: UDP_Socket, buf: []byte) -> (int, Endpoint, Network_Error) { +_recv_udp :: proc(udp_sock: UDP_Socket, buf: []byte) -> (int, Endpoint, UDP_Recv_Error) { if len(buf) <= 0 { // NOTE(flysand): It was returning no error, I didn't change anything return 0, {}, {} @@ -268,28 +273,24 @@ _recv_udp :: proc(udp_sock: UDP_Socket, buf: []byte) -> (int, Endpoint, Network_ from_addr: linux.Sock_Addr_Any bytes_read, errno := linux.recvfrom(linux.Fd(udp_sock), buf, {.TRUNC}, &from_addr) if errno != .NONE { - return 0, {}, UDP_Recv_Error(errno) + return 0, {}, _udp_recv_error(errno) } if bytes_read > len(buf) { // NOTE(tetra): The buffer has been filled, with a partial message. - return len(buf), {}, .Buffer_Too_Small + return len(buf), {}, .Excess_Truncated } return bytes_read, _wrap_os_addr(from_addr), nil } @(private) -_send_tcp :: proc(tcp_sock: TCP_Socket, buf: []byte) -> (int, Network_Error) { +_send_tcp :: proc(tcp_sock: TCP_Socket, buf: []byte) -> (int, TCP_Send_Error) { total_written := 0 for total_written < len(buf) { limit := min(int(max(i32)), len(buf) - total_written) remaining := buf[total_written:][:limit] res, errno := linux.send(linux.Fd(tcp_sock), remaining, {.NOSIGNAL}) - if errno == .EPIPE { - // If the peer is disconnected when we are trying to send we will get an `EPIPE` error, - // so we turn that into a clearer error - return total_written, TCP_Send_Error.Connection_Closed - } else if errno != .NONE { - return total_written, TCP_Send_Error(errno) + if errno != .NONE { + return total_written, _tcp_send_error(errno) } total_written += int(res) } @@ -297,28 +298,28 @@ _send_tcp :: proc(tcp_sock: TCP_Socket, buf: []byte) -> (int, Network_Error) { } @(private) -_send_udp :: proc(udp_sock: UDP_Socket, buf: []byte, to: Endpoint) -> (int, Network_Error) { +_send_udp :: proc(udp_sock: UDP_Socket, buf: []byte, to: Endpoint) -> (int, UDP_Send_Error) { to_addr := _unwrap_os_addr(to) bytes_written, errno := linux.sendto(linux.Fd(udp_sock), buf, {}, &to_addr) if errno != .NONE { - return bytes_written, UDP_Send_Error(errno) + return bytes_written, _udp_send_error(errno) } return int(bytes_written), nil } @(private) -_shutdown :: proc(sock: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { +_shutdown :: proc(sock: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) { os_sock := _unwrap_os_socket(sock) errno := linux.shutdown(os_sock, cast(linux.Shutdown_How) manner) if errno != .NONE { - return Shutdown_Error(errno) + return _shutdown_error(errno) } return nil } // TODO(flysand): Figure out what we want to do with this on core:sys/ level. @(private) -_set_option :: proc(sock: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { +_set_option :: proc(sock: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error { level: int if option == .TCP_Nodelay { level = int(linux.SOL_TCP) @@ -388,19 +389,19 @@ _set_option :: proc(sock: Any_Socket, option: Socket_Option, value: any, loc := errno = linux.setsockopt(os_sock, level, int(option), &int_value) } if errno != .NONE { - return Socket_Option_Error(errno) + return _socket_option_error(errno) } return nil } @(private) -_set_blocking :: proc(sock: Any_Socket, should_block: bool) -> (err: Network_Error) { +_set_blocking :: proc(sock: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) { errno: linux.Errno flags: linux.Open_Flags os_sock := _unwrap_os_socket(sock) flags, errno = linux.fcntl(os_sock, linux.F_GETFL) if errno != .NONE { - return Set_Blocking_Error(errno) + return _set_blocking_error(errno) } if should_block { flags -= {.NONBLOCK} @@ -409,7 +410,7 @@ _set_blocking :: proc(sock: Any_Socket, should_block: bool) -> (err: Network_Err } errno = linux.fcntl(os_sock, linux.F_SETFL, flags) if errno != .NONE { - return Set_Blocking_Error(errno) + return _set_blocking_error(errno) } return nil } diff --git a/core/net/socket_windows.odin b/core/net/socket_windows.odin index f19be536a..4576149de 100644 --- a/core/net/socket_windows.odin +++ b/core/net/socket_windows.odin @@ -24,13 +24,67 @@ import "core:c" import win "core:sys/windows" import "core:time" +Socket_Option :: enum c.int { + // bool: Whether the address that this socket is bound to can be reused by other sockets. + // This allows you to bypass the cooldown period if a program dies while the socket is bound. + Reuse_Address = win.SO_REUSEADDR, + + // bool: Whether other programs will be inhibited from binding the same endpoint as this socket. + Exclusive_Addr_Use = win.SO_EXCLUSIVEADDRUSE, + + // bool: When true, keepalive packets will be automatically be sent for this connection. TODO: verify this understanding + Keep_Alive = win.SO_KEEPALIVE, + + // bool: When true, client connections will immediately be sent a TCP/IP RST response, rather than being accepted. + Conditional_Accept = win.SO_CONDITIONAL_ACCEPT, + + // bool: If true, when the socket is closed, but data is still waiting to be sent, discard that data. + Dont_Linger = win.SO_DONTLINGER, + + // bool: When true, 'out-of-band' data sent over the socket will be read by a normal net.recv() call, the same as normal 'in-band' data. + Out_Of_Bounds_Data_Inline = win.SO_OOBINLINE, + + // bool: When true, disables send-coalescing, therefore reducing latency. + TCP_Nodelay = win.TCP_NODELAY, + + // win.LINGER: Customizes how long (if at all) the socket will remain open when there + // is some remaining data waiting to be sent, and net.close() is called. + Linger = win.SO_LINGER, + + // win.DWORD: The size, in bytes, of the OS-managed receive-buffer for this socket. + Receive_Buffer_Size = win.SO_RCVBUF, + + // win.DWORD: The size, in bytes, of the OS-managed send-buffer for this socket. + Send_Buffer_Size = win.SO_SNDBUF, + + // win.DWORD: For blocking sockets, the time in milliseconds to wait for incoming data to be received, before giving up and returning .Timeout. + // For non-blocking sockets, ignored. + // Use a value of zero to potentially wait forever. + Receive_Timeout = win.SO_RCVTIMEO, + + // win.DWORD: For blocking sockets, the time in milliseconds to wait for outgoing data to be sent, before giving up and returning .Timeout. + // For non-blocking sockets, ignored. + // Use a value of zero to potentially wait forever. + Send_Timeout = win.SO_SNDTIMEO, + + // bool: Allow sending to, receiving from, and binding to, a broadcast address. + Broadcast = win.SO_BROADCAST, +} + + +Shutdown_Manner :: enum c.int { + Receive = win.SD_RECEIVE, + Send = win.SD_SEND, + Both = win.SD_BOTH, +} + @(init, private) ensure_winsock_initialized :: proc() { win.ensure_winsock_initialized() } @(private) -_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { +_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { c_type, c_protocol, c_family: c.int switch family { @@ -49,7 +103,7 @@ _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (so sock := win.socket(c_family, c_type, c_protocol) if sock == win.INVALID_SOCKET { - err = Create_Socket_Error(win.WSAGetLastError()) + err = _create_socket_error() return } @@ -62,7 +116,7 @@ _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (so } @(private) -_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (socket: TCP_Socket, err: Network_Error) { +_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) { if endpoint.port == 0 { err = .Port_Required return @@ -80,7 +134,7 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio sockaddr := _endpoint_to_sockaddr(endpoint) res := win.connect(win.SOCKET(socket), &sockaddr, size_of(sockaddr)) if res < 0 { - err = Dial_Error(win.WSAGetLastError()) + err = _dial_error() close(socket) return {}, err } @@ -93,12 +147,12 @@ _dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_optio } @(private) -_bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Network_Error) { +_bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Bind_Error) { sockaddr := _endpoint_to_sockaddr(ep) sock := any_socket_to_socket(socket) res := win.bind(win.SOCKET(sock), &sockaddr, size_of(sockaddr)) if res < 0 { - err = Bind_Error(win.WSAGetLastError()) + err = _bind_error() } return } @@ -117,17 +171,17 @@ _listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (socket: T bind(sock, interface_endpoint) or_return if res := win.listen(win.SOCKET(socket), i32(backlog)); res == win.SOCKET_ERROR { - err = Listen_Error(win.WSAGetLastError()) + err = _listen_error() } return } @(private) -_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) { +_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Listen_Error) { sockaddr: win.SOCKADDR_STORAGE_LH sockaddrlen := c.int(size_of(sockaddr)) if win.getsockname(win.SOCKET(any_socket_to_socket(sock)), &sockaddr, &sockaddrlen) == win.SOCKET_ERROR { - err = Listen_Error(win.WSAGetLastError()) + err = _listen_error() return } @@ -136,7 +190,7 @@ _bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Network_Error) } @(private) -_accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { +_accept_tcp :: proc(sock: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (client: TCP_Socket, source: Endpoint, err: Accept_Error) { for { sockaddr: win.SOCKADDR_STORAGE_LH sockaddrlen := c.int(size_of(sockaddr)) @@ -150,7 +204,7 @@ _accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client // can do this to match the behaviour. continue } - err = Accept_Error(e) + err = _accept_error() return } client = TCP_Socket(client_sock) @@ -170,20 +224,20 @@ _close :: proc(socket: Any_Socket) { } @(private) -_recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { +_recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: TCP_Recv_Error) { if len(buf) <= 0 { return } res := win.recv(win.SOCKET(socket), raw_data(buf), c.int(len(buf)), 0) if res < 0 { - err = TCP_Recv_Error(win.WSAGetLastError()) + err = _tcp_recv_error() return } return int(res), nil } @(private) -_recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { +_recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: UDP_Recv_Error) { if len(buf) <= 0 { return } @@ -192,7 +246,7 @@ _recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_e fromsize := c.int(size_of(from)) res := win.recvfrom(win.SOCKET(socket), raw_data(buf), c.int(len(buf)), 0, &from, &fromsize) if res < 0 { - err = UDP_Recv_Error(win.WSAGetLastError()) + err = _udp_recv_error() return } @@ -202,13 +256,13 @@ _recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_e } @(private) -_send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { +_send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: TCP_Send_Error) { for bytes_written < len(buf) { limit := min(int(max(i32)), len(buf) - bytes_written) remaining := buf[bytes_written:] res := win.send(win.SOCKET(socket), raw_data(remaining), c.int(limit), 0) if res < 0 { - err = TCP_Send_Error(win.WSAGetLastError()) + err = _tcp_send_error() return } bytes_written += int(res) @@ -217,34 +271,34 @@ _send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: } @(private) -_send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { - if len(buf) > int(max(c.int)) { - // NOTE(tetra): If we don't guard this, we'll return (0, nil) instead, which is misleading. - err = .Message_Too_Long - return - } +_send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: UDP_Send_Error) { toaddr := _endpoint_to_sockaddr(to) - res := win.sendto(win.SOCKET(socket), raw_data(buf), c.int(len(buf)), 0, &toaddr, size_of(toaddr)) - if res < 0 { - err = UDP_Send_Error(win.WSAGetLastError()) - return + for bytes_written < len(buf) { + limit := min(int(max(i32)), len(buf) - bytes_written) + remaining := buf[bytes_written:] + res := win.sendto(win.SOCKET(socket), raw_data(remaining), c.int(limit), 0, &toaddr, size_of(toaddr)) + if res < 0 { + err = _udp_send_error() + return + } + + bytes_written += int(res) } - bytes_written = int(res) return } @(private) -_shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { +_shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) { s := any_socket_to_socket(socket) res := win.shutdown(win.SOCKET(s), c.int(manner)) if res < 0 { - return Shutdown_Error(win.WSAGetLastError()) + return _shutdown_error() } return } @(private) -_set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { +_set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error { level := win.SOL_SOCKET if option != .TCP_Nodelay else win.IPPROTO_TCP bool_value: b32 @@ -283,11 +337,8 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca t := value.(time.Duration) or_else panic("set_option() value must be a time.Duration here", loc) num_secs := i64(time.duration_seconds(t)) - if time.Duration(num_secs * 1e9) != t { - return .Linger_Only_Supports_Whole_Seconds - } if num_secs > i64(max(u16)) { - return .Value_Out_Of_Range + return .Invalid_Value } linger_value.l_onoff = 1 linger_value.l_linger = c.ushort(num_secs) @@ -323,19 +374,19 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca socket := any_socket_to_socket(s) res := win.setsockopt(win.SOCKET(socket), c.int(level), c.int(option), ptr, len) if res < 0 { - return Socket_Option_Error(win.WSAGetLastError()) + return _socket_option_error() } return nil } @(private) -_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Network_Error) { +_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) { socket := any_socket_to_socket(socket) arg: win.DWORD = 0 if should_block else 1 res := win.ioctlsocket(win.SOCKET(socket), transmute(win.c_long)win.FIONBIO, &arg) if res == win.SOCKET_ERROR { - return Set_Blocking_Error(win.WSAGetLastError()) + return _set_blocking_error() } return nil diff --git a/core/odin/doc-format/doc_format.odin b/core/odin/doc-format/doc_format.odin index c2d86a0ba..e6804c981 100644 --- a/core/odin/doc-format/doc_format.odin +++ b/core/odin/doc-format/doc_format.odin @@ -180,7 +180,7 @@ Type_Kind :: enum u32le { Struct = 10, Union = 11, Enum = 12, - Tuple = 13, + Parameters = 13, Proc = 14, Bit_Set = 15, Simd_Vector = 16, @@ -256,10 +256,10 @@ Type :: struct { types: Array(Type_Index), // Used by: - // .Named - 1 field for the definition - // .Struct - fields - // .Enum - fields - // .Tuple - parameters (procedures only) + // .Named - 1 field for the definition + // .Struct - fields + // .Enum - fields + // .Parameters - parameters (procedures only) entities: Array(Entity_Index), // Used By: .Struct, .Union diff --git a/core/odin/parser/file_tags.odin b/core/odin/parser/file_tags.odin index c5c6637c3..24aea3b9e 100644 --- a/core/odin/parser/file_tags.odin +++ b/core/odin/parser/file_tags.odin @@ -30,14 +30,27 @@ File_Tags :: struct { } @require_results -get_build_os_from_string :: proc(str: string) -> runtime.Odin_OS_Type { +get_build_os_from_string :: proc(str: string) -> (found_os: runtime.Odin_OS_Type, found_subtarget: runtime.Odin_Platform_Subtarget_Type) { + str_os, _, str_subtarget := strings.partition(str, ":") + fields := reflect.enum_fields_zipped(runtime.Odin_OS_Type) for os in fields { - if strings.equal_fold(os.name, str) { - return runtime.Odin_OS_Type(os.value) + if strings.equal_fold(os.name, str_os) { + found_os = runtime.Odin_OS_Type(os.value) + break } } - return .Unknown + if str_subtarget != "" { + st_fields := reflect.enum_fields_zipped(runtime.Odin_Platform_Subtarget_Type) + for subtarget in st_fields { + if strings.equal_fold(subtarget.name, str_subtarget) { + found_subtarget = runtime.Odin_Platform_Subtarget_Type(subtarget.value) + break + } + } + } + + return } @require_results get_build_arch_from_string :: proc(str: string) -> runtime.Odin_Arch_Type { @@ -187,7 +200,8 @@ parse_file_tags :: proc(file: ast.File, allocator := context.allocator) -> (tags if value == "ignore" { tags.ignore = true - } else if os := get_build_os_from_string(value); os != .Unknown { + } else if os, subtarget := get_build_os_from_string(value); os != .Unknown { + _ = subtarget // TODO(bill): figure out how to handle the subtarget logic if is_notted { os_negative += {os} } else { diff --git a/core/odin/parser/parser.odin b/core/odin/parser/parser.odin index 63c7e388f..7f1f4ca87 100644 --- a/core/odin/parser/parser.odin +++ b/core/odin/parser/parser.odin @@ -1276,28 +1276,28 @@ parse_unrolled_for_loop :: proc(p: ^Parser, inline_tok: tokenizer.Token) -> ^ast args = make([dynamic]^ast.Expr) for p.curr_tok.kind != .Close_Paren && p.curr_tok.kind != .EOF { - arg := parse_value(p) + arg := parse_value(p) - if p.curr_tok.kind == .Eq { - eq := expect_token(p, .Eq) - if arg != nil { - if _, ok := arg.derived.(^ast.Ident); !ok { - error(p, arg.pos, "expected an identifier for 'key=value'") - } - } - value := parse_value(p) - fv := ast.new(ast.Field_Value, arg.pos, value) - fv.field = arg - fv.sep = eq.pos - fv.value = value + if p.curr_tok.kind == .Eq { + eq := expect_token(p, .Eq) + if arg != nil { + if _, ok := arg.derived.(^ast.Ident); !ok { + error(p, arg.pos, "expected an identifier for 'key=value'") + } + } + value := parse_value(p) + fv := ast.new(ast.Field_Value, arg.pos, value) + fv.field = arg + fv.sep = eq.pos + fv.value = value - arg = fv - } + arg = fv + } - append(&args, arg) + append(&args, arg) allow_token(p, .Comma) or_break - } + } } p.expr_level -= 1 diff --git a/core/os/os2/allocators.odin b/core/os/os2/allocators.odin index 864532850..cedfbdee1 100644 --- a/core/os/os2/allocators.odin +++ b/core/os/os2/allocators.odin @@ -8,43 +8,13 @@ file_allocator :: proc() -> runtime.Allocator { return heap_allocator() } -temp_allocator_proc :: runtime.arena_allocator_proc - @(private="file") MAX_TEMP_ARENA_COUNT :: 2 - +@(private="file") +MAX_TEMP_ARENA_COLLISIONS :: MAX_TEMP_ARENA_COUNT - 1 @(private="file", thread_local) global_default_temp_allocator_arenas: [MAX_TEMP_ARENA_COUNT]runtime.Arena -@(private="file", thread_local) -global_default_temp_allocator_index: uint - - -@(require_results) -temp_allocator :: proc() -> runtime.Allocator { - arena := &global_default_temp_allocator_arenas[global_default_temp_allocator_index] - if arena.backing_allocator.procedure == nil { - arena.backing_allocator = heap_allocator() - } - - return runtime.Allocator{ - procedure = temp_allocator_proc, - data = arena, - } -} - - - -@(require_results) -temp_allocator_temp_begin :: proc(loc := #caller_location) -> (temp: runtime.Arena_Temp) { - temp = runtime.arena_temp_begin(&global_default_temp_allocator_arenas[global_default_temp_allocator_index], loc) - return -} - -temp_allocator_temp_end :: proc(temp: runtime.Arena_Temp, loc := #caller_location) { - runtime.arena_temp_end(temp, loc) -} - @(fini, private) temp_allocator_fini :: proc() { for &arena in global_default_temp_allocator_arenas { @@ -53,18 +23,49 @@ temp_allocator_fini :: proc() { global_default_temp_allocator_arenas = {} } -TEMP_ALLOCATOR_GUARD_END :: proc(temp: runtime.Arena_Temp, loc := #caller_location) { - runtime.arena_temp_end(temp, loc) - if temp.arena != nil { - global_default_temp_allocator_index = (global_default_temp_allocator_index-1)%MAX_TEMP_ARENA_COUNT - } +Temp_Allocator :: struct { + using arena: ^runtime.Arena, + using allocator: runtime.Allocator, + tmp: runtime.Arena_Temp, + loc: runtime.Source_Code_Location, +} + +TEMP_ALLOCATOR_GUARD_END :: proc(temp: Temp_Allocator) { + runtime.arena_temp_end(temp.tmp, temp.loc) } @(deferred_out=TEMP_ALLOCATOR_GUARD_END) -TEMP_ALLOCATOR_GUARD :: #force_inline proc(loc := #caller_location) -> (runtime.Arena_Temp, runtime.Source_Code_Location) { - global_default_temp_allocator_index = (global_default_temp_allocator_index+1)%MAX_TEMP_ARENA_COUNT - tmp := temp_allocator_temp_begin(loc) - return tmp, loc +TEMP_ALLOCATOR_GUARD :: #force_inline proc(collisions: []runtime.Allocator, loc := #caller_location) -> Temp_Allocator { + assert(len(collisions) <= MAX_TEMP_ARENA_COLLISIONS, "Maximum collision count exceeded. MAX_TEMP_ARENA_COUNT must be increased!") + good_arena: ^runtime.Arena + for i in 0.. (runtime.Arena_Temp) { + return temp_allocator_begin(tmp.arena) +} +@(private="file") +_temp_allocator_end :: proc(tmp: runtime.Arena_Temp) { + temp_allocator_end(tmp) } @(init, private) diff --git a/core/os/os2/dir.odin b/core/os/os2/dir.odin index 4a7762ded..10b06a8ce 100644 --- a/core/os/os2/dir.odin +++ b/core/os/os2/dir.odin @@ -2,6 +2,7 @@ package os2 import "base:runtime" import "core:slice" +import "core:strings" read_dir :: read_directory @@ -18,12 +19,12 @@ read_directory :: proc(f: ^File, n: int, allocator: runtime.Allocator) -> (files size = 100 } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) it := read_directory_iterator_create(f) defer _read_directory_iterator_destroy(&it) - dfi := make([dynamic]File_Info, 0, size, temp_allocator()) + dfi := make([dynamic]File_Info, 0, size, temp_allocator) defer if err != nil { for fi in dfi { file_info_delete(fi, allocator) @@ -192,3 +193,56 @@ read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, return _read_directory_iterator(it) } + +// Recursively copies a directory to `dst` from `src` +copy_directory_all :: proc(dst, src: string, dst_perm := 0o755) -> Error { + when #defined(_copy_directory_all_native) { + return _copy_directory_all_native(dst, src, dst_perm) + } else { + return _copy_directory_all(dst, src, dst_perm) + } +} + +@(private) +_copy_directory_all :: proc(dst, src: string, dst_perm := 0o755) -> Error { + err := make_directory(dst, dst_perm) + if err != nil && err != .Exist { + return err + } + + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + + abs_src := get_absolute_path(src, temp_allocator) or_return + abs_dst := get_absolute_path(dst, temp_allocator) or_return + + dst_buf := make([dynamic]byte, 0, len(abs_dst) + 256, temp_allocator) or_return + + w: Walker + walker_init_path(&w, src) + defer walker_destroy(&w) + + for info in walker_walk(&w) { + _ = walker_error(&w) or_break + + rel := strings.trim_prefix(info.fullpath, abs_src) + + non_zero_resize(&dst_buf, 0) + reserve(&dst_buf, len(abs_dst) + len(Path_Separator_String) + len(rel)) or_return + append(&dst_buf, abs_dst) + append(&dst_buf, Path_Separator_String) + append(&dst_buf, rel) + + if info.type == .Directory { + err = make_directory(string(dst_buf[:]), dst_perm) + if err != nil && err != .Exist { + return err + } + } else { + copy_file(string(dst_buf[:]), info.fullpath) or_return + } + } + + _ = walker_error(&w) or_return + + return nil +} diff --git a/core/os/os2/dir_linux.odin b/core/os/os2/dir_linux.odin index a868a02c4..34346c02f 100644 --- a/core/os/os2/dir_linux.odin +++ b/core/os/os2/dir_linux.odin @@ -78,7 +78,8 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info it.impl.prev_fi = fi if err != nil { - path, _ := _get_full_path(entry_fd, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + path, _ := _get_full_path(entry_fd, temp_allocator) read_directory_iterator_set_error(it, path, err) } diff --git a/core/os/os2/dir_posix_darwin.odin b/core/os/os2/dir_posix_darwin.odin new file mode 100644 index 000000000..3cae50d25 --- /dev/null +++ b/core/os/os2/dir_posix_darwin.odin @@ -0,0 +1,17 @@ +#+private +package os2 + +import "core:sys/darwin" + +_copy_directory_all_native :: proc(dst, src: string, dst_perm := 0o755) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + + csrc := clone_to_cstring(src, temp_allocator) or_return + cdst := clone_to_cstring(dst, temp_allocator) or_return + + if darwin.copyfile(csrc, cdst, nil, darwin.COPYFILE_ALL + {.RECURSIVE}) < 0 { + err = _get_platform_error() + } + + return +} diff --git a/core/os/os2/dir_windows.odin b/core/os/os2/dir_windows.odin index d592e8036..4cf1f8396 100644 --- a/core/os/os2/dir_windows.odin +++ b/core/os/os2/dir_windows.odin @@ -14,7 +14,9 @@ find_data_to_file_info :: proc(base_path: string, d: ^win32.WIN32_FIND_DATAW, al if d.cFileName[0] == '.' && d.cFileName[1] == '.' && d.cFileName[2] == 0 { return } - path := concatenate({base_path, `\`, win32_utf16_to_utf8(d.cFileName[:], temp_allocator()) or_else ""}, allocator) or_return + + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + path := concatenate({base_path, `\`, win32_wstring_to_utf8(raw_data(d.cFileName[:]), temp_allocator) or_else ""}, allocator) or_return handle := win32.HANDLE(_open_internal(path, {.Read}, 0o666) or_else 0) defer win32.CloseHandle(handle) @@ -49,8 +51,6 @@ Read_Directory_Iterator_Impl :: struct { @(require_results) _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) { - TEMP_ALLOCATOR_GUARD() - for !it.impl.no_more_files { err: Error file_info_delete(it.impl.prev_fi, file_allocator()) @@ -116,9 +116,9 @@ _read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) { wpath = impl.wname[:i] } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - wpath_search := make([]u16, len(wpath)+3, temp_allocator()) + wpath_search := make([]u16, len(wpath)+3, temp_allocator) copy(wpath_search, wpath) wpath_search[len(wpath)+0] = '\\' wpath_search[len(wpath)+1] = '*' diff --git a/core/os/os2/env_posix.odin b/core/os/os2/env_posix.odin index 9661768b4..13682f76b 100644 --- a/core/os/os2/env_posix.odin +++ b/core/os/os2/env_posix.odin @@ -12,9 +12,9 @@ _lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string return } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - ckey := strings.clone_to_cstring(key, temp_allocator()) + ckey := strings.clone_to_cstring(key, temp_allocator) cval := posix.getenv(ckey) if cval == nil { return @@ -27,10 +27,10 @@ _lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string } _set_env :: proc(key, value: string) -> (err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - ckey := strings.clone_to_cstring(key, temp_allocator()) or_return - cval := strings.clone_to_cstring(key, temp_allocator()) or_return + ckey := strings.clone_to_cstring(key, temp_allocator) or_return + cval := strings.clone_to_cstring(value, temp_allocator) or_return if posix.setenv(ckey, cval, true) != nil { err = _get_platform_error_from_errno() @@ -39,9 +39,9 @@ _set_env :: proc(key, value: string) -> (err: Error) { } _unset_env :: proc(key: string) -> (ok: bool) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - ckey := strings.clone_to_cstring(key, temp_allocator()) + ckey := strings.clone_to_cstring(key, temp_allocator) ok = posix.unsetenv(ckey) == .OK return diff --git a/core/os/os2/env_wasi.odin b/core/os/os2/env_wasi.odin index 305192c92..faa54e36b 100644 --- a/core/os/os2/env_wasi.odin +++ b/core/os/os2/env_wasi.odin @@ -39,9 +39,9 @@ build_env :: proc() -> (err: Error) { g_env_buf = make([]byte, size_of_envs, file_allocator()) or_return defer if err != nil { delete(g_env_buf, file_allocator()) } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - envs := make([]cstring, num_envs, temp_allocator()) or_return + envs := make([]cstring, num_envs, temp_allocator) or_return _err = wasi.environ_get(raw_data(envs), raw_data(g_env_buf)) if _err != nil { diff --git a/core/os/os2/env_windows.odin b/core/os/os2/env_windows.odin index 3ac26a261..6bfde34bb 100644 --- a/core/os/os2/env_windows.odin +++ b/core/os/os2/env_windows.odin @@ -8,8 +8,8 @@ _lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string if key == "" { return } - TEMP_ALLOCATOR_GUARD() - wkey, _ := win32_utf8_to_wstring(key, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + wkey, _ := win32_utf8_to_wstring(key, temp_allocator) n := win32.GetEnvironmentVariableW(wkey, nil, 0) if n == 0 { @@ -20,7 +20,7 @@ _lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string return "", true } - b := make([]u16, n+1, temp_allocator()) + b := make([]u16, n+1, temp_allocator) n = win32.GetEnvironmentVariableW(wkey, raw_data(b), u32(len(b))) if n == 0 { @@ -37,9 +37,9 @@ _lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string } _set_env :: proc(key, value: string) -> Error { - TEMP_ALLOCATOR_GUARD() - k := win32_utf8_to_wstring(key, temp_allocator()) or_return - v := win32_utf8_to_wstring(value, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + k := win32_utf8_to_wstring(key, temp_allocator) or_return + v := win32_utf8_to_wstring(value, temp_allocator) or_return if !win32.SetEnvironmentVariableW(k, v) { return _get_platform_error() @@ -48,14 +48,14 @@ _set_env :: proc(key, value: string) -> Error { } _unset_env :: proc(key: string) -> bool { - TEMP_ALLOCATOR_GUARD() - k, _ := win32_utf8_to_wstring(key, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + k, _ := win32_utf8_to_wstring(key, temp_allocator) return bool(win32.SetEnvironmentVariableW(k, nil)) } _clear_env :: proc() { - TEMP_ALLOCATOR_GUARD() - envs, _ := environ(temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + envs, _ := environ(temp_allocator) for env in envs { for j in 1.. bool { @(require_results) is_file :: proc(path: string) -> bool { - TEMP_ALLOCATOR_GUARD() - fi, err := stat(path, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + fi, err := stat(path, temp_allocator) if err != nil { return false } @@ -303,8 +303,8 @@ is_dir :: is_directory @(require_results) is_directory :: proc(path: string) -> bool { - TEMP_ALLOCATOR_GUARD() - fi, err := stat(path, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + fi, err := stat(path, temp_allocator) if err != nil { return false } @@ -313,6 +313,15 @@ is_directory :: proc(path: string) -> bool { copy_file :: proc(dst_path, src_path: string) -> Error { + when #defined(_copy_file_native) { + return _copy_file_native(dst_path, src_path) + } else { + return _copy_file(dst_path, src_path) + } +} + +@(private) +_copy_file :: proc(dst_path, src_path: string) -> Error { src := open(src_path) or_return defer close(src) diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index 811ee7055..a1ead7f9f 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -66,8 +66,8 @@ _standard_stream_init :: proc() { } _open :: proc(name: string, flags: File_Flags, perm: int) -> (f: ^File, err: Error) { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return // Just default to using O_NOCTTY because needing to open a controlling // terminal would be incredibly rare. This has no effect on files while @@ -299,8 +299,8 @@ _truncate :: proc(f: ^File, size: i64) -> Error { } _remove :: proc(name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return if fd, errno := linux.open(name_cstr, _OPENDIR_FLAGS + {.NOFOLLOW}); errno == .NONE { linux.close(fd) @@ -311,25 +311,25 @@ _remove :: proc(name: string) -> Error { } _rename :: proc(old_name, new_name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - old_name_cstr := temp_cstring(old_name) or_return - new_name_cstr := temp_cstring(new_name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + old_name_cstr := clone_to_cstring(old_name, temp_allocator) or_return + new_name_cstr := clone_to_cstring(new_name, temp_allocator) or_return return _get_platform_error(linux.rename(old_name_cstr, new_name_cstr)) } _link :: proc(old_name, new_name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - old_name_cstr := temp_cstring(old_name) or_return - new_name_cstr := temp_cstring(new_name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + old_name_cstr := clone_to_cstring(old_name, temp_allocator) or_return + new_name_cstr := clone_to_cstring(new_name, temp_allocator) or_return return _get_platform_error(linux.link(old_name_cstr, new_name_cstr)) } _symlink :: proc(old_name, new_name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - old_name_cstr := temp_cstring(old_name) or_return - new_name_cstr := temp_cstring(new_name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + old_name_cstr := clone_to_cstring(old_name, temp_allocator) or_return + new_name_cstr := clone_to_cstring(new_name, temp_allocator) or_return return _get_platform_error(linux.symlink(old_name_cstr, new_name_cstr)) } @@ -352,14 +352,14 @@ _read_link_cstr :: proc(name_cstr: cstring, allocator: runtime.Allocator) -> (st } _read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, e: Error) { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + name_cstr := clone_to_cstring(name, temp_allocator) or_return return _read_link_cstr(name_cstr, allocator) } _chdir :: proc(name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return return _get_platform_error(linux.chdir(name_cstr)) } @@ -369,8 +369,8 @@ _fchdir :: proc(f: ^File) -> Error { } _chmod :: proc(name: string, mode: int) -> Error { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return return _get_platform_error(linux.chmod(name_cstr, transmute(linux.Mode)(u32(mode)))) } @@ -381,15 +381,15 @@ _fchmod :: proc(f: ^File, mode: int) -> Error { // NOTE: will throw error without super user priviledges _chown :: proc(name: string, uid, gid: int) -> Error { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return return _get_platform_error(linux.chown(name_cstr, linux.Uid(uid), linux.Gid(gid))) } // NOTE: will throw error without super user priviledges _lchown :: proc(name: string, uid, gid: int) -> Error { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return return _get_platform_error(linux.lchown(name_cstr, linux.Uid(uid), linux.Gid(gid))) } @@ -400,8 +400,8 @@ _fchown :: proc(f: ^File, uid, gid: int) -> Error { } _chtimes :: proc(name: string, atime, mtime: time.Time) -> Error { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr := clone_to_cstring(name, temp_allocator) or_return times := [2]linux.Time_Spec { { uint(atime._nsec) / uint(time.Second), @@ -431,8 +431,8 @@ _fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error { } _exists :: proc(name: string) -> bool { - TEMP_ALLOCATOR_GUARD() - name_cstr, _ := temp_cstring(name) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + name_cstr, _ := clone_to_cstring(name, temp_allocator) return linux.access(name_cstr, linux.F_OK) == .NONE } @@ -440,8 +440,8 @@ _exists :: proc(name: string) -> bool { _read_entire_pseudo_file :: proc { _read_entire_pseudo_file_string, _read_entire_pseudo_file_cstring } _read_entire_pseudo_file_string :: proc(name: string, allocator: runtime.Allocator) -> (b: []u8, e: Error) { - TEMP_ALLOCATOR_GUARD() - name_cstr := clone_to_cstring(name, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + name_cstr := clone_to_cstring(name, temp_allocator) or_return return _read_entire_pseudo_file_cstring(name_cstr, allocator) } diff --git a/core/os/os2/file_posix.odin b/core/os/os2/file_posix.odin index 43d5866b1..2d74618ee 100644 --- a/core/os/os2/file_posix.odin +++ b/core/os/os2/file_posix.odin @@ -69,8 +69,8 @@ _open :: proc(name: string, flags: File_Flags, perm: int) -> (f: ^File, err: Err if .Trunc in flags { sys_flags += {.TRUNC} } if .Inheritable in flags { sys_flags -= {.CLOEXEC} } - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return fd := posix.open(cname, sys_flags, transmute(posix.mode_t)posix._mode_t(perm)) if fd < 0 { @@ -183,39 +183,39 @@ _truncate :: proc(f: ^File, size: i64) -> Error { return nil } -_remove :: proc(name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) +_remove :: proc(name: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.remove(cname) != 0 { return _get_platform_error() } return nil } -_rename :: proc(old_path, new_path: string) -> Error { - TEMP_ALLOCATOR_GUARD() - cold := temp_cstring(old_path) - cnew := temp_cstring(new_path) +_rename :: proc(old_path, new_path: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cold := clone_to_cstring(old_path, temp_allocator) or_return + cnew := clone_to_cstring(new_path, temp_allocator) or_return if posix.rename(cold, cnew) != 0 { return _get_platform_error() } return nil } -_link :: proc(old_name, new_name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - cold := temp_cstring(old_name) - cnew := temp_cstring(new_name) +_link :: proc(old_name, new_name: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cold := clone_to_cstring(old_name, temp_allocator) or_return + cnew := clone_to_cstring(new_name, temp_allocator) or_return if posix.link(cold, cnew) != .OK { return _get_platform_error() } return nil } -_symlink :: proc(old_name, new_name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - cold := temp_cstring(old_name) - cnew := temp_cstring(new_name) +_symlink :: proc(old_name, new_name: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cold := clone_to_cstring(old_name, temp_allocator) or_return + cnew := clone_to_cstring(new_name, temp_allocator) or_return if posix.symlink(cold, cnew) != .OK { return _get_platform_error() } @@ -223,8 +223,8 @@ _symlink :: proc(old_name, new_name: string) -> Error { } _read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, err: Error) { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + cname := clone_to_cstring(name, temp_allocator) or_return buf: [dynamic]byte buf.allocator = allocator @@ -268,9 +268,9 @@ _read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, er } } -_chdir :: proc(name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) +_chdir :: proc(name: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.chdir(cname) != .OK { return _get_platform_error() } @@ -291,9 +291,9 @@ _fchmod :: proc(f: ^File, mode: int) -> Error { return nil } -_chmod :: proc(name: string, mode: int) -> Error { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) +_chmod :: proc(name: string, mode: int) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.chmod(cname, transmute(posix.mode_t)posix._mode_t(mode)) != .OK { return _get_platform_error() } @@ -307,9 +307,9 @@ _fchown :: proc(f: ^File, uid, gid: int) -> Error { return nil } -_chown :: proc(name: string, uid, gid: int) -> Error { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) +_chown :: proc(name: string, uid, gid: int) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.chown(cname, posix.uid_t(uid), posix.gid_t(gid)) != .OK { return _get_platform_error() } @@ -317,15 +317,15 @@ _chown :: proc(name: string, uid, gid: int) -> Error { } _lchown :: proc(name: string, uid, gid: int) -> Error { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.lchown(cname, posix.uid_t(uid), posix.gid_t(gid)) != .OK { return _get_platform_error() } return nil } -_chtimes :: proc(name: string, atime, mtime: time.Time) -> Error { +_chtimes :: proc(name: string, atime, mtime: time.Time) -> (err: Error) { times := [2]posix.timeval{ { tv_sec = posix.time_t(atime._nsec/1e9), /* seconds */ @@ -337,8 +337,8 @@ _chtimes :: proc(name: string, atime, mtime: time.Time) -> Error { }, } - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.utimes(cname, ×) != .OK { return _get_platform_error() @@ -365,8 +365,9 @@ _fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error { } _exists :: proc(path: string) -> bool { - TEMP_ALLOCATOR_GUARD() - cpath := temp_cstring(path) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cpath, err := clone_to_cstring(path, temp_allocator) + if err != nil { return false } return posix.access(cpath) == .OK } diff --git a/core/os/os2/file_posix_darwin.odin b/core/os/os2/file_posix_darwin.odin index 920a63a71..aed3e56f5 100644 --- a/core/os/os2/file_posix_darwin.odin +++ b/core/os/os2/file_posix_darwin.odin @@ -3,6 +3,7 @@ package os2 import "base:runtime" +import "core:sys/darwin" import "core:sys/posix" _posix_absolute_path :: proc(fd: posix.FD, name: string, allocator: runtime.Allocator) -> (path: cstring, err: Error) { @@ -16,3 +17,30 @@ _posix_absolute_path :: proc(fd: posix.FD, name: string, allocator: runtime.Allo return clone_to_cstring(string(cstring(&buf[0])), allocator) } + +_copy_file_native :: proc(dst_path, src_path: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + + csrc := clone_to_cstring(src_path, temp_allocator) or_return + cdst := clone_to_cstring(dst_path, temp_allocator) or_return + + // Disallow directories, as specified by the generic implementation. + + stat: posix.stat_t + if posix.stat(csrc, &stat) != .OK { + err = _get_platform_error() + return + } + + if posix.S_ISDIR(stat.st_mode) { + err = .Invalid_File + return + } + + ret := darwin.copyfile(csrc, cdst, nil, darwin.COPYFILE_ALL) + if ret < 0 { + err = _get_platform_error() + } + + return +} diff --git a/core/os/os2/file_posix_other.odin b/core/os/os2/file_posix_other.odin index 74b6374ec..d2946098b 100644 --- a/core/os/os2/file_posix_other.odin +++ b/core/os/os2/file_posix_other.odin @@ -7,8 +7,8 @@ import "base:runtime" import "core:sys/posix" _posix_absolute_path :: proc(fd: posix.FD, name: string, allocator: runtime.Allocator) -> (path: cstring, err: Error) { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + cname := clone_to_cstring(name, temp_allocator) buf: [posix.PATH_MAX]byte path = posix.realpath(cname, raw_data(buf[:])) diff --git a/core/os/os2/file_windows.odin b/core/os/os2/file_windows.odin index b123330e0..40d012183 100644 --- a/core/os/os2/file_windows.odin +++ b/core/os/os2/file_windows.odin @@ -86,9 +86,9 @@ _open_internal :: proc(name: string, flags: File_Flags, perm: int) -> (handle: u err = .Not_Exist return } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - path := _fix_long_path(name, temp_allocator()) or_return + path := _fix_long_path(name, temp_allocator) or_return access: u32 switch flags & {.Read, .Write} { case {.Read}: access = win32.FILE_GENERIC_READ @@ -506,10 +506,16 @@ _write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (n: i64, err: Error) _file_size :: proc(f: ^File_Impl) -> (n: i64, err: Error) { length: win32.LARGE_INTEGER - if f.kind == .Pipe { - return 0, .No_Size - } handle := _handle(&f.file) + if f.kind == .Pipe { + bytes_available: u32 + if win32.PeekNamedPipe(handle, nil, 0, nil, &bytes_available, nil) { + return i64(bytes_available), nil + } else { + err = _get_platform_error() + return + } + } if !win32.GetFileSizeEx(handle, &length) { err = _get_platform_error() } @@ -551,8 +557,8 @@ _truncate :: proc(f: ^File, size: i64) -> Error { } _remove :: proc(name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - p := _fix_long_path(name, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + p := _fix_long_path(name, temp_allocator) or_return err, err1: Error if !win32.DeleteFileW(p) { err = _get_platform_error() @@ -589,9 +595,9 @@ _remove :: proc(name: string) -> Error { } _rename :: proc(old_path, new_path: string) -> Error { - TEMP_ALLOCATOR_GUARD() - from := _fix_long_path(old_path, temp_allocator()) or_return - to := _fix_long_path(new_path, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + from := _fix_long_path(old_path, temp_allocator) or_return + to := _fix_long_path(new_path, temp_allocator) or_return if win32.MoveFileExW(from, to, win32.MOVEFILE_REPLACE_EXISTING) { return nil } @@ -600,9 +606,9 @@ _rename :: proc(old_path, new_path: string) -> Error { } _link :: proc(old_name, new_name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - o := _fix_long_path(old_name, temp_allocator()) or_return - n := _fix_long_path(new_name, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + o := _fix_long_path(old_name, temp_allocator) or_return + n := _fix_long_path(new_name, temp_allocator) or_return if win32.CreateHardLinkW(n, o, nil) { return nil } @@ -663,9 +669,9 @@ _normalize_link_path :: proc(p: []u16, allocator: runtime.Allocator) -> (str: st return "", _get_platform_error() } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buf := make([]u16, n+1, temp_allocator()) + buf := make([]u16, n+1, temp_allocator) n = win32.GetFinalPathNameByHandleW(handle, raw_data(buf), u32(len(buf)), win32.VOLUME_NAME_DOS) if n == 0 { return "", _get_platform_error() @@ -689,9 +695,9 @@ _read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, er @thread_local rdb_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - p := _fix_long_path(name, temp_allocator()) or_return + p := _fix_long_path(name, temp_allocator) or_return handle := _open_sym_link(p) or_return defer win32.CloseHandle(handle) @@ -766,8 +772,8 @@ _fchown :: proc(f: ^File, uid, gid: int) -> Error { } _chdir :: proc(name: string) -> Error { - TEMP_ALLOCATOR_GUARD() - p := _fix_long_path(name, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + p := _fix_long_path(name, temp_allocator) or_return if !win32.SetCurrentDirectoryW(p) { return _get_platform_error() } @@ -794,19 +800,11 @@ _chtimes :: proc(name: string, atime, mtime: time.Time) -> Error { defer close(f) return _fchtimes(f, atime, mtime) } + _fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error { if f == nil || f.impl == nil { return nil } - d: win32.BY_HANDLE_FILE_INFORMATION - if !win32.GetFileInformationByHandle(_handle(f), &d) { - return _get_platform_error() - } - - to_windows_time :: #force_inline proc(t: time.Time) -> win32.LARGE_INTEGER { - // a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC) - return win32.LARGE_INTEGER(time.time_to_unix_nano(t) * 100 + 116444736000000000) - } atime, mtime := atime, mtime if time.time_to_unix_nano(atime) < time.time_to_unix_nano(mtime) { @@ -814,17 +812,17 @@ _fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error { } info: win32.FILE_BASIC_INFO - info.LastAccessTime = to_windows_time(atime) - info.LastWriteTime = to_windows_time(mtime) - if !win32.SetFileInformationByHandle(_handle(f), .FileBasicInfo, &info, size_of(d)) { + info.LastAccessTime = time_as_filetime(atime) + info.LastWriteTime = time_as_filetime(mtime) + if !win32.SetFileInformationByHandle(_handle(f), .FileBasicInfo, &info, size_of(info)) { return _get_platform_error() } return nil } _exists :: proc(path: string) -> bool { - TEMP_ALLOCATOR_GUARD() - wpath, _ := _fix_long_path(path, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + wpath, _ := _fix_long_path(path, temp_allocator) attribs := win32.GetFileAttributesW(wpath) return attribs != win32.INVALID_FILE_ATTRIBUTES } diff --git a/core/os/os2/internal_util.odin b/core/os/os2/internal_util.odin index ce253d17b..9616af8b0 100644 --- a/core/os/os2/internal_util.odin +++ b/core/os/os2/internal_util.odin @@ -43,11 +43,6 @@ clone_to_cstring :: proc(s: string, allocator: runtime.Allocator) -> (res: cstri return cstring(&buf[0]), nil } -@(require_results) -temp_cstring :: proc(s: string) -> (cstring, runtime.Allocator_Error) #optional_allocator_error { - return clone_to_cstring(s, temp_allocator()) -} - @(require_results) string_from_null_terminated_bytes :: proc(b: []byte) -> (res: string) { s := string(b) diff --git a/core/os/os2/path.odin b/core/os/os2/path.odin index 47ac0236d..e12aa3c9c 100644 --- a/core/os/os2/path.odin +++ b/core/os/os2/path.odin @@ -119,11 +119,11 @@ clean_path :: proc(path: string, allocator: runtime.Allocator) -> (cleaned: stri return strings.clone(".", allocator) } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) // The extra byte is to simplify appending path elements by letting the // loop to end each with a separator. We'll trim the last one when we're done. - buffer := make([]u8, len(path) + 1, temp_allocator()) or_return + buffer := make([]u8, len(path) + 1, temp_allocator) or_return // This is the only point where Windows and POSIX differ, as Windows has // alphabet-based volumes for root paths. @@ -326,8 +326,8 @@ For example, `join_path({"/home", "foo", "bar.txt"})` will result in `"/home/foo join_path :: proc(elems: []string, allocator: runtime.Allocator) -> (joined: string, err: Error) { for e, i in elems { if e != "" { - TEMP_ALLOCATOR_GUARD() - p := strings.join(elems[i:], Path_Separator_String, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + p := strings.join(elems[i:], Path_Separator_String, temp_allocator) or_return return clean_path(p, allocator) } } diff --git a/core/os/os2/path_freebsd.odin b/core/os/os2/path_freebsd.odin index 577108eca..e7e4f63c9 100644 --- a/core/os/os2/path_freebsd.odin +++ b/core/os/os2/path_freebsd.odin @@ -25,5 +25,5 @@ _get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err return } - return string(buf[:size]), nil + return string(buf[:size-1]), nil } diff --git a/core/os/os2/path_linux.odin b/core/os/os2/path_linux.odin index 410b4cb28..8b185f419 100644 --- a/core/os/os2/path_linux.odin +++ b/core/os/os2/path_linux.odin @@ -18,8 +18,8 @@ _is_path_separator :: proc(c: byte) -> bool { } _mkdir :: proc(path: string, perm: int) -> Error { - TEMP_ALLOCATOR_GUARD() - path_cstr := temp_cstring(path) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + path_cstr := clone_to_cstring(path, temp_allocator) or_return return _get_platform_error(linux.mkdir(path_cstr, transmute(linux.Mode)u32(perm))) } @@ -52,9 +52,9 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { } return _get_platform_error(errno) } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) // need something we can edit, and use to generate cstrings - path_bytes := make([]u8, len(path) + 1, temp_allocator()) + path_bytes := make([]u8, len(path) + 1, temp_allocator) // zero terminate the byte slice to make it a valid cstring copy(path_bytes, path) @@ -129,8 +129,8 @@ _remove_all :: proc(path: string) -> Error { return nil } - TEMP_ALLOCATOR_GUARD() - path_cstr := temp_cstring(path) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + path_cstr := clone_to_cstring(path, temp_allocator) or_return fd, errno := linux.open(path_cstr, _OPENDIR_FLAGS) #partial switch errno { @@ -168,14 +168,16 @@ _get_working_directory :: proc(allocator: runtime.Allocator) -> (string, Error) } _set_working_directory :: proc(dir: string) -> Error { - dir_cstr := temp_cstring(dir) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + + dir_cstr := clone_to_cstring(dir, temp_allocator) or_return return _get_platform_error(linux.chdir(dir_cstr)) } _get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buf := make([dynamic]byte, 1024, temp_allocator()) or_return + buf := make([dynamic]byte, 1024, temp_allocator) or_return for { n, errno := linux.readlink("/proc/self/exe", buf[:]) if errno != .NONE { @@ -205,3 +207,21 @@ _get_full_path :: proc(fd: linux.Fd, allocator: runtime.Allocator) -> (fullpath: } return } + +_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) { + rel := path + if rel == "" { + rel = "." + } + + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + + fd, errno := linux.open(clone_to_cstring(path, temp_allocator) or_return, {}) + if errno != nil { + err = _get_platform_error(errno) + return + } + defer linux.close(fd) + + return _get_full_path(fd, allocator) +} diff --git a/core/os/os2/path_netbsd.odin b/core/os/os2/path_netbsd.odin index f56a91fd6..815102dea 100644 --- a/core/os/os2/path_netbsd.odin +++ b/core/os/os2/path_netbsd.odin @@ -5,9 +5,9 @@ import "base:runtime" import "core:sys/posix" _get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buf := make([dynamic]byte, 1024, temp_allocator()) or_return + buf := make([dynamic]byte, 1024, temp_allocator) or_return for { n := posix.readlink("/proc/curproc/exe", raw_data(buf), len(buf)) if n < 0 { diff --git a/core/os/os2/path_openbsd.odin b/core/os/os2/path_openbsd.odin index 37b5de927..cbc0346d4 100644 --- a/core/os/os2/path_openbsd.odin +++ b/core/os/os2/path_openbsd.odin @@ -35,11 +35,11 @@ _get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err return real(arg, allocator) } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buf := strings.builder_make(temp_allocator()) + buf := strings.builder_make(temp_allocator) - paths := get_env("PATH", temp_allocator()) + paths := get_env("PATH", temp_allocator) for dir in strings.split_iterator(&paths, ":") { strings.builder_reset(&buf) strings.write_string(&buf, dir) diff --git a/core/os/os2/path_posix.odin b/core/os/os2/path_posix.odin index 39bd0a188..f22cd446b 100644 --- a/core/os/os2/path_posix.odin +++ b/core/os/os2/path_posix.odin @@ -14,9 +14,9 @@ _is_path_separator :: proc(c: byte) -> bool { return c == _Path_Separator } -_mkdir :: proc(name: string, perm: int) -> Error { - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) +_mkdir :: proc(name: string, perm: int) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cname := clone_to_cstring(name, temp_allocator) or_return if posix.mkdir(cname, transmute(posix.mode_t)posix._mode_t(perm)) != .OK { return _get_platform_error() } @@ -28,13 +28,13 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { return .Invalid_Path } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) if exists(path) { return .Exist } - clean_path := clean_path(path, temp_allocator()) or_return + clean_path := clean_path(path, temp_allocator) or_return return internal_mkdir_all(clean_path, perm) internal_mkdir_all :: proc(path: string, perm: int) -> Error { @@ -52,9 +52,9 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { } } -_remove_all :: proc(path: string) -> Error { - TEMP_ALLOCATOR_GUARD() - cpath := temp_cstring(path) +_remove_all :: proc(path: string) -> (err: Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cpath := clone_to_cstring(path, temp_allocator) or_return dir := posix.opendir(cpath) if dir == nil { @@ -78,7 +78,7 @@ _remove_all :: proc(path: string) -> Error { continue } - fullpath, _ := concatenate({path, "/", string(cname), "\x00"}, temp_allocator()) + fullpath, _ := concatenate({path, "/", string(cname), "\x00"}, temp_allocator) if entry.d_type == .DIR { _remove_all(fullpath[:len(fullpath)-1]) or_return } else { @@ -95,10 +95,10 @@ _remove_all :: proc(path: string) -> Error { } _get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) buf: [dynamic]byte - buf.allocator = temp_allocator() + buf.allocator = temp_allocator size := uint(posix.PATH_MAX) cwd: cstring @@ -116,10 +116,27 @@ _get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, er } _set_working_directory :: proc(dir: string) -> (err: Error) { - TEMP_ALLOCATOR_GUARD() - cdir := temp_cstring(dir) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + cdir := clone_to_cstring(dir, temp_allocator) or_return if posix.chdir(cdir) != .OK { err = _get_platform_error() } return } + +_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) { + rel := path + if rel == "" { + rel = "." + } + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + rel_cstr := clone_to_cstring(rel, temp_allocator) or_return + path_ptr := posix.realpath(rel_cstr, nil) + if path_ptr == nil { + return "", Platform_Error(posix.errno()) + } + defer posix.free(path_ptr) + + path_str := clone_string(string(path_ptr), allocator) or_return + return path_str, nil +} diff --git a/core/os/os2/path_posixfs.odin b/core/os/os2/path_posixfs.odin index 8f9d43d63..0736e73d1 100644 --- a/core/os/os2/path_posixfs.odin +++ b/core/os/os2/path_posixfs.odin @@ -4,10 +4,6 @@ package os2 // This implementation is for all systems that have POSIX-compliant filesystem paths. -import "base:runtime" -import "core:strings" -import "core:sys/posix" - _are_paths_identical :: proc(a, b: string) -> (identical: bool) { return a == b } @@ -26,23 +22,6 @@ _is_absolute_path :: proc(path: string) -> bool { return len(path) > 0 && _is_path_separator(path[0]) } -_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) { - rel := path - if rel == "" { - rel = "." - } - TEMP_ALLOCATOR_GUARD() - rel_cstr := strings.clone_to_cstring(rel, temp_allocator()) - path_ptr := posix.realpath(rel_cstr, nil) - if path_ptr == nil { - return "", Platform_Error(posix.errno()) - } - defer posix.free(path_ptr) - - path_str := strings.clone(string(path_ptr), allocator) - return path_str, nil -} - _get_relative_path_handle_start :: proc(base, target: string) -> bool { base_rooted := len(base) > 0 && _is_path_separator(base[0]) target_rooted := len(target) > 0 && _is_path_separator(target[0]) diff --git a/core/os/os2/path_wasi.odin b/core/os/os2/path_wasi.odin index 7aee8fcc0..b8240e188 100644 --- a/core/os/os2/path_wasi.odin +++ b/core/os/os2/path_wasi.odin @@ -28,13 +28,13 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { return .Invalid_Path } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) if exists(path) { return .Exist } - clean_path := clean_path(path, temp_allocator()) + clean_path := clean_path(path, temp_allocator) return internal_mkdir_all(clean_path) internal_mkdir_all :: proc(path: string) -> Error { diff --git a/core/os/os2/path_windows.odin b/core/os/os2/path_windows.odin index dd9b7748c..c2e51040f 100644 --- a/core/os/os2/path_windows.odin +++ b/core/os/os2/path_windows.odin @@ -14,8 +14,8 @@ _is_path_separator :: proc(c: byte) -> bool { } _mkdir :: proc(name: string, perm: int) -> Error { - TEMP_ALLOCATOR_GUARD() - if !win32.CreateDirectoryW(_fix_long_path(name, temp_allocator()) or_return, nil) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + if !win32.CreateDirectoryW(_fix_long_path(name, temp_allocator) or_return, nil) { return _get_platform_error() } return nil @@ -33,9 +33,9 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { return p, false, nil } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - dir_stat, err := stat(path, temp_allocator()) + dir_stat, err := stat(path, temp_allocator) if err == nil { if dir_stat.type == .Directory { return nil @@ -63,7 +63,7 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { err = mkdir(path, perm) if err != nil { - new_dir_stat, err1 := lstat(path, temp_allocator()) + new_dir_stat, err1 := lstat(path, temp_allocator) if err1 == nil && new_dir_stat.type == .Directory { return nil } @@ -82,8 +82,8 @@ _remove_all :: proc(path: string) -> Error { return nil } - TEMP_ALLOCATOR_GUARD() - dir := win32_utf8_to_wstring(path, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + dir := win32_utf8_to_wstring(path, temp_allocator) or_return empty: [1]u16 @@ -109,10 +109,10 @@ _remove_all :: proc(path: string) -> Error { _get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) { win32.AcquireSRWLockExclusive(&cwd_lock) - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) sz_utf16 := win32.GetCurrentDirectoryW(0, nil) - dir_buf_wstr := make([]u16, sz_utf16, temp_allocator()) or_return + dir_buf_wstr := make([]u16, sz_utf16, temp_allocator) or_return sz_utf16 = win32.GetCurrentDirectoryW(win32.DWORD(len(dir_buf_wstr)), raw_data(dir_buf_wstr)) assert(int(sz_utf16)+1 == len(dir_buf_wstr)) // the second time, it _excludes_ the NUL. @@ -123,8 +123,8 @@ _get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, er } _set_working_directory :: proc(dir: string) -> (err: Error) { - TEMP_ALLOCATOR_GUARD() - wstr := win32_utf8_to_wstring(dir, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + wstr := win32_utf8_to_wstring(dir, temp_allocator) or_return win32.AcquireSRWLockExclusive(&cwd_lock) @@ -138,9 +138,9 @@ _set_working_directory :: proc(dir: string) -> (err: Error) { } _get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buf := make([dynamic]u16, 512, temp_allocator()) or_return + buf := make([dynamic]u16, 512, temp_allocator) or_return for { ret := win32.GetModuleFileNameW(nil, raw_data(buf), win32.DWORD(len(buf))) if ret == 0 { @@ -187,7 +187,6 @@ init_long_path_support :: proc() { if value == 1 { can_use_long_paths = true } - } @(require_results) @@ -222,10 +221,10 @@ _fix_long_path_internal :: proc(path: string) -> string { return path } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) PREFIX :: `\\?` - path_buf := make([]byte, len(PREFIX)+len(path)+1, temp_allocator()) + path_buf := make([]byte, len(PREFIX)+len(path)+1, temp_allocator) copy(path_buf, PREFIX) n := len(path) r, w := 0, len(PREFIX) @@ -271,6 +270,11 @@ _clean_path_handle_start :: proc(path: string, buffer: []u8) -> (rooted: bool, s start += 1 } copy(buffer, path[:start]) + for n in 0.. (absol if rel == "" { rel = "." } - TEMP_ALLOCATOR_GUARD() - rel_utf16 := win32.utf8_to_utf16(rel, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + rel_utf16 := win32.utf8_to_utf16(rel, temp_allocator) n := win32.GetFullPathNameW(raw_data(rel_utf16), 0, nil, nil) if n == 0 { return "", Platform_Error(win32.GetLastError()) } - buf := make([]u16, n, temp_allocator()) or_return + buf := make([]u16, n, temp_allocator) or_return n = win32.GetFullPathNameW(raw_data(rel_utf16), u32(n), raw_data(buf), nil) if n == 0 { return "", Platform_Error(win32.GetLastError()) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index c90e3add2..3c84f3539 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -264,7 +264,7 @@ specific process, even after it has died. **Note(linux)**: The `handle` will be referring to pidfd. */ Process :: struct { - pid: int, + pid: int, handle: uintptr, } @@ -290,21 +290,10 @@ process_open :: proc(pid: int, flags := Process_Open_Flags {}) -> (Process, Erro return _process_open(pid, flags) } - -/* -OS-specific process attributes. -*/ -Process_Attributes :: struct { - sys_attr: _Sys_Process_Attributes, -} - /* The description of how a process should be created. */ Process_Desc :: struct { - // OS-specific attributes. - sys_attr: Process_Attributes, - // The working directory of the process. If the string has length 0, the // working directory is assumed to be the current working directory of the // current process. @@ -407,11 +396,9 @@ process_exec :: proc( { stdout_b: [dynamic]byte stdout_b.allocator = allocator - defer stdout = stdout_b[:] stderr_b: [dynamic]byte stderr_b.allocator = allocator - defer stderr = stderr_b[:] buf: [1024]u8 = --- @@ -450,6 +437,9 @@ process_exec :: proc( } } } + + stdout = stdout_b[:] + stderr = stderr_b[:] } if err != nil { diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 6d654008b..170f0ea1a 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -50,7 +50,7 @@ _get_ppid :: proc() -> int { @(private="package") _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) dir_fd, errno := linux.open("/proc/", _OPENDIR_FLAGS) #partial switch errno { @@ -68,9 +68,9 @@ _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) } defer linux.close(dir_fd) - dynamic_list := make([dynamic]int, temp_allocator()) or_return + dynamic_list := make([dynamic]int, temp_allocator) or_return - buf := make([dynamic]u8, 128, 128, temp_allocator()) or_return + buf := make([dynamic]u8, 128, 128, temp_allocator) or_return loop: for { buflen: int buflen, errno = linux.getdents(dir_fd, buf[:]) @@ -100,7 +100,7 @@ _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) @(private="package") _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) info.pid = pid @@ -126,7 +126,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator passwd_bytes: []u8 passwd_err: Error - passwd_bytes, passwd_err = _read_entire_pseudo_file_cstring("/etc/passwd", temp_allocator()) + passwd_bytes, passwd_err = _read_entire_pseudo_file_cstring("/etc/passwd", temp_allocator) if passwd_err != nil { err = passwd_err break username_if @@ -162,13 +162,13 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } - cmdline_if: if selection & {.Working_Dir, .Command_Line, .Command_Args, .Executable_Path} != {} { + cmdline_if: if selection & {.Working_Dir, .Command_Line, .Command_Args} != {} { strings.builder_reset(&path_builder) strings.write_string(&path_builder, "/proc/") strings.write_int(&path_builder, pid) strings.write_string(&path_builder, "/cmdline") - cmdline_bytes, cmdline_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator()) + cmdline_bytes, cmdline_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator) if cmdline_err != nil || len(cmdline_bytes) == 0 { err = cmdline_err break cmdline_if @@ -178,18 +178,18 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator terminator := strings.index_byte(cmdline, 0) assert(terminator > 0) - command_line_exec := cmdline[:terminator] + // command_line_exec := cmdline[:terminator] // Still need cwd if the execution on the command line is relative. cwd: string cwd_err: Error - if .Working_Dir in selection || (.Executable_Path in selection && command_line_exec[0] != '/') { + if .Working_Dir in selection { strings.builder_reset(&path_builder) strings.write_string(&path_builder, "/proc/") strings.write_int(&path_builder, pid) strings.write_string(&path_builder, "/cwd") - cwd, cwd_err = _read_link_cstr(strings.to_cstring(&path_builder) or_return, temp_allocator()) // allowed to fail + cwd, cwd_err = _read_link_cstr(strings.to_cstring(&path_builder) or_return, temp_allocator) // allowed to fail if cwd_err == nil && .Working_Dir in selection { info.working_dir = strings.clone(cwd, allocator) or_return info.fields += {.Working_Dir} @@ -199,18 +199,6 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } - if .Executable_Path in selection { - if cmdline[0] == '/' { - info.executable_path = strings.clone(cmdline[:terminator], allocator) or_return - info.fields += {.Executable_Path} - } else if cwd_err == nil { - info.executable_path = join_path({ cwd, cmdline[:terminator] }, allocator) or_return - info.fields += {.Executable_Path} - } else { - break cmdline_if - } - } - if selection & {.Command_Line, .Command_Args} != {} { // skip to first arg //cmdline = cmdline[terminator + 1:] @@ -257,7 +245,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator strings.write_int(&path_builder, pid) strings.write_string(&path_builder, "/stat") - proc_stat_bytes, stat_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator()) + proc_stat_bytes, stat_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator) if stat_err != nil { err = stat_err break stat_if @@ -296,7 +284,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator Nice, //... etc, } - stat_fields := strings.split(stats, " ", temp_allocator()) or_return + stat_fields := strings.split(stats, " ", temp_allocator) or_return if len(stat_fields) <= int(Fields.Nice) { break stat_if @@ -323,13 +311,37 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } + if .Executable_Path in selection { + /* + NOTE(Jeroen): + + The old version returned the wrong executable path for things like `bash` or `sh`, + for whom `/proc//cmdline` will just report "bash" or "sh", + resulting in misleading paths like `$PWD/sh`, even though that executable doesn't exist there. + + Thanks to Yawning for suggesting `/proc/self/exe`. + */ + + strings.builder_reset(&path_builder) + strings.write_string(&path_builder, "/proc/") + strings.write_int(&path_builder, pid) + strings.write_string(&path_builder, "/exe") + + if exe_bytes, exe_err := _read_link(strings.to_string(path_builder), temp_allocator); exe_err == nil { + info.executable_path = strings.clone(string(exe_bytes), allocator) or_return + info.fields += {.Executable_Path} + } else { + err = exe_err + } + } + if .Environment in selection { strings.builder_reset(&path_builder) strings.write_string(&path_builder, "/proc/") strings.write_int(&path_builder, pid) strings.write_string(&path_builder, "/environ") - if env_bytes, env_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator()); env_err == nil { + if env_bytes, env_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator); env_err == nil { env := string(env_bytes) env_list := make([dynamic]string, allocator) or_return @@ -378,12 +390,9 @@ _process_open :: proc(pid: int, _: Process_Open_Flags) -> (process: Process, err return } -@(private="package") -_Sys_Process_Attributes :: struct {} - @(private="package") _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) if len(desc.command) == 0 { return process, .Invalid_Command @@ -392,7 +401,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { dir_fd := linux.AT_FDCWD errno: linux.Errno if desc.working_dir != "" { - dir_cstr := temp_cstring(desc.working_dir) or_return + dir_cstr := clone_to_cstring(desc.working_dir, temp_allocator) or_return if dir_fd, errno = linux.open(dir_cstr, _OPENDIR_FLAGS); errno != .NONE { return process, _get_platform_error(errno) } @@ -405,10 +414,10 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { exe_path: cstring executable_name := desc.command[0] if strings.index_byte(executable_name, '/') < 0 { - path_env := get_env("PATH", temp_allocator()) - path_dirs := split_path_list(path_env, temp_allocator()) or_return + path_env := get_env("PATH", temp_allocator) + path_dirs := split_path_list(path_env, temp_allocator) or_return - exe_builder := strings.builder_make(temp_allocator()) or_return + exe_builder := strings.builder_make(temp_allocator) or_return found: bool for dir in path_dirs { @@ -435,7 +444,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } } } else { - exe_path = temp_cstring(executable_name) or_return + exe_path = clone_to_cstring(executable_name, temp_allocator) or_return if linux.access(exe_path, linux.X_OK) != .NONE { return process, .Not_Exist } @@ -443,20 +452,20 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // args and environment need to be a list of cstrings // that are terminated by a nil pointer. - cargs := make([]cstring, len(desc.command) + 1, temp_allocator()) or_return + cargs := make([]cstring, len(desc.command) + 1, temp_allocator) or_return for command, i in desc.command { - cargs[i] = temp_cstring(command) or_return + cargs[i] = clone_to_cstring(command, temp_allocator) or_return } // Use current process' environment if description didn't provide it. env: [^]cstring if desc.env == nil { // take this process's current environment - env = raw_data(export_cstring_environment(temp_allocator())) + env = raw_data(export_cstring_environment(temp_allocator)) } else { - cenv := make([]cstring, len(desc.env) + 1, temp_allocator()) or_return + cenv := make([]cstring, len(desc.env) + 1, temp_allocator) or_return for env, i in desc.env { - cenv[i] = temp_cstring(env) or_return + cenv[i] = clone_to_cstring(env, temp_allocator) or_return } env = &cenv[0] } @@ -584,7 +593,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } _process_state_update_times :: proc(state: ^Process_State) -> (err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) stat_path_buf: [48]u8 path_builder := strings.builder_from_bytes(stat_path_buf[:]) @@ -593,7 +602,7 @@ _process_state_update_times :: proc(state: ^Process_State) -> (err: Error) { strings.write_string(&path_builder, "/stat") stat_buf: []u8 - stat_buf, err = _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator()) + stat_buf, err = _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator) if err != nil { return } diff --git a/core/os/os2/process_posix.odin b/core/os/os2/process_posix.odin index cd451781f..fcacdf654 100644 --- a/core/os/os2/process_posix.odin +++ b/core/os/os2/process_posix.odin @@ -46,22 +46,20 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime return _process_info_by_pid(_get_pid(), selection, allocator) } -_Sys_Process_Attributes :: struct {} - _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if len(desc.command) == 0 { err = .Invalid_Path return } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) // search PATH if just a plain name is provided. - exe_builder := strings.builder_make(temp_allocator()) + exe_builder := strings.builder_make(temp_allocator) exe_name := desc.command[0] if strings.index_byte(exe_name, '/') < 0 { - path_env := get_env("PATH", temp_allocator()) - path_dirs := split_path_list(path_env, temp_allocator()) or_return + path_env := get_env("PATH", temp_allocator) + path_dirs := split_path_list(path_env, temp_allocator) or_return found: bool for dir in path_dirs { @@ -110,12 +108,12 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } cwd: cstring; if desc.working_dir != "" { - cwd = temp_cstring(desc.working_dir) + cwd = clone_to_cstring(desc.working_dir, temp_allocator) or_return } - cmd := make([]cstring, len(desc.command) + 1, temp_allocator()) + cmd := make([]cstring, len(desc.command) + 1, temp_allocator) for part, i in desc.command { - cmd[i] = temp_cstring(part) + cmd[i] = clone_to_cstring(part, temp_allocator) or_return } env: [^]cstring @@ -123,9 +121,9 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // take this process's current environment env = posix.environ } else { - cenv := make([]cstring, len(desc.env) + 1, temp_allocator()) + cenv := make([]cstring, len(desc.env) + 1, temp_allocator) for env, i in desc.env { - cenv[i] = temp_cstring(env) + cenv[i] = clone_to_cstring(env, temp_allocator) or_return } env = raw_data(cenv) } diff --git a/core/os/os2/process_posix_darwin.odin b/core/os/os2/process_posix_darwin.odin index 0ea1f643c..ac2d4b78c 100644 --- a/core/os/os2/process_posix_darwin.odin +++ b/core/os/os2/process_posix_darwin.odin @@ -50,6 +50,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) info.pid = pid // Thought on errors is: allocation failures return immediately (also why the non-allocation stuff is done first), @@ -127,7 +128,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator break args } - buf := runtime.make_aligned([]byte, length, 4, temp_allocator()) + buf := runtime.make_aligned([]byte, length, 4, temp_allocator) if sysctl(raw_data(mib), 3, raw_data(buf), &length, nil, 0) != .OK { if err == nil { err = _get_platform_error() @@ -239,9 +240,9 @@ _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) return } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buffer := make([]i32, ret, temp_allocator()) + buffer := make([]i32, ret, temp_allocator) ret = darwin.proc_listallpids(raw_data(buffer), ret*size_of(i32)) if ret < 0 { err = _get_platform_error() diff --git a/core/os/os2/process_wasi.odin b/core/os/os2/process_wasi.odin index 6ebfe3788..9f4d61649 100644 --- a/core/os/os2/process_wasi.odin +++ b/core/os/os2/process_wasi.odin @@ -44,8 +44,6 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime return } -_Sys_Process_Attributes :: struct {} - _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { err = .Unsupported return diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 536930ee1..199e5ad74 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -162,9 +162,10 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator if err != nil { break read_peb } + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) if selection >= {.Command_Line, .Command_Args} { - TEMP_ALLOCATOR_GUARD() - cmdline_w := make([]u16, process_params.CommandLine.Length, temp_allocator()) or_return + temp_allocator_scope(temp_allocator) + cmdline_w := make([]u16, process_params.CommandLine.Length, temp_allocator) or_return _, err = read_memory_as_slice(ph, process_params.CommandLine.Buffer, cmdline_w) if err != nil { break read_peb @@ -179,9 +180,9 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } if .Environment in selection { - TEMP_ALLOCATOR_GUARD() + temp_allocator_scope(temp_allocator) env_len := process_params.EnvironmentSize / 2 - envs_w := make([]u16, env_len, temp_allocator()) or_return + envs_w := make([]u16, env_len, temp_allocator) or_return _, err = read_memory_as_slice(ph, process_params.Environment, envs_w) if err != nil { break read_peb @@ -190,8 +191,8 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator info.fields += {.Environment} } if .Working_Dir in selection { - TEMP_ALLOCATOR_GUARD() - cwd_w := make([]u16, process_params.CurrentDirectoryPath.Length, temp_allocator()) or_return + temp_allocator_scope(temp_allocator) + cwd_w := make([]u16, process_params.CurrentDirectoryPath.Length, temp_allocator) or_return _, err = read_memory_as_slice(ph, process_params.CurrentDirectoryPath.Buffer, cwd_w) if err != nil { break read_peb @@ -272,9 +273,10 @@ _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields if err != nil { break read_peb } + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) if selection >= {.Command_Line, .Command_Args} { - TEMP_ALLOCATOR_GUARD() - cmdline_w := make([]u16, process_params.CommandLine.Length, temp_allocator()) or_return + temp_allocator_scope(temp_allocator) + cmdline_w := make([]u16, process_params.CommandLine.Length, temp_allocator) or_return _, err = read_memory_as_slice(ph, process_params.CommandLine.Buffer, cmdline_w) if err != nil { break read_peb @@ -289,9 +291,9 @@ _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields } } if .Environment in selection { - TEMP_ALLOCATOR_GUARD() + temp_allocator_scope(temp_allocator) env_len := process_params.EnvironmentSize / 2 - envs_w := make([]u16, env_len, temp_allocator()) or_return + envs_w := make([]u16, env_len, temp_allocator) or_return _, err = read_memory_as_slice(ph, process_params.Environment, envs_w) if err != nil { break read_peb @@ -300,8 +302,8 @@ _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields info.fields += {.Environment} } if .Working_Dir in selection { - TEMP_ALLOCATOR_GUARD() - cwd_w := make([]u16, process_params.CurrentDirectoryPath.Length, temp_allocator()) or_return + temp_allocator_scope(temp_allocator) + cwd_w := make([]u16, process_params.CurrentDirectoryPath.Length, temp_allocator) or_return _, err = read_memory_as_slice(ph, process_params.CurrentDirectoryPath.Buffer, cwd_w) if err != nil { break read_peb @@ -417,35 +419,64 @@ _process_open :: proc(pid: int, flags: Process_Open_Flags) -> (process: Process, return } -@(private="package") -_Sys_Process_Attributes :: struct {} - @(private="package") _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { - TEMP_ALLOCATOR_GUARD() - command_line := _build_command_line(desc.command, temp_allocator()) - command_line_w := win32_utf8_to_wstring(command_line, temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + command_line := _build_command_line(desc.command, temp_allocator) + command_line_w := win32_utf8_to_wstring(command_line, temp_allocator) or_return environment := desc.env if desc.env == nil { - environment = environ(temp_allocator()) or_return + environment = environ(temp_allocator) or_return } - environment_block := _build_environment_block(environment, temp_allocator()) - environment_block_w := win32_utf8_to_utf16(environment_block, temp_allocator()) or_return - stderr_handle := win32.GetStdHandle(win32.STD_ERROR_HANDLE) - stdout_handle := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE) - stdin_handle := win32.GetStdHandle(win32.STD_INPUT_HANDLE) + environment_block := _build_environment_block(environment, temp_allocator) + environment_block_w := win32_utf8_to_utf16(environment_block, temp_allocator) or_return - if desc.stdout != nil { + stderr_handle: win32.HANDLE + stdout_handle: win32.HANDLE + stdin_handle: win32.HANDLE + + null_handle: win32.HANDLE + if desc.stdout == nil || desc.stderr == nil || desc.stdin == nil { + null_handle = win32.CreateFileW( + win32.L("NUL"), + win32.GENERIC_READ|win32.GENERIC_WRITE, + win32.FILE_SHARE_READ|win32.FILE_SHARE_WRITE, + &win32.SECURITY_ATTRIBUTES{ + nLength = size_of(win32.SECURITY_ATTRIBUTES), + bInheritHandle = true, + }, + win32.OPEN_EXISTING, + win32.FILE_ATTRIBUTE_NORMAL, + nil, + ) + // Opening NUL should always succeed. + assert(null_handle != nil) + } + // NOTE(laytan): I believe it is fine to close this handle right after CreateProcess, + // and we don't have to hold onto this until the process exits. + defer if null_handle != nil { + win32.CloseHandle(null_handle) + } + + if desc.stdout == nil { + stdout_handle = null_handle + } else { stdout_handle = win32.HANDLE((^File_Impl)(desc.stdout.impl).fd) } - if desc.stderr != nil { + + if desc.stderr == nil { + stderr_handle = null_handle + } else { stderr_handle = win32.HANDLE((^File_Impl)(desc.stderr.impl).fd) } - if desc.stdin != nil { + + if desc.stdin == nil { + stdin_handle = null_handle + } else { stdin_handle = win32.HANDLE((^File_Impl)(desc.stdin.impl).fd) } - working_dir_w := (win32_utf8_to_wstring(desc.working_dir, temp_allocator()) or_else nil) if len(desc.working_dir) > 0 else nil + working_dir_w := (win32_utf8_to_wstring(desc.working_dir, temp_allocator) or_else nil) if len(desc.working_dir) > 0 else nil process_info: win32.PROCESS_INFORMATION ok := win32.CreateProcessW( nil, @@ -583,7 +614,7 @@ _process_exe_by_pid :: proc(pid: int, allocator: runtime.Allocator) -> (exe_path } _get_process_user :: proc(process_handle: win32.HANDLE, allocator: runtime.Allocator) -> (full_username: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) token_handle: win32.HANDLE if !win32.OpenProcessToken(process_handle, win32.TOKEN_QUERY, &token_handle) { err = _get_platform_error() @@ -598,7 +629,7 @@ _get_process_user :: proc(process_handle: win32.HANDLE, allocator: runtime.Alloc } err = nil } - token_user := (^win32.TOKEN_USER)(raw_data(make([]u8, token_user_size, temp_allocator()) or_return)) + token_user := (^win32.TOKEN_USER)(raw_data(make([]u8, token_user_size, temp_allocator) or_return)) if !win32.GetTokenInformation(token_handle, .TokenUser, token_user, token_user_size, &token_user_size) { err = _get_platform_error() return @@ -614,8 +645,8 @@ _get_process_user :: proc(process_handle: win32.HANDLE, allocator: runtime.Alloc err = _get_platform_error() return } - username := win32_utf16_to_utf8(username_w[:username_chrs], temp_allocator()) or_return - domain := win32_utf16_to_utf8(domain_w[:domain_chrs], temp_allocator()) or_return + username := win32_utf16_to_utf8(username_w[:username_chrs], temp_allocator) or_return + domain := win32_utf16_to_utf8(domain_w[:domain_chrs], temp_allocator) or_return return strings.concatenate({domain, "\\", username}, allocator) } diff --git a/core/os/os2/stat.odin b/core/os/os2/stat.odin index 7d76902eb..d6b524684 100644 --- a/core/os/os2/stat.odin +++ b/core/os/os2/stat.odin @@ -73,14 +73,14 @@ last_write_time_by_name :: modification_time_by_path @(require_results) modification_time :: proc(f: ^File) -> (time.Time, Error) { - TEMP_ALLOCATOR_GUARD() - fi, err := fstat(f, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + fi, err := fstat(f, temp_allocator) return fi.modification_time, err } @(require_results) modification_time_by_path :: proc(path: string) -> (time.Time, Error) { - TEMP_ALLOCATOR_GUARD() - fi, err := stat(path, temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + fi, err := stat(path, temp_allocator) return fi.modification_time, err } diff --git a/core/os/os2/stat_linux.odin b/core/os/os2/stat_linux.odin index 7bff08f29..373765be5 100644 --- a/core/os/os2/stat_linux.odin +++ b/core/os/os2/stat_linux.odin @@ -47,8 +47,8 @@ _fstat_internal :: proc(fd: linux.Fd, allocator: runtime.Allocator) -> (fi: File // NOTE: _stat and _lstat are using _fstat to avoid a race condition when populating fullpath _stat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + name_cstr := clone_to_cstring(name, temp_allocator) or_return fd, errno := linux.open(name_cstr, {}) if errno != .NONE { @@ -59,8 +59,8 @@ _stat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err } _lstat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) { - TEMP_ALLOCATOR_GUARD() - name_cstr := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + name_cstr := clone_to_cstring(name, temp_allocator) or_return fd, errno := linux.open(name_cstr, {.PATH, .NOFOLLOW}) if errno != .NONE { diff --git a/core/os/os2/stat_posix.odin b/core/os/os2/stat_posix.odin index 260dc7b52..6ffbdf1da 100644 --- a/core/os/os2/stat_posix.odin +++ b/core/os/os2/stat_posix.odin @@ -69,8 +69,8 @@ _stat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err return } - TEMP_ALLOCATOR_GUARD() - cname := temp_cstring(name) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + cname := clone_to_cstring(name, temp_allocator) or_return fd := posix.open(cname, {}) if fd == -1 { @@ -96,33 +96,34 @@ _lstat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, er return } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) // NOTE: can't use realpath or open (+ fcntl F_GETPATH) here because it tries to resolve symlinks. // NOTE: This might not be correct when given "/symlink/foo.txt", // you would want that to resolve "/symlink", but not resolve "foo.txt". - fullpath := clean_path(name, temp_allocator()) or_return + fullpath := clean_path(name, temp_allocator) or_return assert(len(fullpath) > 0) switch { case fullpath[0] == '/': // nothing. case fullpath == ".": - fullpath = getwd(temp_allocator()) or_return + fullpath = getwd(temp_allocator) or_return case len(fullpath) > 1 && fullpath[0] == '.' && fullpath[1] == '/': fullpath = fullpath[2:] fallthrough case: fullpath = concatenate({ - getwd(temp_allocator()) or_return, + getwd(temp_allocator) or_return, "/", fullpath, - }, temp_allocator()) or_return + }, temp_allocator) or_return } stat: posix.stat_t - if posix.lstat(temp_cstring(fullpath), &stat) != .OK { + c_fullpath := clone_to_cstring(fullpath, temp_allocator) or_return + if posix.lstat(c_fullpath, &stat) != .OK { err = _get_platform_error() return } diff --git a/core/os/os2/stat_windows.odin b/core/os/os2/stat_windows.odin index 7d8dd3843..3cdc80405 100644 --- a/core/os/os2/stat_windows.odin +++ b/core/os/os2/stat_windows.odin @@ -45,15 +45,15 @@ full_path_from_name :: proc(name: string, allocator: runtime.Allocator) -> (path name = "." } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - p := win32_utf8_to_utf16(name, temp_allocator()) or_return + p := win32_utf8_to_utf16(name, temp_allocator) or_return n := win32.GetFullPathNameW(raw_data(p), 0, nil, nil) if n == 0 { return "", _get_platform_error() } - buf := make([]u16, n+1, temp_allocator()) + buf := make([]u16, n+1, temp_allocator) n = win32.GetFullPathNameW(raw_data(p), u32(len(buf)), raw_data(buf), nil) if n == 0 { return "", _get_platform_error() @@ -65,9 +65,9 @@ internal_stat :: proc(name: string, create_file_attributes: u32, allocator: runt if len(name) == 0 { return {}, .Not_Exist } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - wname := _fix_long_path(name, temp_allocator()) or_return + wname := _fix_long_path(name, temp_allocator) or_return fa: win32.WIN32_FILE_ATTRIBUTE_DATA ok := win32.GetFileAttributesExW(wname, win32.GetFileExInfoStandard, &fa) if ok && fa.dwFileAttributes & win32.FILE_ATTRIBUTE_REPARSE_POINT == 0 { @@ -137,9 +137,9 @@ _cleanpath_from_handle :: proc(f: ^File, allocator: runtime.Allocator) -> (strin return "", _get_platform_error() } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - buf := make([]u16, max(n, 260)+1, temp_allocator()) + buf := make([]u16, max(n, 260)+1, temp_allocator) n = win32.GetFinalPathNameByHandleW(h, raw_data(buf), u32(len(buf)), 0) return _cleanpath_from_buf(buf[:n], allocator) } @@ -155,9 +155,9 @@ _cleanpath_from_handle_u16 :: proc(f: ^File) -> ([]u16, Error) { return nil, _get_platform_error() } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({}) - buf := make([]u16, max(n, 260)+1, temp_allocator()) + buf := make([]u16, max(n, 260)+1, temp_allocator) n = win32.GetFinalPathNameByHandleW(h, raw_data(buf), u32(len(buf)), 0) return _cleanpath_strip_prefix(buf[:n]), nil } @@ -236,14 +236,30 @@ _file_type_mode_from_file_attributes :: proc(file_attributes: win32.DWORD, h: wi return } +// a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC) +time_as_filetime :: #force_inline proc(t: time.Time) -> (ft: win32.LARGE_INTEGER) { + win := u64(t._nsec / 100) + 116444736000000000 + return win32.LARGE_INTEGER(win) +} + +filetime_as_time_li :: #force_inline proc(ft: win32.LARGE_INTEGER) -> (t: time.Time) { + return {_nsec=(i64(ft) - 116444736000000000) * 100} +} + +filetime_as_time_ft :: #force_inline proc(ft: win32.FILETIME) -> (t: time.Time) { + return filetime_as_time_li(win32.LARGE_INTEGER(ft.dwLowDateTime) + win32.LARGE_INTEGER(ft.dwHighDateTime) << 32) +} + +filetime_as_time :: proc{filetime_as_time_ft, filetime_as_time_li} + _file_info_from_win32_file_attribute_data :: proc(d: ^win32.WIN32_FILE_ATTRIBUTE_DATA, name: string, allocator: runtime.Allocator) -> (fi: File_Info, e: Error) { fi.size = i64(d.nFileSizeHigh)<<32 + i64(d.nFileSizeLow) type, mode := _file_type_mode_from_file_attributes(d.dwFileAttributes, nil, 0) fi.type = type fi.mode |= mode - fi.creation_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftCreationTime)) - fi.modification_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastWriteTime)) - fi.access_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastAccessTime)) + fi.creation_time = filetime_as_time(d.ftCreationTime) + fi.modification_time = filetime_as_time(d.ftLastWriteTime) + fi.access_time = filetime_as_time(d.ftLastAccessTime) fi.fullpath, e = full_path_from_name(name, allocator) fi.name = basename(fi.fullpath) return @@ -254,9 +270,9 @@ _file_info_from_win32_find_data :: proc(d: ^win32.WIN32_FIND_DATAW, name: string type, mode := _file_type_mode_from_file_attributes(d.dwFileAttributes, nil, 0) fi.type = type fi.mode |= mode - fi.creation_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftCreationTime)) - fi.modification_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastWriteTime)) - fi.access_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastAccessTime)) + fi.creation_time = filetime_as_time(d.ftCreationTime) + fi.modification_time = filetime_as_time(d.ftLastWriteTime) + fi.access_time = filetime_as_time(d.ftLastAccessTime) fi.fullpath, e = full_path_from_name(name, allocator) fi.name = basename(fi.fullpath) return @@ -286,9 +302,9 @@ _file_info_from_get_file_information_by_handle :: proc(path: string, h: win32.HA type, mode := _file_type_mode_from_file_attributes(d.dwFileAttributes, h, 0) fi.type = type fi.mode |= mode - fi.creation_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftCreationTime)) - fi.modification_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastWriteTime)) - fi.access_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastAccessTime)) + fi.creation_time = filetime_as_time(d.ftCreationTime) + fi.modification_time = filetime_as_time(d.ftLastWriteTime) + fi.access_time = filetime_as_time(d.ftLastAccessTime) return fi, nil } @@ -310,42 +326,68 @@ _is_reserved_name :: proc(path: string) -> bool { return false } -_is_UNC :: proc(path: string) -> bool { - return _volume_name_len(path) > 2 -} - -_volume_name_len :: proc(path: string) -> int { +_volume_name_len :: proc(path: string) -> (length: int) { if len(path) < 2 { return 0 } - c := path[0] + if path[1] == ':' { - switch c { + switch path[0] { case 'a'..='z', 'A'..='Z': return 2 } } - // URL: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx - if l := len(path); l >= 5 && _is_path_separator(path[0]) && _is_path_separator(path[1]) && - !_is_path_separator(path[2]) && path[2] != '.' { - for n := 3; n < l-1; n += 1 { - if _is_path_separator(path[n]) { - n += 1 - if !_is_path_separator(path[n]) { - if path[n] == '.' { - break - } - } - for ; n < l; n += 1 { - if _is_path_separator(path[n]) { - break - } - } - return n + /* + See: URL: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx + Further allowed paths can be of the form of: + - \\server\share or \\server\share\more\path + - \\?\C:\... + - \\.\PhysicalDriveX + */ + // Any remaining kind of path has to start with two slashes. + if !_is_path_separator(path[0]) || !_is_path_separator(path[1]) { + return 0 + } + + // Device path. The volume name is the whole string + if len(path) >= 5 && path[2] == '.' && _is_path_separator(path[3]) { + return len(path) + } + + // We're a UNC share `\\host\share`, file namespace `\\?\C:` or UNC in file namespace `\\?\\host\share` + prefix := 2 + + // File namespace. + if len(path) >= 5 && path[2] == '?' && _is_path_separator(path[3]) { + if _is_path_separator(path[4]) { + // `\\?\\` UNC path in file namespace + prefix = 5 + } + + if len(path) >= 6 && path[5] == ':' { + switch path[4] { + case 'a'..='z', 'A'..='Z': + return 6 + case: + return 0 } - break } } - return 0 -} + + // UNC path, minimum version of the volume is `\\h\s` for host, share. + // Can also contain an IP address in the host position. + slash_count := 0 + for i in prefix.. 0 { + slash_count += 1 + + if slash_count == 2 { + return i + } + } + } + + return len(path) +} \ No newline at end of file diff --git a/core/os/os2/temp_file.odin b/core/os/os2/temp_file.odin index e93117f02..ad20b5706 100644 --- a/core/os/os2/temp_file.odin +++ b/core/os/os2/temp_file.odin @@ -15,13 +15,13 @@ MAX_ATTEMPTS :: 1<<13 // Should be enough for everyone, right? // The caller must `close` the file once finished with. @(require_results) create_temp_file :: proc(dir, pattern: string) -> (f: ^File, err: Error) { - TEMP_ALLOCATOR_GUARD() - dir := dir if dir != "" else temp_directory(temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + dir := dir if dir != "" else temp_directory(temp_allocator) or_return prefix, suffix := _prefix_and_suffix(pattern) or_return prefix = temp_join_path(dir, prefix) or_return rand_buf: [10]byte - name_buf := make([]byte, len(prefix)+len(rand_buf)+len(suffix), temp_allocator()) + name_buf := make([]byte, len(prefix)+len(rand_buf)+len(suffix), temp_allocator) attempts := 0 for { @@ -47,13 +47,13 @@ mkdir_temp :: make_directory_temp // If `dir` is an empty tring, `temp_directory()` will be used. @(require_results) make_directory_temp :: proc(dir, pattern: string, allocator: runtime.Allocator) -> (temp_path: string, err: Error) { - TEMP_ALLOCATOR_GUARD() - dir := dir if dir != "" else temp_directory(temp_allocator()) or_return + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + dir := dir if dir != "" else temp_directory(temp_allocator) or_return prefix, suffix := _prefix_and_suffix(pattern) or_return prefix = temp_join_path(dir, prefix) or_return rand_buf: [10]byte - name_buf := make([]byte, len(prefix)+len(rand_buf)+len(suffix), temp_allocator()) + name_buf := make([]byte, len(prefix)+len(rand_buf)+len(suffix), temp_allocator) attempts := 0 for { @@ -70,7 +70,7 @@ make_directory_temp :: proc(dir, pattern: string, allocator: runtime.Allocator) return "", err } if err == .Not_Exist { - if _, serr := stat(dir, temp_allocator()); serr == .Not_Exist { + if _, serr := stat(dir, temp_allocator); serr == .Not_Exist { return "", serr } } @@ -89,9 +89,11 @@ temp_directory :: proc(allocator: runtime.Allocator) -> (string, Error) { @(private="file") temp_join_path :: proc(dir, name: string) -> (string, runtime.Allocator_Error) { + temp_allocator := TEMP_ALLOCATOR_GUARD({}) + if len(dir) > 0 && is_path_separator(dir[len(dir)-1]) { - return concatenate({dir, name}, temp_allocator(),) + return concatenate({dir, name}, temp_allocator,) } - return concatenate({dir, Path_Separator_String, name}, temp_allocator()) + return concatenate({dir, Path_Separator_String, name}, temp_allocator) } diff --git a/core/os/os2/temp_file_linux.odin b/core/os/os2/temp_file_linux.odin index 4eacbc54a..310720cbe 100644 --- a/core/os/os2/temp_file_linux.odin +++ b/core/os/os2/temp_file_linux.odin @@ -4,8 +4,8 @@ package os2 import "base:runtime" _temp_dir :: proc(allocator: runtime.Allocator) -> (string, runtime.Allocator_Error) { - TEMP_ALLOCATOR_GUARD() - tmpdir := get_env("TMPDIR", temp_allocator()) + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) + tmpdir := get_env("TMPDIR", temp_allocator) if tmpdir == "" { tmpdir = "/tmp" } diff --git a/core/os/os2/temp_file_windows.odin b/core/os/os2/temp_file_windows.odin index 3e3e1285c..9d75ef99d 100644 --- a/core/os/os2/temp_file_windows.odin +++ b/core/os/os2/temp_file_windows.odin @@ -9,9 +9,9 @@ _temp_dir :: proc(allocator: runtime.Allocator) -> (string, runtime.Allocator_Er if n == 0 { return "", nil } - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) - b := make([]u16, max(win32.MAX_PATH, n), temp_allocator()) + b := make([]u16, max(win32.MAX_PATH, n), temp_allocator) n = win32.GetTempPathW(u32(len(b)), raw_data(b)) if n == 3 && b[1] == ':' && b[2] == '\\' { diff --git a/core/os/os2/user.odin b/core/os/os2/user.odin index 7fcc87bf8..b2856a319 100644 --- a/core/os/os2/user.odin +++ b/core/os/os2/user.odin @@ -4,27 +4,27 @@ import "base:runtime" @(require_results) user_cache_dir :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) #partial switch ODIN_OS { case .Windows: - dir = get_env("LocalAppData", temp_allocator()) + dir = get_env("LocalAppData", temp_allocator) if dir != "" { - dir = clone_string(dir, allocator) or_return + dir = clone_string(dir, temp_allocator) or_return } case .Darwin: - dir = get_env("HOME", temp_allocator()) + dir = get_env("HOME", temp_allocator) if dir != "" { - dir = concatenate({dir, "/Library/Caches"}, allocator) or_return + dir = concatenate({dir, "/Library/Caches"}, temp_allocator) or_return } case: // All other UNIX systems dir = get_env("XDG_CACHE_HOME", allocator) if dir == "" { - dir = get_env("HOME", temp_allocator()) + dir = get_env("HOME", temp_allocator) if dir == "" { return } - dir = concatenate({dir, "/.cache"}, allocator) or_return + dir = concatenate({dir, "/.cache"}, temp_allocator) or_return } } if dir == "" { @@ -35,23 +35,23 @@ user_cache_dir :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error @(require_results) user_config_dir :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) { - TEMP_ALLOCATOR_GUARD() + temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator }) #partial switch ODIN_OS { case .Windows: - dir = get_env("AppData", temp_allocator()) + dir = get_env("AppData", temp_allocator) if dir != "" { dir = clone_string(dir, allocator) or_return } case .Darwin: - dir = get_env("HOME", temp_allocator()) + dir = get_env("HOME", temp_allocator) if dir != "" { dir = concatenate({dir, "/.config"}, allocator) or_return } case: // All other UNIX systems dir = get_env("XDG_CONFIG_HOME", allocator) if dir == "" { - dir = get_env("HOME", temp_allocator()) + dir = get_env("HOME", temp_allocator) if dir == "" { return } diff --git a/core/reflect/reflect.odin b/core/reflect/reflect.odin index 7f79acb77..b3315a0c3 100644 --- a/core/reflect/reflect.odin +++ b/core/reflect/reflect.odin @@ -24,7 +24,6 @@ Type_Info_Enumerated_Array :: runtime.Type_Info_Enumerated_Array Type_Info_Dynamic_Array :: runtime.Type_Info_Dynamic_Array Type_Info_Slice :: runtime.Type_Info_Slice Type_Info_Parameters :: runtime.Type_Info_Parameters -Type_Info_Tuple :: runtime.Type_Info_Parameters Type_Info_Struct :: runtime.Type_Info_Struct Type_Info_Union :: runtime.Type_Info_Union Type_Info_Enum :: runtime.Type_Info_Enum @@ -58,7 +57,7 @@ Type_Kind :: enum { Enumerated_Array, Dynamic_Array, Slice, - Tuple, + Parameters, Struct, Union, Enum, @@ -93,7 +92,7 @@ type_kind :: proc(T: typeid) -> Type_Kind { case Type_Info_Enumerated_Array: return .Enumerated_Array case Type_Info_Dynamic_Array: return .Dynamic_Array case Type_Info_Slice: return .Slice - case Type_Info_Parameters: return .Tuple + case Type_Info_Parameters: return .Parameters case Type_Info_Struct: return .Struct case Type_Info_Union: return .Union case Type_Info_Enum: return .Enum @@ -176,6 +175,7 @@ typeid_elem :: proc(id: typeid) -> typeid { case Type_Info_Enumerated_Array: return v.elem.id case Type_Info_Slice: return v.elem.id case Type_Info_Dynamic_Array: return v.elem.id + case Type_Info_Simd_Vector: return v.elem.id } return id } @@ -260,7 +260,11 @@ length :: proc(val: any) -> int { } else { return (^runtime.Raw_String)(val.data).len } + + case Type_Info_Simd_Vector: + return a.count } + return 0 } @@ -286,7 +290,11 @@ capacity :: proc(val: any) -> int { case Type_Info_Map: return runtime.map_cap((^runtime.Raw_Map)(val.data)^) + + case Type_Info_Simd_Vector: + return a.count } + return 0 } @@ -1431,6 +1439,11 @@ as_f64 :: proc(a: any) -> (value: f64, valid: bool) { case Type_Info_Complex: switch v in a { + case complex32: + if imag(v) == 0 { + value = f64(real(v)) + valid = true + } case complex64: if imag(v) == 0 { value = f64(real(v)) @@ -1445,6 +1458,11 @@ as_f64 :: proc(a: any) -> (value: f64, valid: bool) { case Type_Info_Quaternion: switch v in a { + case quaternion64: + if imag(v) == 0 && jmag(v) == 0 && kmag(v) == 0 { + value = f64(real(v)) + valid = true + } case quaternion128: if imag(v) == 0 && jmag(v) == 0 && kmag(v) == 0 { value = f64(real(v)) @@ -1638,13 +1656,40 @@ equal :: proc(a, b: any, including_indirect_array_recursion := false, recursion_ return equal(va, vb, including_indirect_array_recursion, recursion_level+1) case Type_Info_Map: return false + case Type_Info_Float: + x, _ := as_f64(a) + y, _ := as_f64(b) + return x == y + case Type_Info_Complex: + switch x in a { + case complex32: + #no_type_assert y := b.(complex32) + return x == y + case complex64: + #no_type_assert y := b.(complex64) + return x == y + case complex128: + #no_type_assert y := b.(complex128) + return x == y + } + return false + case Type_Info_Quaternion: + switch x in a { + case quaternion64: + #no_type_assert y := b.(quaternion64) + return x == y + case quaternion128: + #no_type_assert y := b.(quaternion128) + return x == y + case quaternion256: + #no_type_assert y := b.(quaternion256) + return x == y + } + return false case Type_Info_Boolean, Type_Info_Integer, Type_Info_Rune, - Type_Info_Float, - Type_Info_Complex, - Type_Info_Quaternion, Type_Info_Type_Id, Type_Info_Pointer, Type_Info_Multi_Pointer, diff --git a/core/reflect/types.odin b/core/reflect/types.odin index cb31a27e2..ba47fee4d 100644 --- a/core/reflect/types.odin +++ b/core/reflect/types.odin @@ -348,12 +348,6 @@ is_parameters :: proc(info: ^Type_Info) -> bool { _, ok := type_info_base(info).variant.(Type_Info_Parameters) return ok } -@(require_results, deprecated="prefer is_parameters") -is_tuple :: proc(info: ^Type_Info) -> bool { - if info == nil { return false } - _, ok := type_info_base(info).variant.(Type_Info_Parameters) - return ok -} @(require_results) is_struct :: proc(info: ^Type_Info) -> bool { if info == nil { return false } diff --git a/core/simd/simd.odin b/core/simd/simd.odin index 0bce4e16a..a97155f58 100644 --- a/core/simd/simd.odin +++ b/core/simd/simd.odin @@ -1328,13 +1328,18 @@ Example: // to load valid positions of the `ptrs` array, and the array of defaults which // will have `127` in each position as the default value. - v1 := [4] f32 {1, 2, 3, 4}; - v2 := [4] f32 {9, 10,11,12}; - ptrs := #simd [4]rawptr { &v1[1], nil, &v2[1], nil } - mask := #simd [4]bool { true, false, true, false } - defaults := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } - res := simd.gather(ptrs, defaults, mask) - fmt.println(res) + import "core:fmt" + import "core:simd" + + simd_gather_example :: proc() { + v1 := [4] f32 {1, 2, 3, 4}; + v2 := [4] f32 {9, 10,11,12}; + ptrs := #simd [4]rawptr { &v1[1], nil, &v2[1], nil } + mask := #simd [4]bool { true, false, true, false } + defaults := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } + res := simd.gather(ptrs, defaults, mask) + fmt.println(res) + } Output: @@ -1396,14 +1401,19 @@ Example: // vectors. The addresses of store destinations are written to the first and the // third argument of the `ptr` vector, and the `mask` is set accordingly. - v1 := [4] f32 {1, 2, 3, 4}; - v2 := [4] f32 {5, 6, 7, 8}; - ptrs := #simd [4]rawptr { &v1[1], nil, &v2[1], nil } - mask := #simd [4]bool { true, false, true, false } - vals := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } - simd.scatter(ptrs, vals, mask) - fmt.println(v1) - fmt.println(v2) + import "core:fmt" + import "core:simd" + + simd_scatter_example :: proc() { + v1 := [4] f32 {1, 2, 3, 4}; + v2 := [4] f32 {5, 6, 7, 8}; + ptrs := #simd [4]rawptr { &v1[1], nil, &v2[1], nil } + mask := #simd [4]bool { true, false, true, false } + vals := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } + simd.scatter(ptrs, vals, mask) + fmt.println(v1) + fmt.println(v2) + } Output: @@ -1467,11 +1477,16 @@ Example: // third value (selected by the mask). The masked-off values are given the value // of 127 (`0x7f`). - src := [4] f32 {1, 2, 3, 4}; - mask := #simd [4]bool { true, false, true, false } - vals := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } - res := simd.masked_load(&src, vals, mask) - fmt.println(res) + import "core:fmt" + import "core:simd" + + simd_masked_load_example :: proc() { + src := [4] f32 {1, 2, 3, 4}; + mask := #simd [4]bool { true, false, true, false } + vals := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } + res := simd.masked_load(&src, vals, mask) + fmt.println(res) + } Output: @@ -1526,11 +1541,16 @@ Example: // Example below stores the value 127 into the first and the third slot of the // vector `v`. - v := [4] f32 {1, 2, 3, 4}; - mask := #simd [4]bool { true, false, true, false } - vals := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } - simd.masked_store(&v, vals, mask) - fmt.println(v) + import "core:fmt" + import "core:simd" + + simd_masked_store_example :: proc() { + v := [4] f32 {1, 2, 3, 4}; + mask := #simd [4]bool { true, false, true, false } + vals := #simd [4]f32 { 0x7f, 0x7f, 0x7f, 0x7f } + simd.masked_store(&v, vals, mask) + fmt.println(v) + } Output: @@ -1600,11 +1620,16 @@ Example: // the third lane of the result vector. All the other lanes of the result vector // will be initialized to the default value `127`. - v := [2] f64 {1, 2}; - mask := #simd [4]bool { true, false, true, false } - vals := #simd [4]f64 { 0x7f, 0x7f, 0x7f, 0x7f } - res := simd.masked_expand_load(&v, vals, mask) - fmt.println(res) + import "core:fmt" + import "core:simd" + + simd_masked_expand_load_example :: proc() { + v := [2] f64 {1, 2}; + mask := #simd [4]bool { true, false, true, false } + vals := #simd [4]f64 { 0x7f, 0x7f, 0x7f, 0x7f } + res := simd.masked_expand_load(&v, vals, mask) + fmt.println(res) + } Output: @@ -1661,11 +1686,16 @@ Example: // vector, the first and the third value. The items in the mask are set to `true` // in those lanes. - v := [2] f64 { }; - mask := #simd [4]bool { true, false, true, false } - vals := #simd [4]f64 { 1, 2, 3, 4 } - simd.masked_compress_store(&v, vals, mask) - fmt.println(v) + import "core:fmt" + import "core:simd" + + simd_masked_compress_store_example :: proc() { + v := [2] f64 { }; + mask := #simd [4]bool { true, false, true, false } + vals := #simd [4]f64 { 1, 2, 3, 4 } + simd.masked_compress_store(&v, vals, mask) + fmt.println(v) + } Output: @@ -1729,7 +1759,103 @@ Returns: replace :: intrinsics.simd_replace /* -Reduce a vector to a scalar by adding up all the lanes. +Reduce a vector to a scalar by adding up all the lanes in a bisecting fashion. + +This procedure returns a scalar that is the sum of all lanes, calculated by +bisecting the vector into two parts, where the first contains lanes [0, N/2) +and the second contains lanes [N/2, N), and adding the two halves element-wise +to produce N/2 values. This is repeated until only a single element remains. +This order may be faster to compute than the ordered sum for floats, as it can +often be better parallelized. + +The order of the sum may be important for accounting for precision errors in +floating-point computation, as floating-point addition is not associative, that +is `(a+b)+c` may not be equal to `a+(b+c)`. + +Inputs: +- `v`: The vector to reduce. + +Result: +- Sum of all lanes, as a scalar. + +**Operation**: + + for n > 1 { + n = n / 2 + for i in 0 ..< n { + a[i] += a[i+n] + } + } + res := a[0] + +Graphical representation of the operation for N=4: + + +-----------------------+ + | v0 | v1 | v2 | v3 | + +-----------------------+ + | | | | + [+]<-- | ---' | + | [+]<--------' + | | + `>[+]<' + | + v + +-----+ + result: | y0 | + +-----+ +*/ +reduce_add_bisect :: intrinsics.simd_reduce_add_bisect + +/* +Reduce a vector to a scalar by multiplying up all the lanes in a bisecting fashion. + +This procedure returns a scalar that is the product of all lanes, calculated by +bisecting the vector into two parts, where the first contains indices [0, N/2) +and the second contains indices [N/2, N), and multiplying the two halves +together element-wise to produce N/2 values. This is repeated until only a +single element remains. This order may be faster to compute than the ordered +product for floats, as it can often be better parallelized. + +The order of the product may be important for accounting for precision errors +in floating-point computation, as floating-point multiplication is not +associative, that is `(a*b)*c` may not be equal to `a*(b*c)`. + +Inputs: +- `v`: The vector to reduce. + +Result: +- Product of all lanes, as a scalar. + +**Operation**: + + for n > 1 { + n = n / 2 + for i in 0 ..< n { + a[i] *= a[i+n] + } + } + res := a[0] + +Graphical representation of the operation for N=4: + + +-----------------------+ + | v0 | v1 | v2 | v3 | + +-----------------------+ + | | | | + [x]<-- | ---' | + | [x]<--------' + | | + `>[x]<' + | + v + +-----+ + result: | y0 | + +-----+ +*/ +reduce_mul_bisect :: intrinsics.simd_reduce_mul_bisect + +/* +Reduce a vector to a scalar by adding up all the lanes in an ordered fashion. This procedure returns a scalar that is the ordered sum of all lanes. The ordered sum may be important for accounting for precision errors in @@ -1752,7 +1878,7 @@ Result: reduce_add_ordered :: intrinsics.simd_reduce_add_ordered /* -Reduce a vector to a scalar by multiplying all the lanes. +Reduce a vector to a scalar by multiplying all the lanes in an ordered fashion. This procedure returns a scalar that is the ordered product of all lanes. The ordered product may be important for accounting for precision errors in @@ -1774,6 +1900,100 @@ Result: */ reduce_mul_ordered :: intrinsics.simd_reduce_mul_ordered +/* +Reduce a vector to a scalar by adding up all the lanes in a pairwise fashion. + +This procedure returns a scalar that is the sum of all lanes, calculated by +adding each even-indexed element with the following odd-indexed element to +produce N/2 values. This is repeated until only a single element remains. This +order is supported by hardware instructions for some types/architectures (e.g. +i16/i32/f32/f64 on x86 SSE, i8/i16/i32/f32 on ARM NEON). + +The order of the sum may be important for accounting for precision errors in +floating-point computation, as floating-point addition is not associative, that +is `(a+b)+c` may not be equal to `a+(b+c)`. + +Inputs: +- `v`: The vector to reduce. + +Result: +- Sum of all lanes, as a scalar. + +**Operation**: + + for n > 1 { + n = n / 2 + for i in 0 ..< n { + a[i] = a[2*i+0] + a[2*i+1] + } + } + res := a[0] + +Graphical representation of the operation for N=4: + + +-----------------------+ + v: | v0 | v1 | v2 | v3 | + +-----------------------+ + | | | | + `>[+]<' `>[+]<' + | | + `--->[+]<--' + | + v + +-----+ + result: | y0 | + +-----+ +*/ +reduce_add_pairs :: intrinsics.simd_reduce_add_pairs + +/* +Reduce a vector to a scalar by multiplying all the lanes in a pairwise fashion. + +This procedure returns a scalar that is the product of all lanes, calculated by +bisecting the vector into two parts, where the first contains lanes [0, N/2) +and the second contains lanes [N/2, N), and multiplying the two halves together +multiplying each even-indexed element with the following odd-indexed element to +produce N/2 values. This is repeated until only a single element remains. This +order may be faster to compute than the ordered product for floats, as it can +often be better parallelized. + +The order of the product may be important for accounting for precision errors +in floating-point computation, as floating-point multiplication is not +associative, that is `(a*b)*c` may not be equal to `a*(b*c)`. + +Inputs: +- `v`: The vector to reduce. + +Result: +- Product of all lanes, as a scalar. + +**Operation**: + + for n > 1 { + n = n / 2 + for i in 0 ..< n { + a[i] = a[2*i+0] * a[2*i+1] + } + } + res := a[0] + +Graphical representation of the operation for N=4: + + +-----------------------+ + v: | v0 | v1 | v2 | v3 | + +-----------------------+ + | | | | + `>[x]<' `>[x]<' + | | + `--->[x]<--' + | + v + +-----+ + result: | y0 | + +-----+ +*/ +reduce_mul_pairs :: intrinsics.simd_reduce_mul_pairs + /* Reduce a vector to a scalar by finding the minimum value between all of the lanes. @@ -1949,14 +2169,19 @@ Example: // The example below shows how the indices are used to determine which lanes of the // input vector get written into the result vector. - - x := #simd [4]f32 { 1.5, 2.5, 3.5, 4.5 } - res := simd.swizzle(x, 0, 3, 1, 1) - fmt.println("res") + + import "core:fmt" + import "core:simd" + + swizzle_example :: proc() { + x := #simd [4]f32 { 1.5, 2.5, 3.5, 4.5 } + res := simd.swizzle(x, 0, 3, 1, 1) + fmt.println(res) + } Output: - [ 1.5, 3.5, 2.5, 2.5 ] + <1.5, 4.5, 2.5, 2.5> The graphical representation of the operation is as follows. The `idx` vector in the picture represents the `indices` parameter: @@ -2013,8 +2238,14 @@ Example: // Since lanes 0, 1, 4, 7 contain negative numbers, the most significant // bits for them will be set. - v := #simd [8]i32 { -1, -2, +3, +4, -5, +6, +7, -8 } - fmt.println(simd.extract_msbs(v)) + + import "core:fmt" + import "core:simd" + + simd_extract_msbs_example :: proc() { + v := #simd [8]i32 { -1, -2, +3, +4, -5, +6, +7, -8 } + fmt.println(simd.extract_msbs(v)) + } Output: @@ -2052,8 +2283,14 @@ Example: // Since lanes 0, 2, 4, 6 contain odd integers, the least significant bits // for these lanes are set. - v := #simd [8]i32 { -1, -2, +3, +4, -5, +6, +7, -8 } - fmt.println(simd.extract_lsbs(v)) + + import "core:fmt" + import "core:simd" + + simd_extract_lsbs_example :: proc() { + v := #simd [8]i32 { -1, -2, +3, +4, -5, +6, +7, -8 } + fmt.println(simd.extract_lsbs(v)) + } Output: @@ -2097,15 +2334,20 @@ Example: // The example below shows how the indices are used to determine lanes of the // input vector that are shuffled into the result vector. - - a := #simd [4]f32 { 1, 2, 3, 4 } - b := #simd [4]f32 { 5, 6, 7, 8 } - res := simd.shuffle(a, b, 0, 4, 2, 5) - fmt.println("res") + + import "core:fmt" + import "core:simd" + + simd_shuffle_example :: proc() { + a := #simd [4]f32 { 1, 2, 3, 4 } + b := #simd [4]f32 { 5, 6, 7, 8 } + res := simd.shuffle(a, b, 0, 4, 2, 5) + fmt.println(res) + } Output: - [ 1, 5, 3, 6 ] + <1, 5, 3, 6> The graphical representation of the operation is as follows. The `idx` vector in the picture represents the `indices` parameter: @@ -2163,14 +2405,20 @@ Example: // The following example selects values from the two input vectors, `a` and `b` // into a single vector. - a := #simd [4] f64 { 1,2,3,4 } - b := #simd [4] f64 { 5,6,7,8 } - cond := #simd[4] int { 1, 0, 1, 0 } - fmt.println(simd.select(cond,a,b)) + + import "core:fmt" + import "core:simd" + + simd_select_example :: proc() { + a := #simd [4] f64 { 1,2,3,4 } + b := #simd [4] f64 { 5,6,7,8 } + cond := #simd[4] int { 1, 0, 1, 0 } + fmt.println(simd.select(cond,a,b)) + } Output: - [ 1, 6, 3, 8 ] + <1, 6, 3, 8> Graphically, the operation looks as follows. The `t` and `f` represent the `true` and `false` vectors respectively: @@ -2452,3 +2700,17 @@ Example: recip :: #force_inline proc "contextless" (v: $T/#simd[$LANES]$E) -> T where intrinsics.type_is_float(E) { return T(1) / v } + + +/* +Create a vector where each lane contains the index of that lane. +Inputs: +- `V`: The type of the vector to create. +Result: +- A vector of the given type, where each lane contains the index of that lane. +**Operation**: + for i in 0 ..< N { + res[i] = i + } +*/ +indices :: intrinsics.simd_indices \ No newline at end of file diff --git a/core/simd/x86/bmi.odin b/core/simd/x86/bmi.odin new file mode 100644 index 000000000..661272dbf --- /dev/null +++ b/core/simd/x86/bmi.odin @@ -0,0 +1,79 @@ +#+build i386, amd64 +package simd_x86 + +import "base:intrinsics" + +@(require_results, enable_target_feature="bmi") +_andn_u32 :: #force_inline proc "c" (a, b: u32) -> u32 { + return a &~ b +} +@(require_results, enable_target_feature="bmi") +_andn_u64 :: #force_inline proc "c" (a, b: u64) -> u64 { + return a &~ b +} + +@(require_results, enable_target_feature="bmi") +_bextr_u32 :: #force_inline proc "c" (a, start, len: u32) -> u32 { + return bextr_u32(a, (start & 0xff) | (len << 8)) +} +@(require_results, enable_target_feature="bmi") +_bextr_u64 :: #force_inline proc "c" (a: u64, start, len: u32) -> u64 { + return bextr_u64(a, cast(u64)((start & 0xff) | (len << 8))) +} + +@(require_results, enable_target_feature="bmi") +_bextr2_u32 :: #force_inline proc "c" (a, control: u32) -> u32 { + return bextr_u32(a, control) +} +@(require_results, enable_target_feature="bmi") +_bextr2_u64 :: #force_inline proc "c" (a, control: u64) -> u64 { + return bextr_u64(a, control) +} + +@(require_results, enable_target_feature="bmi") +_blsi_u32 :: #force_inline proc "c" (a: u32) -> u32 { + return a & -a +} +@(require_results, enable_target_feature="bmi") +_blsi_u64 :: #force_inline proc "c" (a: u64) -> u64 { + return a & -a +} + +@(require_results, enable_target_feature="bmi") +_blsmsk_u32 :: #force_inline proc "c" (a: u32) -> u32 { + return a ~ (a-1) +} +@(require_results, enable_target_feature="bmi") +_blsmsk_u64 :: #force_inline proc "c" (a: u64) -> u64 { + return a ~ (a-1) +} + +@(require_results, enable_target_feature="bmi") +_blsr_u32 :: #force_inline proc "c" (a: u32) -> u32 { + return a & (a-1) +} +@(require_results, enable_target_feature="bmi") +_blsr_u64 :: #force_inline proc "c" (a: u64) -> u64 { + return a & (a-1) +} + +@(require_results, enable_target_feature = "bmi") +_tzcnt_u16 :: #force_inline proc "c" (a: u16) -> u16 { + return intrinsics.count_trailing_zeros(a) +} +@(require_results, enable_target_feature = "bmi") +_tzcnt_u32 :: #force_inline proc "c" (a: u32) -> u32 { + return intrinsics.count_trailing_zeros(a) +} +@(require_results, enable_target_feature = "bmi") +_tzcnt_u64 :: #force_inline proc "c" (a: u64) -> u64 { + return intrinsics.count_trailing_zeros(a) +} + +@(private, default_calling_convention = "none") +foreign _ { + @(link_name = "llvm.x86.bmi.bextr.32") + bextr_u32 :: proc(a, control: u32) -> u32 --- + @(link_name = "llvm.x86.bmi.bextr.64") + bextr_u64 :: proc(a, control: u64) -> u64 --- +} diff --git a/core/simd/x86/bmi2.odin b/core/simd/x86/bmi2.odin new file mode 100644 index 000000000..65ce7f77c --- /dev/null +++ b/core/simd/x86/bmi2.odin @@ -0,0 +1,46 @@ +#+build i386, amd64 +package simd_x86 + +@(require_results, enable_target_feature = "bmi2") +_bzhi_u32 :: #force_inline proc "c" (a, index: u32) -> u32 { + return bzhi_u32(a, index) +} +@(require_results, enable_target_feature = "bmi2") +_bzhi_u64 :: #force_inline proc "c" (a, index: u64) -> u64 { + return bzhi_u64(a, index) +} + +@(require_results, enable_target_feature = "bmi2") +_pdep_u32 :: #force_inline proc "c" (a, mask: u32) -> u32 { + return pdep_u32(a, mask) +} +@(require_results, enable_target_feature = "bmi2") +_pdep_u64 :: #force_inline proc "c" (a, mask: u64) -> u64 { + return pdep_u64(a, mask) +} + +@(require_results, enable_target_feature = "bmi2") +_pext_u32 :: #force_inline proc "c" (a, mask: u32) -> u32 { + return pext_u32(a, mask) +} +@(require_results, enable_target_feature = "bmi2") +_pext_u64 :: #force_inline proc "c" (a, mask: u64) -> u64 { + return pext_u64(a, mask) +} + + +@(private, default_calling_convention = "none") +foreign _ { + @(link_name = "llvm.x86.bmi.bzhi.32") + bzhi_u32 :: proc(a, index: u32) -> u32 --- + @(link_name = "llvm.x86.bmi.bzhi.64") + bzhi_u64 :: proc(a, index: u64) -> u64 --- + @(link_name = "llvm.x86.bmi.pdep.32") + pdep_u32 :: proc(a, mask: u32) -> u32 --- + @(link_name = "llvm.x86.bmi.pdep.64") + pdep_u64 :: proc(a, mask: u64) -> u64 --- + @(link_name = "llvm.x86.bmi.pext.32") + pext_u32 :: proc(a, mask: u32) -> u32 --- + @(link_name = "llvm.x86.bmi.pext.64") + pext_u64 :: proc(a, mask: u64) -> u64 --- +} diff --git a/core/strconv/decimal/decimal.odin b/core/strconv/decimal/decimal.odin index 06503d01a..cb9285083 100644 --- a/core/strconv/decimal/decimal.odin +++ b/core/strconv/decimal/decimal.odin @@ -399,7 +399,7 @@ shift_left :: proc(a: ^Decimal, k: uint) #no_bounds_check { a.decimal_point += delta - a.count = clamp(a.count, 0, len(a.digits)) + a.count = clamp(a.count+delta, 0, len(a.digits)) trim(a) } /* @@ -562,4 +562,3 @@ rounded_integer :: proc(a: ^Decimal) -> u64 { } return n } - diff --git a/core/strconv/generic_float.odin b/core/strconv/generic_float.odin index b049f0fe1..b126dc3b6 100644 --- a/core/strconv/generic_float.odin +++ b/core/strconv/generic_float.odin @@ -339,45 +339,37 @@ Converts a decimal number to its floating-point representation with the given fo - b: The bits representing the floating-point number - overflow: A boolean indicating whether an overflow occurred during conversion */ -@(private) decimal_to_float_bits :: proc(d: ^decimal.Decimal, info: ^Float_Info) -> (b: u64, overflow: bool) { - end :: proc "contextless" (d: ^decimal.Decimal, mant: u64, exp: int, info: ^Float_Info) -> (bits: u64) { + overflow_end :: proc "contextless" (d: ^decimal.Decimal, info: ^Float_Info) -> (u64, bool) { + mant: u64 = 0 + exp: int = 1< (bits: u64, overflow: bool) { bits = mant & (u64(1)< bool { - mant^ = 0 - exp^ = 1< 310 { - set_overflow(&mant, &exp, info) - b = end(d, mant, exp, info) - return + return overflow_end(d, info) } else if d.decimal_point < -330 { - mant = 0 - exp = info.bias - b = end(d, mant, exp, info) - return + return end(d, 0, info.bias, info, false) } @(static, rodata) power_table := [?]int{1, 3, 6, 9, 13, 16, 19, 23, 26} - exp = 0 + + exp := 0 for d.decimal_point > 0 { n := 27 if d.decimal_point >= len(power_table) else power_table[d.decimal_point] decimal.shift(d, -n) @@ -392,35 +384,34 @@ decimal_to_float_bits :: proc(d: ^decimal.Decimal, info: ^Float_Info) -> (b: u64 // go from [0.5, 1) to [1, 2) exp -= 1 + // Min rep exp is 1+bias if exp < info.bias + 1 { n := info.bias + 1 - exp - decimal.shift(d, n) + decimal.shift(d, -n) exp += n } if (exp-info.bias) >= (1<>= 1 exp += 1 if (exp-info.bias) >= (1< (value: f64, nr: int, ok: bool) { return transmute(f64)bits, ok } + if len(str) > 2 && str[0] == '0' && str[1] == 'h' { + nr = 2 + + as_int: u64 + digits: int + for r in str[2:] { + if r == '_' { + nr += 1 + continue + } + v := u64(_digit_value(r)) + if v >= 16 { + break + } + as_int *= 16 + as_int += v + digits += 1 + } + nr += digits + ok = len(str) == nr + + switch digits { + case 4: + value = cast(f64)transmute(f16)cast(u16)as_int + case 8: + value = cast(f64)transmute(f32)cast(u32)as_int + case 16: + value = transmute(f64)as_int + case: + ok = false + } + return + } if value, nr, ok = check_special(str); ok { return diff --git a/core/strings/builder.odin b/core/strings/builder.odin index e5a88527a..05382f04e 100644 --- a/core/strings/builder.odin +++ b/core/strings/builder.odin @@ -311,7 +311,7 @@ Returns: - res: A cstring of the Builder's buffer upon success - err: An optional allocator error if one occured, `nil` otherwise */ -to_cstring :: proc(b: ^Builder) -> (res: cstring, err: mem.Allocator_Error) { +to_cstring :: proc(b: ^Builder) -> (res: cstring, err: mem.Allocator_Error) #optional_allocator_error { n := append(&b.buf, 0) or_return if n != 1 { return nil, .Out_Of_Memory diff --git a/core/strings/strings.odin b/core/strings/strings.odin index 52230f572..e15754dff 100644 --- a/core/strings/strings.odin +++ b/core/strings/strings.odin @@ -787,7 +787,7 @@ Example: import "core:fmt" import "core:strings" - cut_example :: proc() { + cut_clone_example :: proc() { fmt.println(strings.cut_clone("some example text", 0, 4)) // -> "some" fmt.println(strings.cut_clone("some example text", 2, 2)) // -> "me" fmt.println(strings.cut_clone("some example text", 5, 7)) // -> "example" @@ -3044,7 +3044,6 @@ left_justify :: proc(str: string, length: int, pad: string, allocator := context pad_len := rune_count(pad) b: Builder - builder_init(&b, allocator) builder_init(&b, 0, len(str) + (remains/pad_len + 1)*len(pad), allocator) or_return w := to_writer(&b) @@ -3079,7 +3078,6 @@ right_justify :: proc(str: string, length: int, pad: string, allocator := contex pad_len := rune_count(pad) b: Builder - builder_init(&b, allocator) builder_init(&b, 0, len(str) + (remains/pad_len + 1)*len(pad), allocator) or_return w := to_writer(&b) diff --git a/core/sync/chan/chan.odin b/core/sync/chan/chan.odin index c470d15f3..eca4c28d7 100644 --- a/core/sync/chan/chan.odin +++ b/core/sync/chan/chan.odin @@ -7,16 +7,62 @@ import "core:mem" import "core:sync" import "core:math/rand" +/* +Determines what operations `Chan` supports. +*/ Direction :: enum { Send = -1, Both = 0, Recv = +1, } +/* +A typed wrapper around `Raw_Chan` which should be used +preferably. + +Note: all procedures accepting `Raw_Chan` also accept `Chan`. + +**Inputs** +- `$T`: The type of the messages +- `Direction`: what `Direction` the channel supports + +Example: + + import "core:sync/chan" + + chan_example :: proc() { + // Create an unbuffered channel with messages of type int, + // supporting both sending and receiving. + // Creating unidirectional channels, although possible, is useless. + c, _ := chan.create(chan.Chan(int), context.allocator) + defer chan.destroy(c) + + // This channel can now only be used for receiving messages + recv_only_channel: chan.Chan(int, .Recv) = chan.as_recv(c) + // This channel can now only be used for sending messages + send_only_channel: chan.Chan(int, .Send) = chan.as_send(c) + } +*/ Chan :: struct($T: typeid, $D: Direction = Direction.Both) { #subtype impl: ^Raw_Chan `fmt:"-"`, } +/* +`Raw_Chan` allows for thread-safe communication using fixed-size messages. +This is the low-level implementation of `Chan`, which does not include +the concept of Direction. + +Example: + + import "core:sync/chan" + + raw_chan_example :: proc() { + // Create an unbuffered channel with messages of type int, + c, _ := chan.create_raw(size_of(int), align_of(int), context.allocator) + defer chan.destroy(c) + } + +*/ Raw_Chan :: struct { // Shared allocator: runtime.Allocator, @@ -36,12 +82,66 @@ Raw_Chan :: struct { unbuffered_data: rawptr, } +/* +Creates a buffered or unbuffered `Chan` instance. +*Allocates Using Provided Allocator* + +**Inputs** +- `$C`: Type of `Chan` to create +- [`cap`: The capacity of the channel] omit for creating unbuffered channels +- `allocator`: The allocator to use + +**Returns**: +- The initialized `Chan` +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + create_example :: proc() { + unbuffered: chan.Chan(int) + buffered: chan.Chan(int) + err: runtime.Allocator_Error + + unbuffered, err = chan.create(chan.Chan(int), context.allocator) + assert(err == .None) + defer chan.destroy(unbuffered) + + buffered, err = chan.create(chan.Chan(int), 10, context.allocator) + assert(err == .None) + defer chan.destroy(buffered) + } +*/ create :: proc{ create_unbuffered, create_buffered, } +/* +Creates an unbuffered version of the specified `Chan` type. + +*Allocates Using Provided Allocator* + +**Inputs** +- `$C`: Type of `Chan` to create +- `allocator`: The allocator to use + +**Returns**: +- The initialized `Chan` +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + create_unbuffered_example :: proc() { + c, err := chan.create_unbuffered(chan.Chan(int), context.allocator) + assert(err == .None) + defer chan.destroy(c) + } +*/ @(require_results) create_unbuffered :: proc($C: typeid/Chan($T), allocator: runtime.Allocator) -> (c: C, err: runtime.Allocator_Error) where size_of(T) <= int(max(u16)) { @@ -49,6 +149,30 @@ create_unbuffered :: proc($C: typeid/Chan($T), allocator: runtime.Allocator) -> return } +/* +Creates a buffered version of the specified `Chan` type. + +*Allocates Using Provided Allocator* + +**Inputs** +- `$C`: Type of `Chan` to create +- `cap`: The capacity of the channel +- `allocator`: The allocator to use + +**Returns**: +- The initialized `Chan` +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + create_buffered_example :: proc() { + c, err := chan.create_buffered(chan.Chan(int), 10, context.allocator) + assert(err == .None) + defer chan.destroy(c) + } +*/ @(require_results) create_buffered :: proc($C: typeid/Chan($T), #any_int cap: int, allocator: runtime.Allocator) -> (c: C, err: runtime.Allocator_Error) where size_of(T) <= int(max(u16)) { @@ -56,11 +180,70 @@ create_buffered :: proc($C: typeid/Chan($T), #any_int cap: int, allocator: runti return } +/* +Creates a buffered or unbuffered `Raw_Chan` for messages of the specified +size and alignment. + +*Allocates Using Provided Allocator* + +**Inputs** +- `msg_size`: The size of the messages the messages being sent +- `msg_alignment`: The alignment of the messages being sent +- [`cap`: The capacity of the channel] omit for creating unbuffered channels +- `allocator`: The allocator to use + +**Returns**: +- The initialized `Raw_Chan` +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + create_raw_example :: proc() { + unbuffered: ^chan.Raw_Chan + buffered: ^chan.Raw_Chan + err: runtime.Allocator_Error + + unbuffered, err = chan.create_raw(size_of(int), align_of(int), context.allocator) + assert(err == .None) + defer chan.destroy(unbuffered) + + buffered, err = chan.create_raw(size_of(int), align_of(int), 10, context.allocator) + assert(err == .None) + defer chan.destroy(buffered) + } +*/ create_raw :: proc{ create_raw_unbuffered, create_raw_buffered, } +/* +Creates an unbuffered `Raw_Chan` for messages of the specified +size and alignment. + +*Allocates Using Provided Allocator* + +**Inputs** +- `msg_size`: The size of the messages the messages being sent +- `msg_alignment`: The alignment of the messages being sent +- `allocator`: The allocator to use + +**Returns**: +- The initialized `Raw_Chan` +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + create_raw_unbuffered_example :: proc() { + unbuffered, err := chan.create_raw(size_of(int), align_of(int), context.allocator) + assert(err == .None) + defer chan.destroy(unbuffered) + } +*/ @(require_results) create_raw_unbuffered :: proc(#any_int msg_size, msg_alignment: int, allocator: runtime.Allocator) -> (c: ^Raw_Chan, err: runtime.Allocator_Error) { assert(msg_size <= int(max(u16))) @@ -80,6 +263,32 @@ create_raw_unbuffered :: proc(#any_int msg_size, msg_alignment: int, allocator: return } +/* +Creates a buffered `Raw_Chan` for messages of the specified +size and alignment. + +*Allocates Using Provided Allocator* + +**Inputs** +- `msg_size`: The size of the messages the messages being sent +- `msg_alignment`: The alignment of the messages being sent +- `cap`: The capacity of the channel +- `allocator`: The allocator to use + +**Returns**: +- The initialized `Raw_Chan` +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + create_raw_unbuffered_example :: proc() { + c, err := chan.create_raw_buffered(size_of(int), align_of(int), 10, context.allocator) + assert(err == .None) + defer chan.destroy(c) + } +*/ @(require_results) create_raw_buffered :: proc(#any_int msg_size, msg_alignment: int, #any_int cap: int, allocator: runtime.Allocator) -> (c: ^Raw_Chan, err: runtime.Allocator_Error) { assert(msg_size <= int(max(u16))) @@ -110,6 +319,16 @@ create_raw_buffered :: proc(#any_int msg_size, msg_alignment: int, #any_int cap: return } + +/* +Destroys the Channel. + +**Inputs** +- `c`: The channel to destroy + +**Returns**: +- An `Allocator_Error` +*/ destroy :: proc(c: ^Raw_Chan) -> (err: runtime.Allocator_Error) { if c != nil { allocator := c.allocator @@ -118,22 +337,142 @@ destroy :: proc(c: ^Raw_Chan) -> (err: runtime.Allocator_Error) { return } +/* +Creates a version of a channel that can only be used for sending +not receiving. + +**Inputs** +- `c`: The channel + +**Returns**: +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + as_send_example :: proc() { + // this procedure takes a channel that can only + // be used for sending not receiving. + producer :: proc(c: chan.Chan(int, .Send)) { + chan.send(c, 112) + + // compile-time error: + // value, ok := chan.recv(c) + } + + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + producer(chan.as_send(c)) + } +*/ @(require_results) as_send :: #force_inline proc "contextless" (c: $C/Chan($T, $D)) -> (s: Chan(T, .Send)) where C.D <= .Both { return transmute(type_of(s))c } + +/* +Creates a version of a channel that can only be used for receiving +not sending. + +**Inputs** +- `c`: The channel + +**Returns**: +- An `Allocator_Error` + +Example: + + import "core:sync/chan" + + as_recv_example :: proc() { + consumer :: proc(c: chan.Chan(int, .Recv)) { + value, ok := chan.recv(c) + + // compile-time error: + // chan.send(c, 22) + } + + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + chan.send(c, 112) + consumer(chan.as_recv(c)) + } +*/ @(require_results) as_recv :: #force_inline proc "contextless" (c: $C/Chan($T, $D)) -> (r: Chan(T, .Recv)) where C.D >= .Both { return transmute(type_of(r))c } +/* +Sends the specified message, blocking the current thread if: +- the channel is unbuffered +- the channel's buffer is full +until the channel is being read from. `send` will return +`false` when attempting to send on an already closed channel. +**Inputs** +- `c`: The channel +- `data`: The message to send + +**Returns** +- `true` if the message was sent, `false` when the channel was already closed + +Example: + + import "core:sync/chan" + + send_example :: proc() { + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + assert(chan.send(c, 2)) + + // this would block since the channel has a buffersize of 1 + // assert(chan.send(c, 2)) + + // sending on a closed channel returns false + chan.close(c) + assert(! chan.send(c, 2)) + } +*/ send :: proc "contextless" (c: $C/Chan($T, $D), data: T) -> (ok: bool) where C.D <= .Both { data := data ok = send_raw(c, &data) return } +/* +Tries sending the specified message which is: +- blocking: given the channel is unbuffered +- non-blocking: given the channel is buffered + +**Inputs** +- `c`: The channel +- `data`: The message to send + +**Returns** +- `true` if the message was sent, `false` when the channel was +already closed or the channel's buffer was full + +Example: + + import "core:sync/chan" + + try_send_example :: proc() { + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + assert(chan.try_send(c, 2), "there is enough space") + assert(!chan.try_send(c, 2), "the buffer is already full") + } +*/ @(require_results) try_send :: proc "contextless" (c: $C/Chan($T, $D), data: T) -> (ok: bool) where C.D <= .Both { data := data @@ -141,6 +480,43 @@ try_send :: proc "contextless" (c: $C/Chan($T, $D), data: T) -> (ok: bool) where return } +/* +Reads a message from the channel, blocking the current thread if: +- the channel is unbuffered +- the channel's buffer is empty +until the channel is being written to. `recv` will return +`false` when attempting to receive a message on an already closed channel. + +**Inputs** +- `c`: The channel + +**Returns** +- The message +- `true` if a message was received, `false` when the channel was already closed + +Example: + + import "core:sync/chan" + + recv_example :: proc() { + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + assert(chan.send(c, 2)) + + value, ok := chan.recv(c) + assert(ok, "the value was received") + + // this would block since the channel is now empty + // value, ok = chan.recv(c) + + // reading from a closed channel returns false + chan.close(c) + value, ok = chan.recv(c) + assert(!ok, "the channel is closed") + } +*/ @(require_results) recv :: proc "contextless" (c: $C/Chan($T)) -> (data: T, ok: bool) where C.D >= .Both { ok = recv_raw(c, &data) @@ -148,6 +524,29 @@ recv :: proc "contextless" (c: $C/Chan($T)) -> (data: T, ok: bool) where C.D >= } +/* +Tries reading a message from the channel in a non-blocking fashion. + +**Inputs** +- `c`: The channel + +**Returns** +- The message +- `true` if a message was received, `false` when the channel was already closed or no message was available + +Example: + + import "core:sync/chan" + + try_recv_example :: proc() { + c, err := chan.create(chan.Chan(int), context.allocator) + assert(err == .None) + defer chan.destroy(c) + + _, ok := chan.try_recv(c) + assert(!ok, "there is not value to read") + } +*/ @(require_results) try_recv :: proc "contextless" (c: $C/Chan($T)) -> (data: T, ok: bool) where C.D >= .Both { ok = try_recv_raw(c, &data) @@ -155,6 +554,43 @@ try_recv :: proc "contextless" (c: $C/Chan($T)) -> (data: T, ok: bool) where C.D } +/* +Sends the specified message, blocking the current thread if: +- the channel is unbuffered +- the channel's buffer is full +until the channel is being read from. `send_raw` will return +`false` when attempting to send on an already closed channel. + +Note: The message referenced by `msg_out` must match the size +and alignment used when the `Raw_Chan` was created. + +**Inputs** +- `c`: The channel +- `msg_out`: Pointer to the data to send + +**Returns** +- `true` if the message was sent, `false` when the channel was already closed + +Example: + + import "core:sync/chan" + + send_raw_example :: proc() { + c, err := chan.create_raw(size_of(int), align_of(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + value := 2 + assert(chan.send_raw(c, &value)) + + // this would block since the channel has a buffersize of 1 + // assert(chan.send_raw(c, &value)) + + // sending on a closed channel returns false + chan.close(c) + assert(! chan.send_raw(c, &value)) + } +*/ @(require_results) send_raw :: proc "contextless" (c: ^Raw_Chan, msg_in: rawptr) -> (ok: bool) { if c == nil { @@ -194,6 +630,45 @@ send_raw :: proc "contextless" (c: ^Raw_Chan, msg_in: rawptr) -> (ok: bool) { return } +/* +Reads a message from the channel, blocking the current thread if: +- the channel is unbuffered +- the channel's buffer is empty +until the channel is being written to. `recv_raw` will return +`false` when attempting to receive a message on an already closed channel. + +Note: The location pointed to by `msg_out` must match the size +and alignment used when the `Raw_Chan` was created. + +**Inputs** +- `c`: The channel +- `msg_out`: Pointer to where the message should be stored + +**Returns** +- `true` if a message was received, `false` when the channel was already closed + +Example: + + import "core:sync/chan" + + recv_raw_example :: proc() { + c, err := chan.create_raw(size_of(int), align_of(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + value := 2 + assert(chan.send_raw(c, &value)) + + assert(chan.recv_raw(c, &value)) + + // this would block since the channel is now empty + // assert(chan.recv_raw(c, &value)) + + // reading from a closed channel returns false + chan.close(c) + assert(! chan.recv_raw(c, &value)) + } +*/ @(require_results) recv_raw :: proc "contextless" (c: ^Raw_Chan, msg_out: rawptr) -> (ok: bool) { if c == nil { @@ -244,6 +719,36 @@ recv_raw :: proc "contextless" (c: ^Raw_Chan, msg_out: rawptr) -> (ok: bool) { } +/* +Tries sending the specified message which is: +- blocking: given the channel is unbuffered +- non-blocking: given the channel is buffered + +Note: The message referenced by `msg_out` must match the size +and alignment used when the `Raw_Chan` was created. + +**Inputs** +- `c`: the channel +- `msg_out`: pointer to the data to send + +**Returns** +- `true` if the message was sent, `false` when the channel was +already closed or the channel's buffer was full + +Example: + + import "core:sync/chan" + + try_send_raw_example :: proc() { + c, err := chan.create_raw(size_of(int), align_of(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + value := 2 + assert(chan.try_send_raw(c, &value), "there is enough space") + assert(!chan.try_send_raw(c, &value), "the buffer is already full") + } +*/ @(require_results) try_send_raw :: proc "contextless" (c: ^Raw_Chan, msg_in: rawptr) -> (ok: bool) { if c == nil { @@ -281,6 +786,32 @@ try_send_raw :: proc "contextless" (c: ^Raw_Chan, msg_in: rawptr) -> (ok: bool) return } +/* +Reads a message from the channel if one is available. + +Note: The location pointed to by `msg_out` must match the size +and alignment used when the `Raw_Chan` was created. + +**Inputs** +- `c`: The channel +- `msg_out`: Pointer to where the message should be stored + +**Returns** +- `true` if a message was received, `false` when the channel was already closed or no message was available + +Example: + + import "core:sync/chan" + + try_recv_raw_example :: proc() { + c, err := chan.create_raw(size_of(int), align_of(int), context.allocator) + assert(err == .None) + defer chan.destroy(c) + + value: int + assert(!chan.try_recv_raw(c, &value)) + } +*/ @(require_results) try_recv_raw :: proc "contextless" (c: ^Raw_Chan, msg_out: rawptr) -> bool { if c == nil { @@ -319,16 +850,85 @@ try_recv_raw :: proc "contextless" (c: ^Raw_Chan, msg_out: rawptr) -> bool { +/* +Checks if the given channel is buffered. + +**Inputs** +- `c`: The channel + +**Returns**: +- `true` if the channel is buffered, `false` otherwise + +Example: + + import "core:sync/chan" + + is_buffered_example :: proc() { + c, _ := chan.create(chan.Chan(int), 1, context.allocator) + defer chan.destroy(c) + assert(chan.is_buffered(c)) + } +*/ @(require_results) is_buffered :: proc "contextless" (c: ^Raw_Chan) -> bool { return c != nil && c.queue != nil } +/* +Checks if the given channel is unbuffered. + +**Inputs** +- `c`: The channel + +**Returns**: +- `true` if the channel is unbuffered, `false` otherwise + +Example: + + import "core:sync/chan" + + is_buffered_example :: proc() { + c, _ := chan.create(chan.Chan(int), context.allocator) + defer chan.destroy(c) + assert(chan.is_unbuffered(c)) + } +*/ @(require_results) is_unbuffered :: proc "contextless" (c: ^Raw_Chan) -> bool { return c != nil && c.unbuffered_data != nil } +/* +Returns the number of elements currently in the channel. + +Note: Unbuffered channels will always return `0` +because they cannot hold elements. + +**Inputs** +- `c`: The channel + +**Returns**: +- Number of elements + +Example: + + import "core:sync/chan" + import "core:fmt" + + len_example :: proc() { + c, _ := chan.create(chan.Chan(int), 2, context.allocator) + defer chan.destroy(c) + + fmt.println(chan.len(c)) + assert(chan.send(c, 1)) // add an element + fmt.println(chan.len(c)) + } + +Output: + + 0 + 1 +*/ @(require_results) len :: proc "contextless" (c: ^Raw_Chan) -> int { if c != nil && c.queue != nil { @@ -338,6 +938,34 @@ len :: proc "contextless" (c: ^Raw_Chan) -> int { return 0 } +/* +Returns the number of elements the channel could hold. + +Note: Unbuffered channels will always return `0` +because they cannot hold elements. + +**Inputs** +- `c`: The channel + +**Returns**: +- Number of elements + +Example: + + import "core:sync/chan" + import "core:fmt" + + cap_example :: proc() { + c, _ := chan.create(chan.Chan(int), 2, context.allocator) + defer chan.destroy(c) + + fmt.println(chan.cap(c)) + } + +Output: + + 2 +*/ @(require_results) cap :: proc "contextless" (c: ^Raw_Chan) -> int { if c != nil && c.queue != nil { @@ -347,6 +975,36 @@ cap :: proc "contextless" (c: ^Raw_Chan) -> int { return 0 } +/* +Closes the channel, preventing new messages from being added. + +**Inputs** +- `c`: The channel + +**Returns**: +- `true` if the channel was closed by this operation, `false` if it was already closed + +Example: + + import "core:sync/chan" + + close_example :: proc() { + c, _ := chan.create(chan.Chan(int), 2, context.allocator) + defer chan.destroy(c) + + // Sending a message to an open channel + assert(chan.send(c, 1), "allowed to send") + + // Closing the channel successfully + assert(chan.close(c), "successfully closed") + + // Trying to send a message after the channel is closed (should fail) + assert(!chan.send(c, 1), "not allowed to send after close") + + // Trying to close the channel again (should fail since it's already closed) + assert(!chan.close(c), "was already closed") + } +*/ close :: proc "contextless" (c: ^Raw_Chan) -> bool { if c == nil { return false @@ -361,6 +1019,15 @@ close :: proc "contextless" (c: ^Raw_Chan) -> bool { return true } +/* +Returns if the channel is closed or not + +**Inputs** +- `c`: The channel + +**Returns**: +- `true` if the channel is closed, `false` otherwise +*/ @(require_results) is_closed :: proc "contextless" (c: ^Raw_Chan) -> bool { if c == nil { @@ -370,56 +1037,30 @@ is_closed :: proc "contextless" (c: ^Raw_Chan) -> bool { return bool(c.closed) } +/* +Returns whether a message is ready to be read, i.e., +if a call to `recv` or `recv_raw` would block +**Inputs** +- `c`: The channel +**Returns** +- `true` if a message can be read, `false` otherwise -Raw_Queue :: struct { - data: [^]byte, - len: int, - cap: int, - next: int, - size: int, // element size -} +Example: -raw_queue_init :: proc "contextless" (q: ^Raw_Queue, data: rawptr, cap: int, size: int) { - q.data = ([^]byte)(data) - q.len = 0 - q.cap = cap - q.next = 0 - q.size = size -} + import "core:sync/chan" + can_recv_example :: proc() { + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) -@(require_results) -raw_queue_push :: proc "contextless" (q: ^Raw_Queue, data: rawptr) -> bool { - if q.len == q.cap { - return false + assert(!chan.can_recv(c), "the cannel is empty") + assert(chan.send(c, 2)) + assert(chan.can_recv(c), "there is message to read") } - pos := q.next + q.len - if pos >= q.cap { - pos -= q.cap - } - - val_ptr := q.data[pos*q.size:] - mem.copy(val_ptr, data, q.size) - q.len += 1 - return true -} - -@(require_results) -raw_queue_pop :: proc "contextless" (q: ^Raw_Queue) -> (data: rawptr) { - if q.len > 0 { - data = q.data[q.next*q.size:] - q.next += 1 - q.len -= 1 - if q.next >= q.cap { - q.next -= q.cap - } - } - return -} - - +*/ @(require_results) can_recv :: proc "contextless" (c: ^Raw_Chan) -> bool { sync.guard(&c.mutex) @@ -430,6 +1071,31 @@ can_recv :: proc "contextless" (c: ^Raw_Chan) -> bool { } +/* +Returns whether a message can be sent without blocking the current +thread. Specifically, it checks if the channel is buffered and not full, +or if there is already a reader waiting for a message. + +**Inputs** +- `c`: The channel + +**Returns** +- `true` if a message can be send, `false` otherwise + +Example: + + import "core:sync/chan" + + can_send_example :: proc() { + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + assert(chan.can_send(c), "the channel's buffer is not full") + assert(chan.send(c, 2)) + assert(!chan.can_send(c), "the channel's buffer is full") + } +*/ @(require_results) can_send :: proc "contextless" (c: ^Raw_Chan) -> bool { sync.guard(&c.mutex) @@ -440,7 +1106,69 @@ can_send :: proc "contextless" (c: ^Raw_Chan) -> bool { } +/* +Attempts to either send or receive messages on the specified channels. +`select_raw` first identifies which channels have messages ready to be received +and which are available for sending. It then randomly selects one operation +(either a send or receive) to perform. + +Note: Each message in `send_msgs` corresponds to the send channel at the same index in `sends`. + +**Inputs** +- `recv`: A slice of channels to read from +- `sends`: A slice of channels to send messages on +- `send_msgs`: A slice of messages to send +- `recv_out`: A pointer to the location where, when receiving, the message should be stored + +**Returns** +- Position of the available channel which was used for receiving or sending +- `true` if sending/receiving was successfull, `false` if the channel was closed or no channel was available + +Example: + + import "core:sync/chan" + import "core:fmt" + + select_raw_example :: proc() { + c, err := chan.create(chan.Chan(int), 1, context.allocator) + assert(err == .None) + defer chan.destroy(c) + + // sending value '1' on the channel + value1 := 1 + msgs := [?]rawptr{&value1} + send_chans := [?]^chan.Raw_Chan{c} + + // for simplicity the same channel used for sending is also used for receiving + receive_chans := [?]^chan.Raw_Chan{c} + // where the value from the read should be stored + received_value: int + + idx, ok := chan.select_raw(receive_chans[:], send_chans[:], msgs[:], &received_value) + fmt.println("SELECT: ", idx, ok) + fmt.println("RECEIVED VALUE ", received_value) + + idx, ok = chan.select_raw(receive_chans[:], send_chans[:], msgs[:], &received_value) + fmt.println("SELECT: ", idx, ok) + fmt.println("RECEIVED VALUE ", received_value) + + // closing of a channel also affects the select operation + chan.close(c) + + idx, ok = chan.select_raw(receive_chans[:], send_chans[:], msgs[:], &received_value) + fmt.println("SELECT: ", idx, ok) + } + +Output: + + SELECT: 0 true + RECEIVED VALUE 0 + SELECT: 0 true + RECEIVED VALUE 1 + SELECT: 0 false + +*/ @(require_results) select_raw :: proc "odin" (recvs: []^Raw_Chan, sends: []^Raw_Chan, send_msgs: []rawptr, recv_out: rawptr) -> (select_idx: int, ok: bool) #no_bounds_check { Select_Op :: struct { @@ -486,3 +1214,137 @@ select_raw :: proc "odin" (recvs: []^Raw_Chan, sends: []^Raw_Chan, send_msgs: [] } return } + + +/* +`Raw_Queue` is a non-thread-safe queue implementation designed to store messages +of fixed size and alignment. + +Note: For most use cases, it is recommended to use `core:container/queue` instead, +as `Raw_Queue` is used internally by `Raw_Chan` and may not provide the desired +level of convenience for typical applications. +*/ +@(private) +Raw_Queue :: struct { + data: [^]byte, + len: int, + cap: int, + next: int, + size: int, // element size +} + +/* +Initializes a `Raw_Queue` + +**Inputs** +- `q`: A pointert to the `Raw_Queue` to initialize +- `data`: The pointer to backing slice storing the messages +- `cap`: The capacity of the queue +- `size`: The size of a message + +Example: + + import "core:sync/chan" + + raw_queue_init_example :: proc() { + // use a stack allocated array as backing storage + storage: [100]int + + rq: chan.Raw_Queue + chan.raw_queue_init(&rq, &storage, cap(storage), size_of(int)) + } +*/ +@(private) +raw_queue_init :: proc "contextless" (q: ^Raw_Queue, data: rawptr, cap: int, size: int) { + q.data = ([^]byte)(data) + q.len = 0 + q.cap = cap + q.next = 0 + q.size = size +} + +/* +Add an element to the queue. + +Note: The message referenced by `data` must match the size +and alignment used when the `Raw_Queue` was initialized. + +**Inputs** +- `q`: A pointert to the `Raw_Queue` +- `data`: The pointer to message to add + +**Returns** +- `true` if the element was added, `false` when the queue is already full + +Example: + + import "core:sync/chan" + + raw_queue_push_example :: proc() { + storage: [100]int + rq: chan.Raw_Queue + chan.raw_queue_init(&rq, &storage, cap(storage), size_of(int)) + + value := 2 + assert(chan.raw_queue_push(&rq, &value), "there was enough space") + } +*/ +@(private, require_results) +raw_queue_push :: proc "contextless" (q: ^Raw_Queue, data: rawptr) -> bool { + if q.len == q.cap { + return false + } + pos := q.next + q.len + if pos >= q.cap { + pos -= q.cap + } + + val_ptr := q.data[pos*q.size:] + mem.copy(val_ptr, data, q.size) + q.len += 1 + return true +} + +/* +Removes and returns the first element of the queue. + +Note: The returned element is only guaranteed to be valid until the next +`raw_queue_push` operation. Accessing it after that point may result in +undefined behavior. + +**Inputs** +- `c`: A pointer to the `Raw_Queue`. + +**Returns** +- A pointer to the first element in the queue, or `nil` if the queue is empty. + +Example: + + import "core:sync/chan" + + raw_queue_pop_example :: proc() { + storage: [100]int + rq: chan.Raw_Queue + chan.raw_queue_init(&rq, &storage, cap(storage), size_of(int)) + + assert(chan.raw_queue_pop(&rq) == nil, "queue was empty") + + // add an element to the queue + value := 2 + assert(chan.raw_queue_push(&rq, &value), "there was enough space") + + assert((cast(^int)chan.raw_queue_pop(&rq))^ == 2, "retrieved the element") + } +*/ +@(private, require_results) +raw_queue_pop :: proc "contextless" (q: ^Raw_Queue) -> (data: rawptr) { + if q.len > 0 { + data = q.data[q.next*q.size:] + q.next += 1 + q.len -= 1 + if q.next >= q.cap { + q.next -= q.cap + } + } + return +} diff --git a/core/sync/chan/doc.odin b/core/sync/chan/doc.odin new file mode 100644 index 000000000..5d65d7410 --- /dev/null +++ b/core/sync/chan/doc.odin @@ -0,0 +1,69 @@ +/* +This package provides both high-level and low-level channel types +for thread-safe communication. + +While channels are essentially thread-safe queues under the hood, +their primary purpose is to facilitate safe communication between +multiple readers and multiple writers. +Although they can be used like queues, channels are designed with +synchronization and concurrent messaging patterns in mind. + +Provided types: +- `Chan` a high level channel +- `Raw_Chan` a low level channel +- `Raw_Queue` a low level non-threadsafe queue implementation used internally + +Example: + + import "core:sync/chan" + import "core:fmt" + import "core:thread" + + // The consumer reads from the channel until it's closed. + // Closing the channel acts as a signal to stop. + consumer :: proc(recv_chan: chan.Chan(int, .Recv)) { + for { + value, ok := chan.recv(recv_chan) + if !ok { + break // More idiomatic than return here + } + fmt.println("[CONSUMER] Received:", value) + } + fmt.println("[CONSUMER] Channel closed, stopping.") + } + + // The producer sends `count` number of messages. + producer :: proc(send_chan: chan.Chan(int, .Send), count: int) { + for i in 0.. ^Menu { +Application_mainMenu :: proc "c" (self: ^Application) -> ^Menu { return msgSend(^Menu, self, "mainMenu") } diff --git a/core/sys/darwin/Foundation/NSArray.odin b/core/sys/darwin/Foundation/NSArray.odin index b238f63f8..0977c6469 100644 --- a/core/sys/darwin/Foundation/NSArray.odin +++ b/core/sys/darwin/Foundation/NSArray.odin @@ -40,3 +40,49 @@ Array_objectAs :: proc "c" (self: ^Array, index: UInteger, $T: typeid) -> T wher Array_count :: proc "c" (self: ^Array) -> UInteger { return msgSend(UInteger, self, "count") } + + +@(objc_class="NSMutableArray") +MutableArray :: struct { + using _: Copying(MutableArray), +} + +@(objc_type=MutableArray, objc_name="alloc", objc_is_class_method=true) +MutableArray_alloc :: proc "c" () -> ^MutableArray { + return msgSend(^MutableArray, MutableArray, "alloc") +} + +@(objc_type=MutableArray, objc_name="init") +MutableArray_init :: proc "c" (self: ^MutableArray) -> ^MutableArray { + return msgSend(^MutableArray, self, "init") +} + +@(objc_type=MutableArray, objc_name="initWithObjects") +MutableArray_initWithObjects :: proc "c" (self: ^MutableArray, objects: [^]^Object, count: UInteger) -> ^MutableArray { + return msgSend(^MutableArray, self, "initWithObjects:count:", objects, count) +} + +@(objc_type=MutableArray, objc_name="initWithCoder") +MutableArray_initWithCoder :: proc "c" (self: ^MutableArray, coder: ^Coder) -> ^MutableArray { + return msgSend(^MutableArray, self, "initWithCoder:", coder) +} + +@(objc_type=MutableArray, objc_name="object") +MutableArray_object :: proc "c" (self: ^MutableArray, index: UInteger) -> ^Object { + return msgSend(^Object, self, "objectAtIndex:", index) +} +@(objc_type=MutableArray, objc_name="objectAs") +MutableArray_objectAs :: proc "c" (self: ^MutableArray, index: UInteger, $T: typeid) -> T where intrinsics.type_is_pointer(T), intrinsics.type_is_subtype_of(T, ^Object) { + return (T)(MutableArray_object(self, index)) +} + +@(objc_type=MutableArray, objc_name="count") +MutableArray_count :: proc "c" (self: ^MutableArray) -> UInteger { + return msgSend(UInteger, self, "count") +} + + +@(objc_type=MutableArray, objc_name="exchangeObjectAtIndex") +MutableArray_exchangeObjectAtIndex :: proc "c" (self: ^MutableArray, idx1, idx2: UInteger) { + msgSend(nil, self, "exchangeObjectAtIndex:withObjectAtIndex:", idx1, idx2) +} diff --git a/core/sys/darwin/Foundation/NSDictionary.odin b/core/sys/darwin/Foundation/NSDictionary.odin index 8af58cf62..ed98f3168 100644 --- a/core/sys/darwin/Foundation/NSDictionary.odin +++ b/core/sys/darwin/Foundation/NSDictionary.odin @@ -15,7 +15,7 @@ Dictionary_dictionaryWithObject :: proc "c" (object: ^Object, forKey: ^Object) - @(objc_type=Dictionary, objc_name="dictionaryWithObjects", objc_is_class_method=true) Dictionary_dictionaryWithObjects :: proc "c" (objects: [^]^Object, forKeys: [^]^Object, count: UInteger) -> ^Dictionary { - return msgSend(^Dictionary, Dictionary, "dictionaryWithObjects:forKeys:count", objects, forKeys, count) + return msgSend(^Dictionary, Dictionary, "dictionaryWithObjects:forKeys:count:", objects, forKeys, count) } @@ -31,7 +31,7 @@ Dictionary_init :: proc "c" (self: ^Dictionary) -> ^Dictionary { @(objc_type=Dictionary, objc_name="initWithObjects") Dictionary_initWithObjects :: proc "c" (self: ^Dictionary, objects: [^]^Object, forKeys: [^]^Object, count: UInteger) -> ^Dictionary { - return msgSend(^Dictionary, self, "initWithObjects:forKeys:count", objects, forKeys, count) + return msgSend(^Dictionary, self, "initWithObjects:forKeys:count:", objects, forKeys, count) } @(objc_type=Dictionary, objc_name="objectForKey") diff --git a/core/sys/darwin/Foundation/NSMenu.odin b/core/sys/darwin/Foundation/NSMenu.odin index 9a74151b0..747920ab7 100644 --- a/core/sys/darwin/Foundation/NSMenu.odin +++ b/core/sys/darwin/Foundation/NSMenu.odin @@ -2,127 +2,562 @@ package objc_Foundation import "base:builtin" import "base:intrinsics" - -KeyEquivalentModifierFlag :: enum UInteger { - CapsLock = 16, // Set if Caps Lock key is pressed. - Shift = 17, // Set if Shift key is pressed. - Control = 18, // Set if Control key is pressed. - Option = 19, // Set if Option or Alternate key is pressed. - Command = 20, // Set if Command key is pressed. - NumericPad = 21, // Set if any key in the numeric keypad is pressed. - Help = 22, // Set if the Help key is pressed. - Function = 23, // Set if any function key is pressed. -} -KeyEquivalentModifierMask :: distinct bit_set[KeyEquivalentModifierFlag; UInteger] - -// Used to retrieve only the device-independent modifier flags, allowing applications to mask off the device-dependent modifier flags, including event coalescing information. -KeyEventModifierFlagDeviceIndependentFlagsMask := transmute(KeyEquivalentModifierMask)_KeyEventModifierFlagDeviceIndependentFlagsMask -@(private) _KeyEventModifierFlagDeviceIndependentFlagsMask := UInteger(0xffff0000) +import "core:c" -MenuItemCallback :: proc "c" (unused: rawptr, name: SEL, sender: ^Object) - - -@(objc_class="NSMenuItem") -MenuItem :: struct {using _: Object} - -@(objc_type=MenuItem, objc_name="alloc", objc_is_class_method=true) -MenuItem_alloc :: proc "c" () -> ^MenuItem { - return msgSend(^MenuItem, MenuItem, "alloc") +MenuSelectionMode :: enum c.long { + Automatic = 0, + SelectOne = 1, + SelectAny = 2, } -@(objc_type=MenuItem, objc_name="registerActionCallback", objc_is_class_method=true) -MenuItem_registerActionCallback :: proc "c" (name: cstring, callback: MenuItemCallback) -> SEL { - s := string(name) - n := len(s) - sel: SEL - if n > 0 && s[n-1] != ':' { - col_name := intrinsics.alloca(n+2, 1) - builtin.copy(col_name[:n], s) - col_name[n] = ':' - col_name[n+1] = 0 - sel = sel_registerName(cstring(col_name)) - } else { - sel = sel_registerName(name) - } - if callback != nil { - class_addMethod(intrinsics.objc_find_class("NSObject"), sel, auto_cast callback, "v@:@") - } - return sel +MenuPresentationStyle :: enum c.long { + Regular = 0, + Palette = 1, } -@(objc_type=MenuItem, objc_name="separatorItem", objc_is_class_method=true) -MenuItem_separatorItem :: proc "c" () -> ^MenuItem { - return msgSend(^MenuItem, MenuItem, "separatorItem") +UserInterfaceLayoutDirection :: enum c.long { + LeftToRight = 0, + RightToLeft = 1, } -@(objc_type=MenuItem, objc_name="init") -MenuItem_init :: proc "c" (self: ^MenuItem) -> ^MenuItem { - return msgSend(^MenuItem, self, "init") +MenuPropertyItem :: enum c.ulong { + Title = 0, + AttributedTitle = 1, + KeyEquivalent = 2, + Image = 3, + Enabled = 4, + AccessibilityDescription = 5, } - -@(objc_type=MenuItem, objc_name="initWithTitle") -MenuItem_initWithTitle :: proc "c" (self: ^MenuItem, title: ^String, action: SEL, keyEquivalent: ^String) -> ^MenuItem { - return msgSend(^MenuItem, self, "initWithTitle:action:keyEquivalent:", title, action, keyEquivalent) -} - -@(objc_type=MenuItem, objc_name="setKeyEquivalentModifierMask") -MenuItem_setKeyEquivalentModifierMask :: proc "c" (self: ^MenuItem, modifierMask: KeyEquivalentModifierMask) { - msgSend(nil, self, "setKeyEquivalentModifierMask:", modifierMask) -} - -@(objc_type=MenuItem, objc_name="keyEquivalentModifierMask") -MenuItem_keyEquivalentModifierMask :: proc "c" (self: ^MenuItem) -> KeyEquivalentModifierMask { - return msgSend(KeyEquivalentModifierMask, self, "keyEquivalentModifierMask") -} - -@(objc_type=MenuItem, objc_name="setSubmenu") -MenuItem_setSubmenu :: proc "c" (self: ^MenuItem, submenu: ^Menu) { - msgSend(nil, self, "setSubmenu:", submenu) -} - -@(objc_type=MenuItem, objc_name="title") -MenuItem_title :: proc "c" (self: ^MenuItem) -> ^String { - return msgSend(^String, self, "title") -} - -@(objc_type=MenuItem, objc_name="setTitle") -MenuItem_setTitle :: proc "c" (self: ^MenuItem, title: ^String) -> ^String { - return msgSend(^String, self, "title:", title) -} - +MenuProperties :: distinct bit_set[MenuPropertyItem; c.ulong] @(objc_class="NSMenu") Menu :: struct {using _: Object} -@(objc_type=Menu, objc_name="alloc", objc_is_class_method=true) -Menu_alloc :: proc "c" () -> ^Menu { - return msgSend(^Menu, Menu, "alloc") -} - @(objc_type=Menu, objc_name="init") Menu_init :: proc "c" (self: ^Menu) -> ^Menu { return msgSend(^Menu, self, "init") } + @(objc_type=Menu, objc_name="initWithTitle") -Menu_initWithTitle :: proc "c" (self: ^Menu, title: ^String) -> ^Menu { +Menu_initWithTitle :: #force_inline proc "c" (self: ^Menu, title: ^String) -> ^Menu { return msgSend(^Menu, self, "initWithTitle:", title) } - - +@(objc_type=Menu, objc_name="initWithCoder") +Menu_initWithCoder :: #force_inline proc "c" (self: ^Menu, coder: ^Coder) -> ^Menu { + return msgSend(^Menu, self, "initWithCoder:", coder) +} +@(objc_type=Menu, objc_name="popUpContextMenu_withEvent_forView", objc_is_class_method=true) +Menu_popUpContextMenu_withEvent_forView :: #force_inline proc "c" (menu: ^Menu, event: ^Event, view: ^View) { + msgSend(nil, Menu, "popUpContextMenu:withEvent:forView:", menu, event, view) +} +// @(objc_type=Menu, objc_name="popUpContextMenu_withEvent_forView_withFont", objc_is_class_method=true) +// Menu_popUpContextMenu_withEvent_forView_withFont :: #force_inline proc "c" (menu: ^Menu, event: ^Event, view: ^View, font: ^Font) { +// msgSend(nil, Menu, "popUpContextMenu:withEvent:forView:withFont:", menu, event, view, font) +// } +@(objc_type=Menu, objc_name="popUpMenuPositioningItem") +Menu_popUpMenuPositioningItem :: #force_inline proc "c" (self: ^Menu, item: ^MenuItem, location: Point, view: ^View) -> bool { + return msgSend(bool, self, "popUpMenuPositioningItem:atLocation:inView:", item, location, view) +} +@(objc_type=Menu, objc_name="setMenuBarVisible", objc_is_class_method=true) +Menu_setMenuBarVisible :: #force_inline proc "c" (visible: bool) { + msgSend(nil, Menu, "setMenuBarVisible:", visible) +} +@(objc_type=Menu, objc_name="menuBarVisible", objc_is_class_method=true) +Menu_menuBarVisible :: #force_inline proc "c" () -> bool { + return msgSend(bool, Menu, "menuBarVisible") +} +@(objc_type=Menu, objc_name="insertItem") +Menu_insertItem :: #force_inline proc "c" (self: ^Menu, newItem: ^MenuItem, index: Integer) { + msgSend(nil, self, "insertItem:atIndex:", newItem, index) +} @(objc_type=Menu, objc_name="addItem") -Menu_addItem :: proc "c" (self: ^Menu, item: ^MenuItem) { - msgSend(nil, self, "addItem:", item) +Menu_addItem :: #force_inline proc "c" (self: ^Menu, newItem: ^MenuItem) { + msgSend(nil, self, "addItem:", newItem) +} +@(objc_type=Menu, objc_name="insertItemWithTitle") +Menu_insertItemWithTitle :: #force_inline proc "c" (self: ^Menu, string: ^String, selector: SEL, charCode: ^String, index: Integer) -> ^MenuItem { + return msgSend(^MenuItem, self, "insertItemWithTitle:action:keyEquivalent:atIndex:", string, selector, charCode, index) } - @(objc_type=Menu, objc_name="addItemWithTitle") -Menu_addItemWithTitle :: proc "c" (self: ^Menu, title: ^String, selector: SEL, keyEquivalent: ^String) -> ^MenuItem { - return msgSend(^MenuItem, self, "addItemWithTitle:action:keyEquivalent:", title, selector, keyEquivalent) +Menu_addItemWithTitle :: #force_inline proc "c" (self: ^Menu, string: ^String, selector: SEL, charCode: ^String) -> ^MenuItem { + return msgSend(^MenuItem, self, "addItemWithTitle:action:keyEquivalent:", string, selector, charCode) +} +@(objc_type=Menu, objc_name="removeItemAtIndex") +Menu_removeItemAtIndex :: #force_inline proc "c" (self: ^Menu, index: Integer) { + msgSend(nil, self, "removeItemAtIndex:", index) +} +@(objc_type=Menu, objc_name="removeItem") +Menu_removeItem :: #force_inline proc "c" (self: ^Menu, item: ^MenuItem) { + msgSend(nil, self, "removeItem:", item) +} +@(objc_type=Menu, objc_name="setSubmenu") +Menu_setSubmenu :: #force_inline proc "c" (self: ^Menu, menu: ^Menu, item: ^MenuItem) { + msgSend(nil, self, "setSubmenu:forItem:", menu, item) +} +@(objc_type=Menu, objc_name="removeAllItems") +Menu_removeAllItems :: #force_inline proc "c" (self: ^Menu) { + msgSend(nil, self, "removeAllItems") +} +@(objc_type=Menu, objc_name="itemAtIndex") +Menu_itemAtIndex :: #force_inline proc "c" (self: ^Menu, index: Integer) -> ^MenuItem { + return msgSend(^MenuItem, self, "itemAtIndex:", index) +} +@(objc_type=Menu, objc_name="indexOfItem") +Menu_indexOfItem :: #force_inline proc "c" (self: ^Menu, item: ^MenuItem) -> Integer { + return msgSend(Integer, self, "indexOfItem:", item) +} +@(objc_type=Menu, objc_name="indexOfItemWithTitle") +Menu_indexOfItemWithTitle :: #force_inline proc "c" (self: ^Menu, title: ^String) -> Integer { + return msgSend(Integer, self, "indexOfItemWithTitle:", title) +} +@(objc_type=Menu, objc_name="indexOfItemWithTag") +Menu_indexOfItemWithTag :: #force_inline proc "c" (self: ^Menu, tag: Integer) -> Integer { + return msgSend(Integer, self, "indexOfItemWithTag:", tag) +} +@(objc_type=Menu, objc_name="indexOfItemWithRepresentedObject") +Menu_indexOfItemWithRepresentedObject :: #force_inline proc "c" (self: ^Menu, object: id) -> Integer { + return msgSend(Integer, self, "indexOfItemWithRepresentedObject:", object) +} +@(objc_type=Menu, objc_name="indexOfItemWithSubmenu") +Menu_indexOfItemWithSubmenu :: #force_inline proc "c" (self: ^Menu, submenu: ^Menu) -> Integer { + return msgSend(Integer, self, "indexOfItemWithSubmenu:", submenu) +} +@(objc_type=Menu, objc_name="indexOfItemWithTarget") +Menu_indexOfItemWithTarget :: #force_inline proc "c" (self: ^Menu, target: id, actionSelector: SEL) -> Integer { + return msgSend(Integer, self, "indexOfItemWithTarget:andAction:", target, actionSelector) +} +@(objc_type=Menu, objc_name="itemWithTitle") +Menu_itemWithTitle :: #force_inline proc "c" (self: ^Menu, title: ^String) -> ^MenuItem { + return msgSend(^MenuItem, self, "itemWithTitle:", title) +} +@(objc_type=Menu, objc_name="itemWithTag") +Menu_itemWithTag :: #force_inline proc "c" (self: ^Menu, tag: Integer) -> ^MenuItem { + return msgSend(^MenuItem, self, "itemWithTag:", tag) +} +@(objc_type=Menu, objc_name="update") +Menu_update :: #force_inline proc "c" (self: ^Menu) { + msgSend(nil, self, "update") +} +@(objc_type=Menu, objc_name="performKeyEquivalent") +Menu_performKeyEquivalent :: #force_inline proc "c" (self: ^Menu, event: ^Event) -> bool { + return msgSend(bool, self, "performKeyEquivalent:", event) +} +@(objc_type=Menu, objc_name="itemChanged") +Menu_itemChanged :: #force_inline proc "c" (self: ^Menu, item: ^MenuItem) { + msgSend(nil, self, "itemChanged:", item) +} +@(objc_type=Menu, objc_name="performActionForItemAtIndex") +Menu_performActionForItemAtIndex :: #force_inline proc "c" (self: ^Menu, index: Integer) { + msgSend(nil, self, "performActionForItemAtIndex:", index) +} +@(objc_type=Menu, objc_name="cancelTracking") +Menu_cancelTracking :: #force_inline proc "c" (self: ^Menu) { + msgSend(nil, self, "cancelTracking") +} +@(objc_type=Menu, objc_name="cancelTrackingWithoutAnimation") +Menu_cancelTrackingWithoutAnimation :: #force_inline proc "c" (self: ^Menu) { + msgSend(nil, self, "cancelTrackingWithoutAnimation") +} +@(objc_type=Menu, objc_name="title") +Menu_title :: #force_inline proc "c" (self: ^Menu) -> ^String { + return msgSend(^String, self, "title") +} +@(objc_type=Menu, objc_name="setTitle") +Menu_setTitle :: #force_inline proc "c" (self: ^Menu, title: ^String) { + msgSend(nil, self, "setTitle:", title) +} +@(objc_type=Menu, objc_name="supermenu") +Menu_supermenu :: #force_inline proc "c" (self: ^Menu) -> ^Menu { + return msgSend(^Menu, self, "supermenu") +} +@(objc_type=Menu, objc_name="setSupermenu") +Menu_setSupermenu :: #force_inline proc "c" (self: ^Menu, supermenu: ^Menu) { + msgSend(nil, self, "setSupermenu:", supermenu) +} +@(objc_type=Menu, objc_name="itemArray") +Menu_itemArray :: #force_inline proc "c" (self: ^Menu) -> ^Array { + return msgSend(^Array, self, "itemArray") +} +@(objc_type=Menu, objc_name="setItemArray") +Menu_setItemArray :: #force_inline proc "c" (self: ^Menu, itemArray: ^Array) { + msgSend(nil, self, "setItemArray:", itemArray) +} +@(objc_type=Menu, objc_name="numberOfItems") +Menu_numberOfItems :: #force_inline proc "c" (self: ^Menu) -> Integer { + return msgSend(Integer, self, "numberOfItems") +} +@(objc_type=Menu, objc_name="autoenablesItems") +Menu_autoenablesItems :: #force_inline proc "c" (self: ^Menu) -> bool { + return msgSend(bool, self, "autoenablesItems") +} +@(objc_type=Menu, objc_name="setAutoenablesItems") +Menu_setAutoenablesItems :: #force_inline proc "c" (self: ^Menu, autoenablesItems: bool) { + msgSend(nil, self, "setAutoenablesItems:", autoenablesItems) +} +@(objc_type=Menu, objc_name="delegate") +Menu_delegate :: #force_inline proc "c" (self: ^Menu) -> ^MenuDelegate { + return msgSend(^MenuDelegate, self, "delegate") +} +@(objc_type=Menu, objc_name="setDelegate") +Menu_setDelegate :: #force_inline proc "c" (self: ^Menu, delegate: ^MenuDelegate) { + msgSend(nil, self, "setDelegate:", delegate) +} +@(objc_type=Menu, objc_name="menuBarHeight") +Menu_menuBarHeight :: #force_inline proc "c" (self: ^Menu) -> Float { + return msgSend(Float, self, "menuBarHeight") +} +@(objc_type=Menu, objc_name="highlightedItem") +Menu_highlightedItem :: #force_inline proc "c" (self: ^Menu) -> ^MenuItem { + return msgSend(^MenuItem, self, "highlightedItem") +} +@(objc_type=Menu, objc_name="minimumWidth") +Menu_minimumWidth :: #force_inline proc "c" (self: ^Menu) -> Float { + return msgSend(Float, self, "minimumWidth") +} +@(objc_type=Menu, objc_name="setMinimumWidth") +Menu_setMinimumWidth :: #force_inline proc "c" (self: ^Menu, minimumWidth: Float) { + msgSend(nil, self, "setMinimumWidth:", minimumWidth) +} +@(objc_type=Menu, objc_name="size") +Menu_size :: #force_inline proc "c" (self: ^Menu) -> Size { + return msgSend(Size, self, "size") +} +// @(objc_type=Menu, objc_name="font") +// Menu_font :: #force_inline proc "c" (self: ^Menu) -> ^Font { +// return msgSend(^Font, self, "font") +// } +// @(objc_type=Menu, objc_name="setFont") +// Menu_setFont :: #force_inline proc "c" (self: ^Menu, font: ^Font) { +// msgSend(nil, self, "setFont:", font) +// } +@(objc_type=Menu, objc_name="allowsContextMenuPlugIns") +Menu_allowsContextMenuPlugIns :: #force_inline proc "c" (self: ^Menu) -> bool { + return msgSend(bool, self, "allowsContextMenuPlugIns") +} +@(objc_type=Menu, objc_name="setAllowsContextMenuPlugIns") +Menu_setAllowsContextMenuPlugIns :: #force_inline proc "c" (self: ^Menu, allowsContextMenuPlugIns: bool) { + msgSend(nil, self, "setAllowsContextMenuPlugIns:", allowsContextMenuPlugIns) +} +@(objc_type=Menu, objc_name="showsStateColumn") +Menu_showsStateColumn :: #force_inline proc "c" (self: ^Menu) -> bool { + return msgSend(bool, self, "showsStateColumn") +} +@(objc_type=Menu, objc_name="setShowsStateColumn") +Menu_setShowsStateColumn :: #force_inline proc "c" (self: ^Menu, showsStateColumn: bool) { + msgSend(nil, self, "setShowsStateColumn:", showsStateColumn) +} +@(objc_type=Menu, objc_name="userInterfaceLayoutDirection") +Menu_userInterfaceLayoutDirection :: #force_inline proc "c" (self: ^Menu) -> UserInterfaceLayoutDirection { + return msgSend(UserInterfaceLayoutDirection, self, "userInterfaceLayoutDirection") +} +@(objc_type=Menu, objc_name="setUserInterfaceLayoutDirection") +Menu_setUserInterfaceLayoutDirection :: #force_inline proc "c" (self: ^Menu, userInterfaceLayoutDirection: UserInterfaceLayoutDirection) { + msgSend(nil, self, "setUserInterfaceLayoutDirection:", userInterfaceLayoutDirection) +} +@(objc_type=Menu, objc_name="paletteMenuWithColors_titles_selectionHandler", objc_is_class_method=true) +Menu_paletteMenuWithColors_titles_selectionHandler :: #force_inline proc "c" (colors: ^Array, itemTitles: ^Array, onSelectionChange: proc "c" (_arg_0: ^Menu)) -> ^Menu { + return msgSend(^Menu, Menu, "paletteMenuWithColors:titles:selectionHandler:", colors, itemTitles, onSelectionChange) +} +// @(objc_type=Menu, objc_name="paletteMenuWithColors_titles_templateImage_selectionHandler", objc_is_class_method=true) +// Menu_paletteMenuWithColors_titles_templateImage_selectionHandler :: #force_inline proc "c" (colors: ^Array, itemTitles: ^Array, image: ^Image, onSelectionChange: proc "c" (_arg_0: ^Menu)) -> ^Menu { +// return msgSend(^Menu, Menu, "paletteMenuWithColors:titles:templateImage:selectionHandler:", colors, itemTitles, image, onSelectionChange) +// } +@(objc_type=Menu, objc_name="presentationStyle") +Menu_presentationStyle :: #force_inline proc "c" (self: ^Menu) -> MenuPresentationStyle { + return msgSend(MenuPresentationStyle, self, "presentationStyle") +} +@(objc_type=Menu, objc_name="setPresentationStyle") +Menu_setPresentationStyle :: #force_inline proc "c" (self: ^Menu, presentationStyle: MenuPresentationStyle) { + msgSend(nil, self, "setPresentationStyle:", presentationStyle) +} +@(objc_type=Menu, objc_name="selectionMode") +Menu_selectionMode :: #force_inline proc "c" (self: ^Menu) -> MenuSelectionMode { + return msgSend(MenuSelectionMode, self, "selectionMode") +} +@(objc_type=Menu, objc_name="setSelectionMode") +Menu_setSelectionMode :: #force_inline proc "c" (self: ^Menu, selectionMode: MenuSelectionMode) { + msgSend(nil, self, "setSelectionMode:", selectionMode) +} +@(objc_type=Menu, objc_name="selectedItems") +Menu_selectedItems :: #force_inline proc "c" (self: ^Menu) -> ^Array { + return msgSend(^Array, self, "selectedItems") +} +@(objc_type=Menu, objc_name="setSelectedItems") +Menu_setSelectedItems :: #force_inline proc "c" (self: ^Menu, selectedItems: ^Array) { + msgSend(nil, self, "setSelectedItems:", selectedItems) +} +@(objc_type=Menu, objc_name="submenuAction") +Menu_submenuAction :: #force_inline proc "c" (self: ^Menu, sender: id) { + msgSend(nil, self, "submenuAction:", sender) +} +@(objc_type=Menu, objc_name="propertiesToUpdate") +Menu_propertiesToUpdate :: #force_inline proc "c" (self: ^Menu) -> MenuProperties { + return msgSend(MenuProperties, self, "propertiesToUpdate") +} +@(objc_type=Menu, objc_name="setMenuRepresentation") +Menu_setMenuRepresentation :: #force_inline proc "c" (self: ^Menu, menuRep: id) { + msgSend(nil, self, "setMenuRepresentation:", menuRep) +} +@(objc_type=Menu, objc_name="menuRepresentation") +Menu_menuRepresentation :: #force_inline proc "c" (self: ^Menu) -> id { + return msgSend(id, self, "menuRepresentation") +} +@(objc_type=Menu, objc_name="setContextMenuRepresentation") +Menu_setContextMenuRepresentation :: #force_inline proc "c" (self: ^Menu, menuRep: id) { + msgSend(nil, self, "setContextMenuRepresentation:", menuRep) +} +@(objc_type=Menu, objc_name="contextMenuRepresentation") +Menu_contextMenuRepresentation :: #force_inline proc "c" (self: ^Menu) -> id { + return msgSend(id, self, "contextMenuRepresentation") +} +@(objc_type=Menu, objc_name="setTearOffMenuRepresentation") +Menu_setTearOffMenuRepresentation :: #force_inline proc "c" (self: ^Menu, menuRep: id) { + msgSend(nil, self, "setTearOffMenuRepresentation:", menuRep) +} +@(objc_type=Menu, objc_name="tearOffMenuRepresentation") +Menu_tearOffMenuRepresentation :: #force_inline proc "c" (self: ^Menu) -> id { + return msgSend(id, self, "tearOffMenuRepresentation") +} +@(objc_type=Menu, objc_name="menuZone", objc_is_class_method=true) +Menu_menuZone :: #force_inline proc "c" () -> ^Zone { + return msgSend(^Zone, Menu, "menuZone") +} +@(objc_type=Menu, objc_name="setMenuZone", objc_is_class_method=true) +Menu_setMenuZone :: #force_inline proc "c" (zone: ^Zone) { + msgSend(nil, Menu, "setMenuZone:", zone) +} +@(objc_type=Menu, objc_name="attachedMenu") +Menu_attachedMenu :: #force_inline proc "c" (self: ^Menu) -> ^Menu { + return msgSend(^Menu, self, "attachedMenu") +} +@(objc_type=Menu, objc_name="isAttached") +Menu_isAttached :: #force_inline proc "c" (self: ^Menu) -> bool { + return msgSend(bool, self, "isAttached") +} +@(objc_type=Menu, objc_name="sizeToFit") +Menu_sizeToFit :: #force_inline proc "c" (self: ^Menu) { + msgSend(nil, self, "sizeToFit") +} +@(objc_type=Menu, objc_name="locationForSubmenu") +Menu_locationForSubmenu :: #force_inline proc "c" (self: ^Menu, submenu: ^Menu) -> Point { + return msgSend(Point, self, "locationForSubmenu:", submenu) +} +@(objc_type=Menu, objc_name="helpRequested") +Menu_helpRequested :: #force_inline proc "c" (self: ^Menu, eventPtr: ^Event) { + msgSend(nil, self, "helpRequested:", eventPtr) +} +@(objc_type=Menu, objc_name="menuChangedMessagesEnabled") +Menu_menuChangedMessagesEnabled :: #force_inline proc "c" (self: ^Menu) -> bool { + return msgSend(bool, self, "menuChangedMessagesEnabled") +} +@(objc_type=Menu, objc_name="setMenuChangedMessagesEnabled") +Menu_setMenuChangedMessagesEnabled :: #force_inline proc "c" (self: ^Menu, menuChangedMessagesEnabled: bool) { + msgSend(nil, self, "setMenuChangedMessagesEnabled:", menuChangedMessagesEnabled) +} +@(objc_type=Menu, objc_name="isTornOff") +Menu_isTornOff :: #force_inline proc "c" (self: ^Menu) -> bool { + return msgSend(bool, self, "isTornOff") +} +@(objc_type=Menu, objc_name="load", objc_is_class_method=true) +Menu_load :: #force_inline proc "c" () { + msgSend(nil, Menu, "load") +} +@(objc_type=Menu, objc_name="initialize", objc_is_class_method=true) +Menu_initialize :: #force_inline proc "c" () { + msgSend(nil, Menu, "initialize") +} +@(objc_type=Menu, objc_name="new", objc_is_class_method=true) +Menu_new :: #force_inline proc "c" () -> ^Menu { + return msgSend(^Menu, Menu, "new") +} +@(objc_type=Menu, objc_name="allocWithZone", objc_is_class_method=true) +Menu_allocWithZone :: #force_inline proc "c" (zone: ^Zone) -> ^Menu { + return msgSend(^Menu, Menu, "allocWithZone:", zone) +} +@(objc_type=Menu, objc_name="alloc", objc_is_class_method=true) +Menu_alloc :: #force_inline proc "c" () -> ^Menu { + return msgSend(^Menu, Menu, "alloc") +} +@(objc_type=Menu, objc_name="copyWithZone", objc_is_class_method=true) +Menu_copyWithZone :: #force_inline proc "c" (zone: ^Zone) -> id { + return msgSend(id, Menu, "copyWithZone:", zone) +} +@(objc_type=Menu, objc_name="mutableCopyWithZone", objc_is_class_method=true) +Menu_mutableCopyWithZone :: #force_inline proc "c" (zone: ^Zone) -> id { + return msgSend(id, Menu, "mutableCopyWithZone:", zone) +} +@(objc_type=Menu, objc_name="instancesRespondToSelector", objc_is_class_method=true) +Menu_instancesRespondToSelector :: #force_inline proc "c" (aSelector: SEL) -> bool { + return msgSend(bool, Menu, "instancesRespondToSelector:", aSelector) +} +@(objc_type=Menu, objc_name="conformsToProtocol", objc_is_class_method=true) +Menu_conformsToProtocol :: #force_inline proc "c" (protocol: ^Protocol) -> bool { + return msgSend(bool, Menu, "conformsToProtocol:", protocol) +} +@(objc_type=Menu, objc_name="instanceMethodForSelector", objc_is_class_method=true) +Menu_instanceMethodForSelector :: #force_inline proc "c" (aSelector: SEL) -> IMP { + return msgSend(IMP, Menu, "instanceMethodForSelector:", aSelector) +} +// @(objc_type=Menu, objc_name="instanceMethodSignatureForSelector", objc_is_class_method=true) +// Menu_instanceMethodSignatureForSelector :: #force_inline proc "c" (aSelector: SEL) -> ^MethodSignature { +// return msgSend(^MethodSignature, Menu, "instanceMethodSignatureForSelector:", aSelector) +// } +@(objc_type=Menu, objc_name="isSubclassOfClass", objc_is_class_method=true) +Menu_isSubclassOfClass :: #force_inline proc "c" (aClass: Class) -> bool { + return msgSend(bool, Menu, "isSubclassOfClass:", aClass) +} +@(objc_type=Menu, objc_name="resolveClassMethod", objc_is_class_method=true) +Menu_resolveClassMethod :: #force_inline proc "c" (sel: SEL) -> bool { + return msgSend(bool, Menu, "resolveClassMethod:", sel) +} +@(objc_type=Menu, objc_name="resolveInstanceMethod", objc_is_class_method=true) +Menu_resolveInstanceMethod :: #force_inline proc "c" (sel: SEL) -> bool { + return msgSend(bool, Menu, "resolveInstanceMethod:", sel) +} +@(objc_type=Menu, objc_name="hash", objc_is_class_method=true) +Menu_hash :: #force_inline proc "c" () -> UInteger { + return msgSend(UInteger, Menu, "hash") +} +@(objc_type=Menu, objc_name="superclass", objc_is_class_method=true) +Menu_superclass :: #force_inline proc "c" () -> Class { + return msgSend(Class, Menu, "superclass") +} +@(objc_type=Menu, objc_name="class", objc_is_class_method=true) +Menu_class :: #force_inline proc "c" () -> Class { + return msgSend(Class, Menu, "class") +} +@(objc_type=Menu, objc_name="description", objc_is_class_method=true) +Menu_description :: #force_inline proc "c" () -> ^String { + return msgSend(^String, Menu, "description") +} +@(objc_type=Menu, objc_name="debugDescription", objc_is_class_method=true) +Menu_debugDescription :: #force_inline proc "c" () -> ^String { + return msgSend(^String, Menu, "debugDescription") +} +@(objc_type=Menu, objc_name="version", objc_is_class_method=true) +Menu_version :: #force_inline proc "c" () -> Integer { + return msgSend(Integer, Menu, "version") +} +@(objc_type=Menu, objc_name="setVersion", objc_is_class_method=true) +Menu_setVersion :: #force_inline proc "c" (aVersion: Integer) { + msgSend(nil, Menu, "setVersion:", aVersion) +} +@(objc_type=Menu, objc_name="poseAsClass", objc_is_class_method=true) +Menu_poseAsClass :: #force_inline proc "c" (aClass: Class) { + msgSend(nil, Menu, "poseAsClass:", aClass) +} +@(objc_type=Menu, objc_name="cancelPreviousPerformRequestsWithTarget_selector_object", objc_is_class_method=true) +Menu_cancelPreviousPerformRequestsWithTarget_selector_object :: #force_inline proc "c" (aTarget: id, aSelector: SEL, anArgument: id) { + msgSend(nil, Menu, "cancelPreviousPerformRequestsWithTarget:selector:object:", aTarget, aSelector, anArgument) +} +@(objc_type=Menu, objc_name="cancelPreviousPerformRequestsWithTarget_", objc_is_class_method=true) +Menu_cancelPreviousPerformRequestsWithTarget_ :: #force_inline proc "c" (aTarget: id) { + msgSend(nil, Menu, "cancelPreviousPerformRequestsWithTarget:", aTarget) +} +@(objc_type=Menu, objc_name="accessInstanceVariablesDirectly", objc_is_class_method=true) +Menu_accessInstanceVariablesDirectly :: #force_inline proc "c" () -> bool { + return msgSend(bool, Menu, "accessInstanceVariablesDirectly") +} +@(objc_type=Menu, objc_name="useStoredAccessor", objc_is_class_method=true) +Menu_useStoredAccessor :: #force_inline proc "c" () -> bool { + return msgSend(bool, Menu, "useStoredAccessor") +} +@(objc_type=Menu, objc_name="keyPathsForValuesAffectingValueForKey", objc_is_class_method=true) +Menu_keyPathsForValuesAffectingValueForKey :: #force_inline proc "c" (key: ^String) -> ^Set { + return msgSend(^Set, Menu, "keyPathsForValuesAffectingValueForKey:", key) +} +@(objc_type=Menu, objc_name="automaticallyNotifiesObserversForKey", objc_is_class_method=true) +Menu_automaticallyNotifiesObserversForKey :: #force_inline proc "c" (key: ^String) -> bool { + return msgSend(bool, Menu, "automaticallyNotifiesObserversForKey:", key) +} +@(objc_type=Menu, objc_name="setKeys", objc_is_class_method=true) +Menu_setKeys :: #force_inline proc "c" (keys: ^Array, dependentKey: ^String) { + msgSend(nil, Menu, "setKeys:triggerChangeNotificationsForDependentKey:", keys, dependentKey) +} +@(objc_type=Menu, objc_name="classFallbacksForKeyedArchiver", objc_is_class_method=true) +Menu_classFallbacksForKeyedArchiver :: #force_inline proc "c" () -> ^Array { + return msgSend(^Array, Menu, "classFallbacksForKeyedArchiver") +} +@(objc_type=Menu, objc_name="classForKeyedUnarchiver", objc_is_class_method=true) +Menu_classForKeyedUnarchiver :: #force_inline proc "c" () -> Class { + return msgSend(Class, Menu, "classForKeyedUnarchiver") +} +@(objc_type=Menu, objc_name="exposeBinding", objc_is_class_method=true) +Menu_exposeBinding :: #force_inline proc "c" (binding: ^String) { + msgSend(nil, Menu, "exposeBinding:", binding) +} +@(objc_type=Menu, objc_name="setDefaultPlaceholder", objc_is_class_method=true) +Menu_setDefaultPlaceholder :: #force_inline proc "c" (placeholder: id, marker: id, binding: ^String) { + msgSend(nil, Menu, "setDefaultPlaceholder:forMarker:withBinding:", placeholder, marker, binding) +} +@(objc_type=Menu, objc_name="defaultPlaceholderForMarker", objc_is_class_method=true) +Menu_defaultPlaceholderForMarker :: #force_inline proc "c" (marker: id, binding: ^String) -> id { + return msgSend(id, Menu, "defaultPlaceholderForMarker:withBinding:", marker, binding) +} +@(objc_type=Menu, objc_name="popUpContextMenu") +Menu_popUpContextMenu :: proc { + Menu_popUpContextMenu_withEvent_forView, + // Menu_popUpContextMenu_withEvent_forView_withFont, } -@(objc_type=Menu, objc_name="itemArray") -Menu_itemArray :: proc "c" (self: ^Menu) -> ^Array { - return msgSend(^Array, self, "itemArray") -} \ No newline at end of file +@(objc_type=Menu, objc_name="paletteMenuWithColors") +Menu_paletteMenuWithColors :: proc { + Menu_paletteMenuWithColors_titles_selectionHandler, + // Menu_paletteMenuWithColors_titles_templateImage_selectionHandler, +} + +@(objc_type=Menu, objc_name="cancelPreviousPerformRequestsWithTarget") +Menu_cancelPreviousPerformRequestsWithTarget :: proc { + Menu_cancelPreviousPerformRequestsWithTarget_selector_object, + Menu_cancelPreviousPerformRequestsWithTarget_, +} + + + + + + + +@(objc_class="NSMenuDelegate") +MenuDelegate :: struct {using _: Object, using _: ObjectProtocol} + +@(objc_type=MenuDelegate, objc_name="menuNeedsUpdate") +MenuDelegate_menuNeedsUpdate :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu) { + msgSend(nil, self, "menuNeedsUpdate:", menu) +} +@(objc_type=MenuDelegate, objc_name="numberOfItemsInMenu") +MenuDelegate_numberOfItemsInMenu :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu) -> Integer { + return msgSend(Integer, self, "numberOfItemsInMenu:", menu) +} +@(objc_type=MenuDelegate, objc_name="menu_updateItem_atIndex_shouldCancel") +MenuDelegate_menu_updateItem_atIndex_shouldCancel :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu, item: ^MenuItem, index: Integer, shouldCancel: bool) -> bool { + return msgSend(bool, self, "menu:updateItem:atIndex:shouldCancel:", menu, item, index, shouldCancel) +} +@(objc_type=MenuDelegate, objc_name="menuHasKeyEquivalent") +MenuDelegate_menuHasKeyEquivalent :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu, event: ^Event, target: ^id, action: ^SEL) -> bool { + return msgSend(bool, self, "menuHasKeyEquivalent:forEvent:target:action:", menu, event, target, action) +} +@(objc_type=MenuDelegate, objc_name="menuWillOpen") +MenuDelegate_menuWillOpen :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu) { + msgSend(nil, self, "menuWillOpen:", menu) +} +@(objc_type=MenuDelegate, objc_name="menuDidClose") +MenuDelegate_menuDidClose :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu) { + msgSend(nil, self, "menuDidClose:", menu) +} +@(objc_type=MenuDelegate, objc_name="menu_willHighlightItem") +MenuDelegate_menu_willHighlightItem :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu, item: ^MenuItem) { + msgSend(nil, self, "menu:willHighlightItem:", menu, item) +} +@(objc_type=MenuDelegate, objc_name="confinementRectForMenu") +MenuDelegate_confinementRectForMenu :: #force_inline proc "c" (self: ^MenuDelegate, menu: ^Menu, screen: ^Screen) -> Rect { + return msgSend(Rect, self, "confinementRectForMenu:onScreen:", menu, screen) +} +@(objc_type=MenuDelegate, objc_name="menu") +MenuDelegate_menu :: proc { + MenuDelegate_menu_updateItem_atIndex_shouldCancel, + MenuDelegate_menu_willHighlightItem, +} diff --git a/core/sys/darwin/Foundation/NSMenuItem.odin b/core/sys/darwin/Foundation/NSMenuItem.odin new file mode 100644 index 000000000..248a0cf4f --- /dev/null +++ b/core/sys/darwin/Foundation/NSMenuItem.odin @@ -0,0 +1,460 @@ +package objc_Foundation + +import "base:builtin" +import "base:intrinsics" + +KeyEquivalentModifierFlag :: EventModifierFlag +KeyEquivalentModifierMask :: EventModifierFlags + +// Used to retrieve only the device-independent modifier flags, allowing applications to mask off the device-dependent modifier flags, including event coalescing information. +KeyEventModifierFlagDeviceIndependentFlagsMask := transmute(KeyEquivalentModifierMask)_KeyEventModifierFlagDeviceIndependentFlagsMask +@(private) _KeyEventModifierFlagDeviceIndependentFlagsMask := UInteger(0xffff0000) + +MenuItemCallback :: proc "c" (unused: rawptr, name: SEL, sender: ^Object) + +@(objc_class="NSMenuItem") +MenuItem :: struct {using _: Object} + +@(objc_type=MenuItem, objc_name="registerActionCallback", objc_is_class_method=true) +MenuItem_registerActionCallback :: proc "c" (name: cstring, callback: MenuItemCallback) -> SEL { + s := string(name) + n := len(s) + sel: SEL + if n > 0 && s[n-1] != ':' { + col_name := intrinsics.alloca(n+2, 1) + builtin.copy(col_name[:n], s) + col_name[n] = ':' + col_name[n+1] = 0 + sel = sel_registerName(cstring(col_name)) + } else { + sel = sel_registerName(name) + } + if callback != nil { + class_addMethod(intrinsics.objc_find_class("NSObject"), sel, auto_cast callback, "v@:@") + } + return sel +} + +@(objc_type=MenuItem, objc_name="init") +MenuItem_init :: proc "c" (self: ^MenuItem) -> ^MenuItem { + return msgSend(^MenuItem, self, "init") +} + + +@(objc_type=MenuItem, objc_name="separatorItem", objc_is_class_method=true) +MenuItem_separatorItem :: #force_inline proc "c" () -> ^MenuItem { + return msgSend(^MenuItem, MenuItem, "separatorItem") +} +@(objc_type=MenuItem, objc_name="sectionHeaderWithTitle", objc_is_class_method=true) +MenuItem_sectionHeaderWithTitle :: #force_inline proc "c" (title: ^String) -> ^MenuItem { + return msgSend(^MenuItem, MenuItem, "sectionHeaderWithTitle:", title) +} +@(objc_type=MenuItem, objc_name="initWithTitle") +MenuItem_initWithTitle :: #force_inline proc "c" (self: ^MenuItem, string: ^String, selector: SEL, charCode: ^String) -> ^MenuItem { + return msgSend(^MenuItem, self, "initWithTitle:action:keyEquivalent:", string, selector, charCode) +} +@(objc_type=MenuItem, objc_name="initWithCoder") +MenuItem_initWithCoder :: #force_inline proc "c" (self: ^MenuItem, coder: ^Coder) -> ^MenuItem { + return msgSend(^MenuItem, self, "initWithCoder:", coder) +} +@(objc_type=MenuItem, objc_name="usesUserKeyEquivalents", objc_is_class_method=true) +MenuItem_usesUserKeyEquivalents :: #force_inline proc "c" () -> bool { + return msgSend(bool, MenuItem, "usesUserKeyEquivalents") +} +@(objc_type=MenuItem, objc_name="setUsesUserKeyEquivalents", objc_is_class_method=true) +MenuItem_setUsesUserKeyEquivalents :: #force_inline proc "c" (usesUserKeyEquivalents: bool) { + msgSend(nil, MenuItem, "setUsesUserKeyEquivalents:", usesUserKeyEquivalents) +} +@(objc_type=MenuItem, objc_name="menu") +MenuItem_menu :: #force_inline proc "c" (self: ^MenuItem) -> ^Menu { + return msgSend(^Menu, self, "menu") +} +@(objc_type=MenuItem, objc_name="setMenu") +MenuItem_setMenu :: #force_inline proc "c" (self: ^MenuItem, menu: ^Menu) { + msgSend(nil, self, "setMenu:", menu) +} +@(objc_type=MenuItem, objc_name="hasSubmenu") +MenuItem_hasSubmenu :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "hasSubmenu") +} +@(objc_type=MenuItem, objc_name="submenu") +MenuItem_submenu :: #force_inline proc "c" (self: ^MenuItem) -> ^Menu { + return msgSend(^Menu, self, "submenu") +} +@(objc_type=MenuItem, objc_name="setSubmenu") +MenuItem_setSubmenu :: #force_inline proc "c" (self: ^MenuItem, submenu: ^Menu) { + msgSend(nil, self, "setSubmenu:", submenu) +} +@(objc_type=MenuItem, objc_name="parentItem") +MenuItem_parentItem :: #force_inline proc "c" (self: ^MenuItem) -> ^MenuItem { + return msgSend(^MenuItem, self, "parentItem") +} +@(objc_type=MenuItem, objc_name="title") +MenuItem_title :: #force_inline proc "c" (self: ^MenuItem) -> ^String { + return msgSend(^String, self, "title") +} +@(objc_type=MenuItem, objc_name="setTitle") +MenuItem_setTitle :: #force_inline proc "c" (self: ^MenuItem, title: ^String) { + msgSend(nil, self, "setTitle:", title) +} +// @(objc_type=MenuItem, objc_name="attributedTitle") +// MenuItem_attributedTitle :: #force_inline proc "c" (self: ^MenuItem) -> ^AttributedString { +// return msgSend(^AttributedString, self, "attributedTitle") +// } +// @(objc_type=MenuItem, objc_name="setAttributedTitle") +// MenuItem_setAttributedTitle :: #force_inline proc "c" (self: ^MenuItem, attributedTitle: ^AttributedString) { +// msgSend(nil, self, "setAttributedTitle:", attributedTitle) +// } +@(objc_type=MenuItem, objc_name="subtitle") +MenuItem_subtitle :: #force_inline proc "c" (self: ^MenuItem) -> ^String { + return msgSend(^String, self, "subtitle") +} +@(objc_type=MenuItem, objc_name="setSubtitle") +MenuItem_setSubtitle :: #force_inline proc "c" (self: ^MenuItem, subtitle: ^String) { + msgSend(nil, self, "setSubtitle:", subtitle) +} +@(objc_type=MenuItem, objc_name="isSeparatorItem") +MenuItem_isSeparatorItem :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isSeparatorItem") +} +@(objc_type=MenuItem, objc_name="isSectionHeader") +MenuItem_isSectionHeader :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isSectionHeader") +} +@(objc_type=MenuItem, objc_name="keyEquivalent") +MenuItem_keyEquivalent :: #force_inline proc "c" (self: ^MenuItem) -> ^String { + return msgSend(^String, self, "keyEquivalent") +} +@(objc_type=MenuItem, objc_name="setKeyEquivalent") +MenuItem_setKeyEquivalent :: #force_inline proc "c" (self: ^MenuItem, keyEquivalent: ^String) { + msgSend(nil, self, "setKeyEquivalent:", keyEquivalent) +} +@(objc_type=MenuItem, objc_name="keyEquivalentModifierMask") +MenuItem_keyEquivalentModifierMask :: #force_inline proc "c" (self: ^MenuItem) -> EventModifierFlags { + return msgSend(EventModifierFlags, self, "keyEquivalentModifierMask") +} +@(objc_type=MenuItem, objc_name="setKeyEquivalentModifierMask") +MenuItem_setKeyEquivalentModifierMask :: #force_inline proc "c" (self: ^MenuItem, keyEquivalentModifierMask: EventModifierFlags) { + msgSend(nil, self, "setKeyEquivalentModifierMask:", keyEquivalentModifierMask) +} +@(objc_type=MenuItem, objc_name="userKeyEquivalent") +MenuItem_userKeyEquivalent :: #force_inline proc "c" (self: ^MenuItem) -> ^String { + return msgSend(^String, self, "userKeyEquivalent") +} +@(objc_type=MenuItem, objc_name="allowsKeyEquivalentWhenHidden") +MenuItem_allowsKeyEquivalentWhenHidden :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "allowsKeyEquivalentWhenHidden") +} +@(objc_type=MenuItem, objc_name="setAllowsKeyEquivalentWhenHidden") +MenuItem_setAllowsKeyEquivalentWhenHidden :: #force_inline proc "c" (self: ^MenuItem, allowsKeyEquivalentWhenHidden: bool) { + msgSend(nil, self, "setAllowsKeyEquivalentWhenHidden:", allowsKeyEquivalentWhenHidden) +} +@(objc_type=MenuItem, objc_name="allowsAutomaticKeyEquivalentLocalization") +MenuItem_allowsAutomaticKeyEquivalentLocalization :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "allowsAutomaticKeyEquivalentLocalization") +} +@(objc_type=MenuItem, objc_name="setAllowsAutomaticKeyEquivalentLocalization") +MenuItem_setAllowsAutomaticKeyEquivalentLocalization :: #force_inline proc "c" (self: ^MenuItem, allowsAutomaticKeyEquivalentLocalization: bool) { + msgSend(nil, self, "setAllowsAutomaticKeyEquivalentLocalization:", allowsAutomaticKeyEquivalentLocalization) +} +@(objc_type=MenuItem, objc_name="allowsAutomaticKeyEquivalentMirroring") +MenuItem_allowsAutomaticKeyEquivalentMirroring :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "allowsAutomaticKeyEquivalentMirroring") +} +@(objc_type=MenuItem, objc_name="setAllowsAutomaticKeyEquivalentMirroring") +MenuItem_setAllowsAutomaticKeyEquivalentMirroring :: #force_inline proc "c" (self: ^MenuItem, allowsAutomaticKeyEquivalentMirroring: bool) { + msgSend(nil, self, "setAllowsAutomaticKeyEquivalentMirroring:", allowsAutomaticKeyEquivalentMirroring) +} +// @(objc_type=MenuItem, objc_name="image") +// MenuItem_image :: #force_inline proc "c" (self: ^MenuItem) -> ^Image { +// return msgSend(^Image, self, "image") +// } +// @(objc_type=MenuItem, objc_name="setImage") +// MenuItem_setImage :: #force_inline proc "c" (self: ^MenuItem, image: ^Image) { +// msgSend(nil, self, "setImage:", image) +// } +// @(objc_type=MenuItem, objc_name="state") +// MenuItem_state :: #force_inline proc "c" (self: ^MenuItem) -> ControlStateValue { +// return msgSend(ControlStateValue, self, "state") +// } +// @(objc_type=MenuItem, objc_name="setState") +// MenuItem_setState :: #force_inline proc "c" (self: ^MenuItem, state: ControlStateValue) { +// msgSend(nil, self, "setState:", state) +// } +// @(objc_type=MenuItem, objc_name="onStateImage") +// MenuItem_onStateImage :: #force_inline proc "c" (self: ^MenuItem) -> ^Image { +// return msgSend(^Image, self, "onStateImage") +// } +// @(objc_type=MenuItem, objc_name="setOnStateImage") +// MenuItem_setOnStateImage :: #force_inline proc "c" (self: ^MenuItem, onStateImage: ^Image) { +// msgSend(nil, self, "setOnStateImage:", onStateImage) +// } +// @(objc_type=MenuItem, objc_name="offStateImage") +// MenuItem_offStateImage :: #force_inline proc "c" (self: ^MenuItem) -> ^Image { +// return msgSend(^Image, self, "offStateImage") +// } +// @(objc_type=MenuItem, objc_name="setOffStateImage") +// MenuItem_setOffStateImage :: #force_inline proc "c" (self: ^MenuItem, offStateImage: ^Image) { +// msgSend(nil, self, "setOffStateImage:", offStateImage) +// } +// @(objc_type=MenuItem, objc_name="mixedStateImage") +// MenuItem_mixedStateImage :: #force_inline proc "c" (self: ^MenuItem) -> ^Image { +// return msgSend(^Image, self, "mixedStateImage") +// } +// @(objc_type=MenuItem, objc_name="setMixedStateImage") +// MenuItem_setMixedStateImage :: #force_inline proc "c" (self: ^MenuItem, mixedStateImage: ^Image) { +// msgSend(nil, self, "setMixedStateImage:", mixedStateImage) +// } +@(objc_type=MenuItem, objc_name="isEnabled") +MenuItem_isEnabled :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isEnabled") +} +@(objc_type=MenuItem, objc_name="setEnabled") +MenuItem_setEnabled :: #force_inline proc "c" (self: ^MenuItem, enabled: bool) { + msgSend(nil, self, "setEnabled:", enabled) +} +@(objc_type=MenuItem, objc_name="isAlternate") +MenuItem_isAlternate :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isAlternate") +} +@(objc_type=MenuItem, objc_name="setAlternate") +MenuItem_setAlternate :: #force_inline proc "c" (self: ^MenuItem, alternate: bool) { + msgSend(nil, self, "setAlternate:", alternate) +} +@(objc_type=MenuItem, objc_name="indentationLevel") +MenuItem_indentationLevel :: #force_inline proc "c" (self: ^MenuItem) -> Integer { + return msgSend(Integer, self, "indentationLevel") +} +@(objc_type=MenuItem, objc_name="setIndentationLevel") +MenuItem_setIndentationLevel :: #force_inline proc "c" (self: ^MenuItem, indentationLevel: Integer) { + msgSend(nil, self, "setIndentationLevel:", indentationLevel) +} +@(objc_type=MenuItem, objc_name="target") +MenuItem_target :: #force_inline proc "c" (self: ^MenuItem) -> id { + return msgSend(id, self, "target") +} +@(objc_type=MenuItem, objc_name="setTarget") +MenuItem_setTarget :: #force_inline proc "c" (self: ^MenuItem, target: id) { + msgSend(nil, self, "setTarget:", target) +} +@(objc_type=MenuItem, objc_name="action") +MenuItem_action :: #force_inline proc "c" (self: ^MenuItem) -> SEL { + return msgSend(SEL, self, "action") +} +@(objc_type=MenuItem, objc_name="setAction") +MenuItem_setAction :: #force_inline proc "c" (self: ^MenuItem, action: SEL) { + msgSend(nil, self, "setAction:", action) +} +@(objc_type=MenuItem, objc_name="tag") +MenuItem_tag :: #force_inline proc "c" (self: ^MenuItem) -> Integer { + return msgSend(Integer, self, "tag") +} +@(objc_type=MenuItem, objc_name="setTag") +MenuItem_setTag :: #force_inline proc "c" (self: ^MenuItem, tag: Integer) { + msgSend(nil, self, "setTag:", tag) +} +@(objc_type=MenuItem, objc_name="representedObject") +MenuItem_representedObject :: #force_inline proc "c" (self: ^MenuItem) -> id { + return msgSend(id, self, "representedObject") +} +@(objc_type=MenuItem, objc_name="setRepresentedObject") +MenuItem_setRepresentedObject :: #force_inline proc "c" (self: ^MenuItem, representedObject: id) { + msgSend(nil, self, "setRepresentedObject:", representedObject) +} +@(objc_type=MenuItem, objc_name="view") +MenuItem_view :: #force_inline proc "c" (self: ^MenuItem) -> ^View { + return msgSend(^View, self, "view") +} +@(objc_type=MenuItem, objc_name="setView") +MenuItem_setView :: #force_inline proc "c" (self: ^MenuItem, view: ^View) { + msgSend(nil, self, "setView:", view) +} +@(objc_type=MenuItem, objc_name="isHighlighted") +MenuItem_isHighlighted :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isHighlighted") +} +@(objc_type=MenuItem, objc_name="isHidden") +MenuItem_isHidden :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isHidden") +} +@(objc_type=MenuItem, objc_name="setHidden") +MenuItem_setHidden :: #force_inline proc "c" (self: ^MenuItem, hidden: bool) { + msgSend(nil, self, "setHidden:", hidden) +} +@(objc_type=MenuItem, objc_name="isHiddenOrHasHiddenAncestor") +MenuItem_isHiddenOrHasHiddenAncestor :: #force_inline proc "c" (self: ^MenuItem) -> bool { + return msgSend(bool, self, "isHiddenOrHasHiddenAncestor") +} +@(objc_type=MenuItem, objc_name="toolTip") +MenuItem_toolTip :: #force_inline proc "c" (self: ^MenuItem) -> ^String { + return msgSend(^String, self, "toolTip") +} +@(objc_type=MenuItem, objc_name="setToolTip") +MenuItem_setToolTip :: #force_inline proc "c" (self: ^MenuItem, toolTip: ^String) { + msgSend(nil, self, "setToolTip:", toolTip) +} +// @(objc_type=MenuItem, objc_name="badge") +// MenuItem_badge :: #force_inline proc "c" (self: ^MenuItem) -> ^MenuItemBadge { +// return msgSend(^MenuItemBadge, self, "badge") +// } +// @(objc_type=MenuItem, objc_name="setBadge") +// MenuItem_setBadge :: #force_inline proc "c" (self: ^MenuItem, badge: ^MenuItemBadge) { +// msgSend(nil, self, "setBadge:", badge) +// } +@(objc_type=MenuItem, objc_name="setMnemonicLocation") +MenuItem_setMnemonicLocation :: #force_inline proc "c" (self: ^MenuItem, location: UInteger) { + msgSend(nil, self, "setMnemonicLocation:", location) +} +@(objc_type=MenuItem, objc_name="mnemonicLocation") +MenuItem_mnemonicLocation :: #force_inline proc "c" (self: ^MenuItem) -> UInteger { + return msgSend(UInteger, self, "mnemonicLocation") +} +@(objc_type=MenuItem, objc_name="mnemonic") +MenuItem_mnemonic :: #force_inline proc "c" (self: ^MenuItem) -> ^String { + return msgSend(^String, self, "mnemonic") +} +@(objc_type=MenuItem, objc_name="setTitleWithMnemonic") +MenuItem_setTitleWithMnemonic :: #force_inline proc "c" (self: ^MenuItem, stringWithAmpersand: ^String) { + msgSend(nil, self, "setTitleWithMnemonic:", stringWithAmpersand) +} +@(objc_type=MenuItem, objc_name="load", objc_is_class_method=true) +MenuItem_load :: #force_inline proc "c" () { + msgSend(nil, MenuItem, "load") +} +@(objc_type=MenuItem, objc_name="initialize", objc_is_class_method=true) +MenuItem_initialize :: #force_inline proc "c" () { + msgSend(nil, MenuItem, "initialize") +} +@(objc_type=MenuItem, objc_name="new", objc_is_class_method=true) +MenuItem_new :: #force_inline proc "c" () -> ^MenuItem { + return msgSend(^MenuItem, MenuItem, "new") +} +@(objc_type=MenuItem, objc_name="allocWithZone", objc_is_class_method=true) +MenuItem_allocWithZone :: #force_inline proc "c" (zone: ^Zone) -> ^MenuItem { + return msgSend(^MenuItem, MenuItem, "allocWithZone:", zone) +} +@(objc_type=MenuItem, objc_name="alloc", objc_is_class_method=true) +MenuItem_alloc :: #force_inline proc "c" () -> ^MenuItem { + return msgSend(^MenuItem, MenuItem, "alloc") +} +@(objc_type=MenuItem, objc_name="copyWithZone", objc_is_class_method=true) +MenuItem_copyWithZone :: #force_inline proc "c" (zone: ^Zone) -> id { + return msgSend(id, MenuItem, "copyWithZone:", zone) +} +@(objc_type=MenuItem, objc_name="mutableCopyWithZone", objc_is_class_method=true) +MenuItem_mutableCopyWithZone :: #force_inline proc "c" (zone: ^Zone) -> id { + return msgSend(id, MenuItem, "mutableCopyWithZone:", zone) +} +@(objc_type=MenuItem, objc_name="instancesRespondToSelector", objc_is_class_method=true) +MenuItem_instancesRespondToSelector :: #force_inline proc "c" (aSelector: SEL) -> bool { + return msgSend(bool, MenuItem, "instancesRespondToSelector:", aSelector) +} +@(objc_type=MenuItem, objc_name="conformsToProtocol", objc_is_class_method=true) +MenuItem_conformsToProtocol :: #force_inline proc "c" (protocol: ^Protocol) -> bool { + return msgSend(bool, MenuItem, "conformsToProtocol:", protocol) +} +@(objc_type=MenuItem, objc_name="instanceMethodForSelector", objc_is_class_method=true) +MenuItem_instanceMethodForSelector :: #force_inline proc "c" (aSelector: SEL) -> IMP { + return msgSend(IMP, MenuItem, "instanceMethodForSelector:", aSelector) +} +// @(objc_type=MenuItem, objc_name="instanceMethodSignatureForSelector", objc_is_class_method=true) +// MenuItem_instanceMethodSignatureForSelector :: #force_inline proc "c" (aSelector: SEL) -> ^MethodSignature { +// return msgSend(^MethodSignature, MenuItem, "instanceMethodSignatureForSelector:", aSelector) +// } +@(objc_type=MenuItem, objc_name="isSubclassOfClass", objc_is_class_method=true) +MenuItem_isSubclassOfClass :: #force_inline proc "c" (aClass: Class) -> bool { + return msgSend(bool, MenuItem, "isSubclassOfClass:", aClass) +} +@(objc_type=MenuItem, objc_name="resolveClassMethod", objc_is_class_method=true) +MenuItem_resolveClassMethod :: #force_inline proc "c" (sel: SEL) -> bool { + return msgSend(bool, MenuItem, "resolveClassMethod:", sel) +} +@(objc_type=MenuItem, objc_name="resolveInstanceMethod", objc_is_class_method=true) +MenuItem_resolveInstanceMethod :: #force_inline proc "c" (sel: SEL) -> bool { + return msgSend(bool, MenuItem, "resolveInstanceMethod:", sel) +} +@(objc_type=MenuItem, objc_name="hash", objc_is_class_method=true) +MenuItem_hash :: #force_inline proc "c" () -> UInteger { + return msgSend(UInteger, MenuItem, "hash") +} +@(objc_type=MenuItem, objc_name="superclass", objc_is_class_method=true) +MenuItem_superclass :: #force_inline proc "c" () -> Class { + return msgSend(Class, MenuItem, "superclass") +} +@(objc_type=MenuItem, objc_name="class", objc_is_class_method=true) +MenuItem_class :: #force_inline proc "c" () -> Class { + return msgSend(Class, MenuItem, "class") +} +@(objc_type=MenuItem, objc_name="description", objc_is_class_method=true) +MenuItem_description :: #force_inline proc "c" () -> ^String { + return msgSend(^String, MenuItem, "description") +} +@(objc_type=MenuItem, objc_name="debugDescription", objc_is_class_method=true) +MenuItem_debugDescription :: #force_inline proc "c" () -> ^String { + return msgSend(^String, MenuItem, "debugDescription") +} +@(objc_type=MenuItem, objc_name="version", objc_is_class_method=true) +MenuItem_version :: #force_inline proc "c" () -> Integer { + return msgSend(Integer, MenuItem, "version") +} +@(objc_type=MenuItem, objc_name="setVersion", objc_is_class_method=true) +MenuItem_setVersion :: #force_inline proc "c" (aVersion: Integer) { + msgSend(nil, MenuItem, "setVersion:", aVersion) +} +@(objc_type=MenuItem, objc_name="poseAsClass", objc_is_class_method=true) +MenuItem_poseAsClass :: #force_inline proc "c" (aClass: Class) { + msgSend(nil, MenuItem, "poseAsClass:", aClass) +} +@(objc_type=MenuItem, objc_name="cancelPreviousPerformRequestsWithTarget_selector_object", objc_is_class_method=true) +MenuItem_cancelPreviousPerformRequestsWithTarget_selector_object :: #force_inline proc "c" (aTarget: id, aSelector: SEL, anArgument: id) { + msgSend(nil, MenuItem, "cancelPreviousPerformRequestsWithTarget:selector:object:", aTarget, aSelector, anArgument) +} +@(objc_type=MenuItem, objc_name="cancelPreviousPerformRequestsWithTarget_", objc_is_class_method=true) +MenuItem_cancelPreviousPerformRequestsWithTarget_ :: #force_inline proc "c" (aTarget: id) { + msgSend(nil, MenuItem, "cancelPreviousPerformRequestsWithTarget:", aTarget) +} +@(objc_type=MenuItem, objc_name="accessInstanceVariablesDirectly", objc_is_class_method=true) +MenuItem_accessInstanceVariablesDirectly :: #force_inline proc "c" () -> bool { + return msgSend(bool, MenuItem, "accessInstanceVariablesDirectly") +} +@(objc_type=MenuItem, objc_name="useStoredAccessor", objc_is_class_method=true) +MenuItem_useStoredAccessor :: #force_inline proc "c" () -> bool { + return msgSend(bool, MenuItem, "useStoredAccessor") +} +@(objc_type=MenuItem, objc_name="keyPathsForValuesAffectingValueForKey", objc_is_class_method=true) +MenuItem_keyPathsForValuesAffectingValueForKey :: #force_inline proc "c" (key: ^String) -> ^Set { + return msgSend(^Set, MenuItem, "keyPathsForValuesAffectingValueForKey:", key) +} +@(objc_type=MenuItem, objc_name="automaticallyNotifiesObserversForKey", objc_is_class_method=true) +MenuItem_automaticallyNotifiesObserversForKey :: #force_inline proc "c" (key: ^String) -> bool { + return msgSend(bool, MenuItem, "automaticallyNotifiesObserversForKey:", key) +} +@(objc_type=MenuItem, objc_name="setKeys", objc_is_class_method=true) +MenuItem_setKeys :: #force_inline proc "c" (keys: ^Array, dependentKey: ^String) { + msgSend(nil, MenuItem, "setKeys:triggerChangeNotificationsForDependentKey:", keys, dependentKey) +} +@(objc_type=MenuItem, objc_name="classFallbacksForKeyedArchiver", objc_is_class_method=true) +MenuItem_classFallbacksForKeyedArchiver :: #force_inline proc "c" () -> ^Array { + return msgSend(^Array, MenuItem, "classFallbacksForKeyedArchiver") +} +@(objc_type=MenuItem, objc_name="classForKeyedUnarchiver", objc_is_class_method=true) +MenuItem_classForKeyedUnarchiver :: #force_inline proc "c" () -> Class { + return msgSend(Class, MenuItem, "classForKeyedUnarchiver") +} +@(objc_type=MenuItem, objc_name="exposeBinding", objc_is_class_method=true) +MenuItem_exposeBinding :: #force_inline proc "c" (binding: ^String) { + msgSend(nil, MenuItem, "exposeBinding:", binding) +} +@(objc_type=MenuItem, objc_name="setDefaultPlaceholder", objc_is_class_method=true) +MenuItem_setDefaultPlaceholder :: #force_inline proc "c" (placeholder: id, marker: id, binding: ^String) { + msgSend(nil, MenuItem, "setDefaultPlaceholder:forMarker:withBinding:", placeholder, marker, binding) +} +@(objc_type=MenuItem, objc_name="defaultPlaceholderForMarker", objc_is_class_method=true) +MenuItem_defaultPlaceholderForMarker :: #force_inline proc "c" (marker: id, binding: ^String) -> id { + return msgSend(id, MenuItem, "defaultPlaceholderForMarker:withBinding:", marker, binding) +} +@(objc_type=MenuItem, objc_name="cancelPreviousPerformRequestsWithTarget") +MenuItem_cancelPreviousPerformRequestsWithTarget :: proc { + MenuItem_cancelPreviousPerformRequestsWithTarget_selector_object, + MenuItem_cancelPreviousPerformRequestsWithTarget_, +} \ No newline at end of file diff --git a/core/sys/darwin/Foundation/objc_helper.odin b/core/sys/darwin/Foundation/objc_helper.odin new file mode 100644 index 000000000..0748d700b --- /dev/null +++ b/core/sys/darwin/Foundation/objc_helper.odin @@ -0,0 +1,136 @@ +package objc_Foundation + +import "base:runtime" +import "base:intrinsics" + +Subclasser_Proc :: proc(cls: Class, vtable: rawptr) + +Object_VTable_Info :: struct { + vtable: rawptr, + size: uint, + impl: Subclasser_Proc, +} + +Class_VTable_Info :: struct { + _context: runtime.Context, + super_vtable: rawptr, + protocol_vtable: rawptr, +} + +@(require_results) +class_get_metaclass :: #force_inline proc "contextless" (cls: Class) -> Class { + return (^Class)(cls)^ +} + +@(require_results) +object_get_vtable_info :: proc "contextless" (obj: id) -> ^Class_VTable_Info { + return (^Class_VTable_Info)(object_getIndexedIvars(obj)) +} + +@(require_results) +make_subclasser :: #force_inline proc(vtable: ^$T, impl: proc(cls: Class, vt: ^T)) -> Object_VTable_Info { + return Object_VTable_Info{ + vtable = vtable, + size = size_of(T), + impl = (Subclasser_Proc)(impl), + } +} + +@(require_results) +register_subclass :: proc( + class_name: cstring, + superclass: Class, + superclass_overrides: Maybe(Object_VTable_Info) = nil, + protocol: Maybe(Object_VTable_Info) = nil, + _context: Maybe(runtime.Context) = nil, +) -> Class { + assert(superclass != nil) + + super_size: uint + proto_size: uint + + if superclass_overrides != nil { + // Align to 8-byte boundary + super_size = (superclass_overrides.?.size + 7)/8 * 8 + } + + if protocol != nil { + // Align to 8-byte boundary + proto_size = (protocol.?.size + 7)/8 * 8 + } + + cls := objc_lookUpClass(class_name) + if cls != nil { + return cls + } + + extra_size := uint(size_of(Class_VTable_Info)) + 8 + super_size + proto_size + + cls = objc_allocateClassPair(superclass, class_name, extra_size) + assert(cls != nil) + + if s, ok := superclass_overrides.?; ok { + s.impl(cls, s.vtable) + } + + if p, ok := protocol.?; ok { + p.impl(cls, p.vtable) + } + + objc_registerClassPair(cls) + meta_cls := class_get_metaclass(cls) + meta_size := uint(class_getInstanceSize(meta_cls)) + + // Offsets are always aligned to 8-byte boundary + info_offset := (meta_size + 7) / 8 * 8 + super_vtable_offset := (info_offset + size_of(Class_VTable_Info) + 7) / 8 * 8 + ptoto_vtable_offset := super_vtable_offset + super_size + + + p_info := (^Class_VTable_Info)(([^]u8)(cls)[info_offset:]) + p_super_vtable := ([^]u8)(cls)[super_vtable_offset:] + p_proto_vtable := ([^]u8)(cls)[ptoto_vtable_offset:] + + intrinsics.mem_zero(p_info, size_of(Class_VTable_Info)) + + // Assign the context + p_info._context = _context.? or_else context + + if s, ok := superclass_overrides.?; ok { + p_info.super_vtable = p_super_vtable + intrinsics.mem_copy(p_super_vtable, s.vtable, super_size) + } + if p, ok := protocol.?; ok { + p_info.protocol_vtable = p_proto_vtable + intrinsics.mem_copy(p_proto_vtable, p.vtable, p.size) + } + + return cls +} + +@(require_results) +class_get_vtable_info :: proc "contextless" (cls: Class) -> ^Class_VTable_Info { + meta_cls := class_get_metaclass(cls) + meta_size := uint(class_getInstanceSize(meta_cls)) + + // Align to 8-byte boundary + info_offset := (meta_size+7) / 8 * 8 + + p_cls := ([^]u8)(cls)[info_offset:] + ctx := (^Class_VTable_Info)(p_cls) + return ctx +} + +@(require_results) +alloc_user_object :: proc "contextless" (cls: Class, _context: Maybe(runtime.Context) = nil) -> id { + info := class_get_vtable_info(cls) + + obj := class_createInstance(cls, size_of(Class_VTable_Info)) + obj_info := (^Class_VTable_Info)(object_getIndexedIvars(obj)) + obj_info^ = info^ + + if _context != nil { + obj_info._context = _context.? + } + return obj +} \ No newline at end of file diff --git a/core/sys/darwin/copyfile.odin b/core/sys/darwin/copyfile.odin new file mode 100644 index 000000000..6c58b8067 --- /dev/null +++ b/core/sys/darwin/copyfile.odin @@ -0,0 +1,67 @@ +package darwin + +import "core:sys/posix" + +copyfile_state_t :: distinct rawptr + +copyfile_flags :: bit_set[enum { + ACL, + STAT, + XATTR, + DATA, + + RECURSIVE = 15, + + CHECK, + EXCL, + NOFOLLOW_SRC, + NOFOLLOW_DST, + MOVE, + UNLINK, + PACK, + UNPACK, + + CLONE, + CLONE_FORCE, + RUN_IN_PLACE, + DATA_SPARSE, + PRESERVE_DST_TRACKED, + VERBOSE = 30, +}; u32] + +COPYFILE_SECURITY :: copyfile_flags{.STAT, .ACL} +COPYFILE_METADATA :: COPYFILE_SECURITY + copyfile_flags{.XATTR} +COPYFILE_ALL :: COPYFILE_METADATA + copyfile_flags{.DATA} + +COPYFILE_NOFOLLOW :: copyfile_flags{.NOFOLLOW_SRC, .NOFOLLOW_DST} + +copyfile_state_flag :: enum u32 { + SRC_FD = 1, + SRC_FILENAME, + DST_FD, + DST_FILENAME, + QUARANTINE, + STATUS_CB, + STATUS_CTX, + COPIED, + XATTRNAME, + WAS_CLONED, + SRC_BSIZE, + DST_BSIZE, + BSIZE, + FORBID_CROSS_MOUNT, + NOCPROTECT, + PRESERVE_SUID, + RECURSIVE_SRC_FTSENT, + FORBID_DST_EXISTING_SYMLINKS, +} + +foreign system { + copyfile :: proc(from, to: cstring, state: copyfile_state_t, flags: copyfile_flags) -> i32 --- + fcopyfile :: proc(from, to: posix.FD, state: copyfile_state_t, flags: copyfile_flags) -> i32 --- + + copyfile_state_alloc :: proc() -> copyfile_state_t --- + copyfile_state_free :: proc(state: copyfile_state_t) -> posix.result --- + copyfile_state_get :: proc(state: copyfile_state_t, flag: copyfile_state_flag, dst: rawptr) -> posix.result --- + copyfile_state_set :: proc(state: copyfile_state_t, flag: copyfile_state_flag, src: rawptr) -> posix.result --- +} diff --git a/core/sys/darwin/darwin.odin b/core/sys/darwin/darwin.odin index d109f5544..96cfc7be6 100644 --- a/core/sys/darwin/darwin.odin +++ b/core/sys/darwin/darwin.odin @@ -3,6 +3,7 @@ package darwin import "core:c" +@(export) foreign import system "system:System.framework" Bool :: b8 diff --git a/core/sys/darwin/sync.odin b/core/sys/darwin/sync.odin index 58fc7c9e4..6d68dc8f8 100644 --- a/core/sys/darwin/sync.odin +++ b/core/sys/darwin/sync.odin @@ -1,7 +1,5 @@ package darwin -foreign import system "system:System.framework" - // #define OS_WAIT_ON_ADDR_AVAILABILITY \ // __API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) when ODIN_OS == .Darwin { diff --git a/core/sys/darwin/xnu_system_call_wrappers.odin b/core/sys/darwin/xnu_system_call_wrappers.odin index 1188091a9..6376949f4 100644 --- a/core/sys/darwin/xnu_system_call_wrappers.odin +++ b/core/sys/darwin/xnu_system_call_wrappers.odin @@ -19,16 +19,6 @@ X_OK :: c.int((1 << 0)) /* test for execute or search permission */ W_OK :: c.int((1 << 1)) /* test for write permission */ R_OK :: c.int((1 << 2)) /* test for read permission */ -/* copyfile flags */ -COPYFILE_ACL :: (1 << 0) -COPYFILE_STAT :: (1 << 1) -COPYFILE_XATTR :: (1 << 2) -COPYFILE_DATA :: (1 << 3) - -COPYFILE_SECURITY :: (COPYFILE_STAT | COPYFILE_ACL) -COPYFILE_METADATA :: (COPYFILE_SECURITY | COPYFILE_XATTR) -COPYFILE_ALL :: (COPYFILE_METADATA | COPYFILE_DATA) - /* syslimits.h */ PATH_MAX :: 1024 /* max bytes in pathname */ diff --git a/core/sys/info/platform_linux.odin b/core/sys/info/platform_linux.odin index 45efc3329..9c342e567 100644 --- a/core/sys/info/platform_linux.odin +++ b/core/sys/info/platform_linux.odin @@ -16,9 +16,13 @@ init_os_version :: proc () { b := strings.builder_from_bytes(version_string_buf[:]) // Try to parse `/etc/os-release` for `PRETTY_NAME="Ubuntu 20.04.3 LTS` - { + pretty_parse: { fd, errno := linux.open("/etc/os-release", {}) - assert(errno == .NONE, "Failed to read /etc/os-release") + if errno != .NONE { + strings.write_string(&b, "Unknown Linux Distro") + break pretty_parse + } + defer { cerrno := linux.close(fd) assert(cerrno == .NONE, "Failed to close the file descriptor") @@ -26,7 +30,10 @@ init_os_version :: proc () { os_release_buf: [2048]u8 n, read_errno := linux.read(fd, os_release_buf[:]) - assert(read_errno == .NONE, "Failed to read data from /etc/os-release") + if read_errno != .NONE { + strings.write_string(&b, "Unknown Linux Distro") + break pretty_parse + } release := string(os_release_buf[:n]) // Search the line in the file until we find "PRETTY_NAME=" @@ -59,7 +66,7 @@ init_os_version :: proc () { os_version.as_string = strings.to_string(b) // Parse the Linux version out of the release string - { + version_loop: { version_num, _, version_suffix := strings.partition(release_str, "-") os_version.version = version_suffix @@ -72,11 +79,11 @@ init_os_version :: proc () { case 0: dst = &os_version.major case 1: dst = &os_version.minor case 2: dst = &os_version.patch - case: break + case: break version_loop } num, ok := strconv.parse_int(part) - if !ok { break } + if !ok { break version_loop } dst^ = num } diff --git a/core/sys/linux/bits.odin b/core/sys/linux/bits.odin index 53660dc8f..d4edf354b 100644 --- a/core/sys/linux/bits.odin +++ b/core/sys/linux/bits.odin @@ -579,7 +579,7 @@ Inotify_Event_Bits :: enum u32 { /* Bits for Mem_Protection bitfield */ -Mem_Protection_Bits :: enum{ +Mem_Protection_Bits :: enum { READ = 0, WRITE = 1, EXEC = 2, @@ -594,11 +594,13 @@ Mem_Protection_Bits :: enum{ /* Bits for Map_Flags + + See `constants.odin` for `MAP_SHARED_VALIDATE` and `MAP_HUGE_16KB`, et al. */ Map_Flags_Bits :: enum { SHARED = 0, PRIVATE = 1, - SHARED_VALIDATE = 2, + DROPPABLE = 3, FIXED = 4, ANONYMOUS = 5, // platform-dependent section start diff --git a/core/sys/linux/constants.odin b/core/sys/linux/constants.odin index b3bbcafb3..1010c931a 100644 --- a/core/sys/linux/constants.odin +++ b/core/sys/linux/constants.odin @@ -373,3 +373,22 @@ PTRACE_SECCOMP_GET_FILTER :: PTrace_Seccomp_Get_Filter_Type(.SECCOMP_GET_FIL PTRACE_SECCOMP_GET_METADATA :: PTrace_Seccomp_Get_Metadata_Type(.SECCOMP_GET_METADATA) PTRACE_GET_SYSCALL_INFO :: PTrace_Get_Syscall_Info_Type(.GET_SYSCALL_INFO) PTRACE_GET_RSEQ_CONFIGURATION :: PTrace_Get_RSeq_Configuration_Type(.GET_RSEQ_CONFIGURATION) + +MAP_SHARED_VALIDATE :: Map_Flags{.SHARED, .PRIVATE} + +MAP_HUGE_SHIFT :: 26 +MAP_HUGE_MASK :: 63 + +MAP_HUGE_16KB :: transmute(Map_Flags)(u32(14) << MAP_HUGE_SHIFT) +MAP_HUGE_64KB :: transmute(Map_Flags)(u32(16) << MAP_HUGE_SHIFT) +MAP_HUGE_512KB :: transmute(Map_Flags)(u32(19) << MAP_HUGE_SHIFT) +MAP_HUGE_1MB :: transmute(Map_Flags)(u32(20) << MAP_HUGE_SHIFT) +MAP_HUGE_2MB :: transmute(Map_Flags)(u32(21) << MAP_HUGE_SHIFT) +MAP_HUGE_8MB :: transmute(Map_Flags)(u32(23) << MAP_HUGE_SHIFT) +MAP_HUGE_16MB :: transmute(Map_Flags)(u32(24) << MAP_HUGE_SHIFT) +MAP_HUGE_32MB :: transmute(Map_Flags)(u32(25) << MAP_HUGE_SHIFT) +MAP_HUGE_256MB :: transmute(Map_Flags)(u32(28) << MAP_HUGE_SHIFT) +MAP_HUGE_512MB :: transmute(Map_Flags)(u32(29) << MAP_HUGE_SHIFT) +MAP_HUGE_1GB :: transmute(Map_Flags)(u32(30) << MAP_HUGE_SHIFT) +MAP_HUGE_2GB :: transmute(Map_Flags)(u32(31) << MAP_HUGE_SHIFT) +MAP_HUGE_16GB :: transmute(Map_Flags)(u32(34) << MAP_HUGE_SHIFT) \ No newline at end of file diff --git a/core/sys/linux/types.odin b/core/sys/linux/types.odin index 8f2284f56..08e0026d3 100644 --- a/core/sys/linux/types.odin +++ b/core/sys/linux/types.odin @@ -288,7 +288,7 @@ Rename_Flags :: bit_set[Rename_Flags_Bits; u32] /* Directory entry record. - Recommended iterate these with `dirent_iterator()`, + Recommended to iterate these with `dirent_iterate_buf()`, and obtain the name via `dirent_name()`. */ Dirent :: struct { @@ -368,6 +368,8 @@ Mem_Protection :: bit_set[Mem_Protection_Bits; i32] /* Flags for mmap. + + See `constants.odin` for `MAP_SHARED_VALIDATE` and `MAP_HUGE_16KB`, et al. */ Map_Flags :: bit_set[Map_Flags_Bits; i32] diff --git a/core/sys/linux/wrappers.odin b/core/sys/linux/wrappers.odin index ab1992a57..53eb80f86 100644 --- a/core/sys/linux/wrappers.odin +++ b/core/sys/linux/wrappers.odin @@ -54,22 +54,45 @@ WCOREDUMP :: #force_inline proc "contextless" (s: u32) -> bool { // TODO: sigaddset etc -/// Iterate the results of getdents -/// Only iterates as much data as loaded in the buffer -/// In case you need to iterate *all* files in a directory -/// consider using dirent_get_iterate -/// -/// Example of using dirent_iterate_buf -/// // Get dirents into a buffer -/// buf: [128]u8 -/// sys.getdents(dirfd, buf[:]) -/// // Print the names of the files -/// for dir in sys.dirent_iterate_buf(buf[:], &offs) { -/// name := sys.dirent_name(dir) -/// fmt.println(name) -/// } -/// This function doesn't automatically make a request -/// for the buffer to be refilled +/* +Iterate the results of `getdents()`. + +This procedure extracts a directory entry from `buf` at the offset `offs`. +`offs` will be modified to store an offset to the possible next directory entry +in `buf`. The procedure only iterates as much data as loaded in the buffer and +does not automatically make a request for the buffer to be refilled. + +Inputs: +- buf: A byte buffer with data from `getdents()` +- offs: An offset to the next possible directory entry in `buf` + +Returns: +- A pointer to a directory entry in `buf`, or `nil` +- A bool value denoting if a valid directory entry is returned + +Example: + + import "core:fmt" + import "core:sys/linux" + + print_names :: proc(dirfd: linux.Fd) { + // Get dirents into a buffer. + buf: [128]u8 + // Loop until there are no more entries. + for { + written, err := linux.getdents(dirfd, buf[:]) + if err != .NONE || written == 0 { + break + } + // Print the names of the files. + offset : int + for dir in linux.dirent_iterate_buf(buf[:written], &offset) { + name := linux.dirent_name(dir) + fmt.println(name) + } + } + } +*/ dirent_iterate_buf :: proc "contextless" (buf: []u8, offs: ^int) -> (d: ^Dirent, cont: bool) { // Stopped iterating when there's no space left if offs^ >= len(buf) { @@ -82,8 +105,17 @@ dirent_iterate_buf :: proc "contextless" (buf: []u8, offs: ^int) -> (d: ^Dirent, return dirent, true } -/// Obtain the name of dirent as a string -/// The lifetime of the string is bound to the lifetime of the provided dirent structure +/* +Obtain the name of dirent as a string. + +The lifetime of the returned string is bound to the lifetime of the provided dirent structure. + +Inputs: +- dirent: A directory entry + +Returns: +- A name of the entry +*/ dirent_name :: proc "contextless" (dirent: ^Dirent) -> string #no_bounds_check { str := ([^]u8)(&dirent.name) // Dirents are aligned to 8 bytes, so there is guaranteed to be a null @@ -93,10 +125,10 @@ dirent_name :: proc "contextless" (dirent: ^Dirent) -> string #no_bounds_check { trunc := min(str_size, 8) str_size -= trunc for _ in 0.. HBRUSH --- CreateDIBitmap :: proc(hdc: HDC, pbmih: ^BITMAPINFOHEADER, flInit: DWORD, pjBits: VOID, pbmi: ^BITMAPINFO, iUsage: UINT) -> HBITMAP --- - CreateDIBSection :: proc(hdc: HDC, pbmi: ^BITMAPINFO, usage: UINT, ppvBits: VOID, hSection: HANDLE, offset: DWORD) -> HBITMAP --- + CreateDIBSection :: proc(hdc: HDC, pbmi: ^BITMAPINFO, usage: UINT, ppvBits: ^^VOID, hSection: HANDLE, offset: DWORD) -> HBITMAP --- StretchDIBits :: proc(hdc: HDC, xDest, yDest, DestWidth, DestHeight, xSrc, ySrc, SrcWidth, SrcHeight: INT, lpBits: VOID, lpbmi: ^BITMAPINFO, iUsage: UINT, rop: DWORD) -> INT --- StretchBlt :: proc(hdcDest: HDC, xDest, yDest, wDest, hDest: INT, hdcSrc: HDC, xSrc, ySrc, wSrc, hSrc: INT, rop: DWORD) -> BOOL --- diff --git a/core/sys/windows/scan_codes.odin b/core/sys/windows/scan_codes.odin new file mode 100644 index 000000000..54949c2f6 --- /dev/null +++ b/core/sys/windows/scan_codes.odin @@ -0,0 +1,172 @@ +#+build windows +package sys_windows + +// Win32 scan codes for QWERTY layout +// https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#scan-codes + +KB_SYS_POWERDOWN :: 0xE05E +KB_SYS_SLEEP :: 0xE05F +KB_SYS_WAKEUP :: 0xE063 +KB_ERR_ROLLOVER :: 0x00FF + +KB_A :: 0x001E +KB_B :: 0x0030 +KB_C :: 0x002E +KB_D :: 0x0020 +KB_E :: 0x0012 +KB_F :: 0x0021 +KB_G :: 0x0022 +KB_H :: 0x0023 +KB_I :: 0x0017 +KB_J :: 0x0024 +KB_K :: 0x0025 +KB_L :: 0x0026 +KB_M :: 0x0032 +KB_N :: 0x0031 +KB_O :: 0x0018 +KB_P :: 0x0019 +KB_Q :: 0x0010 +KB_R :: 0x0013 +KB_S :: 0x001F +KB_T :: 0x0014 +KB_U :: 0x0016 +KB_V :: 0x002F +KB_W :: 0x0011 +KB_X :: 0x002D +KB_Y :: 0x0015 +KB_Z :: 0x002C + +KB_1_BANG :: 0x0002 +KB_2_AT :: 0x0003 +KB_3_HASH :: 0x0004 +KB_4_DOLLAR :: 0x0005 +KB_5_PERCENT :: 0x0006 +KB_6_CARET :: 0x0007 +KB_7_AMPERSAND :: 0x0008 +KB_8_STAR :: 0x0009 +KB_9_LEFTBRACKET :: 0x000A +KB_0_RIGHTBRACKET :: 0x000B + +KB_RETURN_ENTER :: 0x001C +KB_ESCAPE :: 0x0001 +KB_DELETE :: 0x000E +KB_TAB :: 0x000F +KB_SPACEBAR :: 0x0039 +KB_DASH_UNDERSCORE :: 0x000C +KB_EQUALS_PLUS :: 0x000D +KB_LEFTBRACE :: 0x001A +KB_RIGHTBRACE :: 0x001B +KB_PIPE_SLASH :: 0x002B +KB_NONUS :: 0x002B +KB_SEMICOLON_COLON :: 0x0027 +KB_APOSTR_DOUBLEQUOT :: 0x0028 +KB_GRAVEACC_TILDE :: 0x0029 +KB_COMMA :: 0x0033 +KB_PERIOD :: 0x0034 +KB_QUESTIONMARK :: 0x0035 +KB_CAPSLOCK :: 0x003A + +KB_F1 :: 0x003B +KB_F2 :: 0x003C +KB_F3 :: 0x003D +KB_F4 :: 0x003E +KB_F5 :: 0x003F +KB_F6 :: 0x0040 +KB_F7 :: 0x0041 +KB_F8 :: 0x0042 +KB_F9 :: 0x0043 +KB_F10 :: 0x0044 +KB_F11 :: 0x0057 +KB_F12 :: 0x0058 + +KB_PRINTSCREEN :: 0xE037 +KB_SCROLLLOCK :: 0x0046 +KB_PAUSE :: 0xE11D45 +KB_INSERT :: 0xE052 +KB_HOME :: 0xE047 +KB_PAGEUP :: 0xE049 +KB_DELETEFORWARD :: 0xE053 +KB_END :: 0xE04F +KB_PAGEDOWN :: 0xE051 +KB_RIGHTARROW :: 0xE04D +KB_LEFTARROW :: 0xE04B +KB_DOWNARROW :: 0xE050 +KB_UPARROW :: 0xE048 + +KP_NUMLOCK_CLEAR :: 0x0045 +KP_FORWARDSLASH :: 0xE035 +KP_STAR :: 0x0037 +KP_DASH :: 0x004A +KP_PLUS :: 0x004E +KP_ENTER :: 0xE01C +KP_1_END :: 0x004F +KP_2_DOWNARROW :: 0x0050 +KP_3_PAGEDN :: 0x0051 +KP_4_LEFTARROW :: 0x004B +KP_5 :: 0x004C +KP_6_RIGHTARROW :: 0x004D +KP_7_HOME :: 0x0047 +KP_8_UPARROW :: 0x0048 +KP_9_PAGEUP :: 0x0049 +KP_0_INSERT :: 0x0052 +KP_PERIOD :: 0x0053 + +KB_NONUS_SLASHBAR :: 0x0056 +KB_APPLICATION :: 0xE05D +KB_POWER :: 0xE05E +KB_EQUALS :: 0x0059 +KB_F13 :: 0x0064 +KB_F14 :: 0x0065 +KB_F15 :: 0x0066 +KB_F16 :: 0x0067 +KB_F17 :: 0x0068 +KB_F18 :: 0x0069 +KB_F19 :: 0x006A +KB_F20 :: 0x006B +KB_F21 :: 0x006C +KB_F22 :: 0x006D +KB_F23 :: 0x006E +KB_F24 :: 0x0076 + +KP_COMMA :: 0x007E + +KB_INTERNATIONAL1 :: 0x0073 +KB_INTERNATIONAL2 :: 0x0070 +KB_INTERNATIONAL3 :: 0x007D +KB_INTERNATIONAL4 :: 0x0079 +KB_INTERNATIONAL5 :: 0x007B +KB_INTERNATIONAL6 :: 0x005C + +KB_LANG1 :: 0x0072 +KB_LANG2 :: 0x0071 +KB_LANG3 :: 0x0078 +KB_LANG4 :: 0x0077 +KB_LANG5 :: 0x0076 + +KB_LEFTCONTROL :: 0x001D +KB_LEFTSHIFT :: 0x002A +KB_LEFTALT :: 0x0038 +KB_LEFTGUI :: 0xE05B +KB_RIGHTCONTROL :: 0xE01D +KB_RIGHTSHIFT :: 0x0036 +KB_RIGHTALT :: 0xE038 +KB_RIGHTGUI :: 0xE05C + +FN_SCANNEXTTRACK :: 0xE019 +FN_SCANPREVTRACK :: 0xE010 +FN_STOP :: 0xE024 +FN_PLAY_PAUSE :: 0xE022 +FN_MUTE :: 0xE020 +FN_VOLUMEINC :: 0xE030 +FN_VOLUMEDEC :: 0xE02E +FN_AL_CONSUMERCTRLCONFIG :: 0xE06D +FN_AL_EMAILREADER :: 0xE06C +FN_AL_CALCULATOR :: 0xE021 +FN_AL_LOCALMACHINEBROWSER :: 0xE06B +FN_AC_SEARCH :: 0xE065 +FN_AC_HOME :: 0xE032 +FN_AC_BACK :: 0xE06A +FN_AC_FORWARD :: 0xE069 +FN_AC_STOP :: 0xE068 +FN_AC_REFRESH :: 0xE067 +FN_AC_BOOKMARKS :: 0xE066 diff --git a/core/sys/windows/user32.odin b/core/sys/windows/user32.odin index 94cd57811..49ebb49cb 100644 --- a/core/sys/windows/user32.odin +++ b/core/sys/windows/user32.odin @@ -47,6 +47,8 @@ foreign user32 { lpParam: LPVOID, ) -> HWND --- + GetWindowThreadProcessId :: proc(hwnd: HWND, lpdwProcessId: LPDWORD) -> DWORD --- + DestroyWindow :: proc(hWnd: HWND) -> BOOL --- ShowWindow :: proc(hWnd: HWND, nCmdShow: INT) -> BOOL --- diff --git a/core/sys/windows/ws2_32.odin b/core/sys/windows/ws2_32.odin index 5b2952495..d808bcd09 100644 --- a/core/sys/windows/ws2_32.odin +++ b/core/sys/windows/ws2_32.odin @@ -91,6 +91,7 @@ foreign ws2_32 { WSACleanup :: proc() -> c_int --- // [MS-Docs](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsagetlasterror) WSAGetLastError :: proc() -> c_int --- + WSASetLastError :: proc(err: c_int) --- // [MS-Docs](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll) WSAPoll :: proc(fdArray: ^WSA_POLLFD, fds: c_ulong, timeout: c_int) -> c_int --- // [MS-Docs](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaduplicatesocketw) diff --git a/core/encoding/ansi/ansi.odin b/core/terminal/ansi/ansi.odin similarity index 100% rename from core/encoding/ansi/ansi.odin rename to core/terminal/ansi/ansi.odin diff --git a/core/encoding/ansi/doc.odin b/core/terminal/ansi/doc.odin similarity index 100% rename from core/encoding/ansi/doc.odin rename to core/terminal/ansi/doc.odin diff --git a/core/terminal/doc.odin b/core/terminal/doc.odin new file mode 100644 index 000000000..490e9d398 --- /dev/null +++ b/core/terminal/doc.odin @@ -0,0 +1,4 @@ +/* +This package is for interacting with the command line interface of the system. +*/ +package terminal diff --git a/core/terminal/internal.odin b/core/terminal/internal.odin new file mode 100644 index 000000000..485f6868d --- /dev/null +++ b/core/terminal/internal.odin @@ -0,0 +1,87 @@ +#+private +package terminal + +import "core:os" +import "core:strings" + +// Reference documentation: +// +// - [[ https://no-color.org/ ]] +// - [[ https://github.com/termstandard/colors ]] +// - [[ https://invisible-island.net/ncurses/terminfo.src.html ]] + +get_no_color :: proc() -> bool { + if no_color, ok := os.lookup_env("NO_COLOR"); ok { + defer delete(no_color) + return no_color != "" + } + return false +} + +get_environment_color :: proc() -> Color_Depth { + // `COLORTERM` is non-standard but widespread and unambiguous. + if colorterm, ok := os.lookup_env("COLORTERM"); ok { + defer delete(colorterm) + // These are the only values that are typically advertised that have + // anything to do with color depth. + if colorterm == "truecolor" || colorterm == "24bit" { + return .True_Color + } + } + + if term, ok := os.lookup_env("TERM"); ok { + defer delete(term) + if strings.contains(term, "-truecolor") { + return .True_Color + } + if strings.contains(term, "-256color") { + return .Eight_Bit + } + if strings.contains(term, "-16color") { + return .Four_Bit + } + + // The `terminfo` database, which is stored in binary on *nix + // platforms, has an undocumented format that is not guaranteed to be + // portable, so beyond this point, we can only make safe assumptions. + // + // This section should only be necessary for terminals that do not + // define any of the previous environment values. + // + // Only a small sampling of some common values are checked here. + switch term { + case "ansi": fallthrough + case "konsole": fallthrough + case "putty": fallthrough + case "rxvt": fallthrough + case "rxvt-color": fallthrough + case "screen": fallthrough + case "st": fallthrough + case "tmux": fallthrough + case "vte": fallthrough + case "xterm": fallthrough + case "xterm-color": + return .Three_Bit + } + } + + return .None +} + +@(init) +init_terminal :: proc() { + _init_terminal() + + // We respect `NO_COLOR` specifically as a color-disabler but not as a + // blanket ban on any terminal manipulation codes, hence why this comes + // after `_init_terminal` which will allow Windows to enable Virtual + // Terminal Processing for non-color control sequences. + if !get_no_color() { + color_enabled = color_depth > .None + } +} + +@(fini) +fini_terminal :: proc() { + _fini_terminal() +} diff --git a/core/terminal/terminal.odin b/core/terminal/terminal.odin new file mode 100644 index 000000000..1e5566295 --- /dev/null +++ b/core/terminal/terminal.odin @@ -0,0 +1,36 @@ +package terminal + +import "core:os" + +/* +This describes the range of colors that a terminal is capable of supporting. +*/ +Color_Depth :: enum { + None, // No color support + Three_Bit, // 8 colors + Four_Bit, // 16 colors + Eight_Bit, // 256 colors + True_Color, // 24-bit true color +} + +/* +Returns true if the file `handle` is attached to a terminal. + +This is normally true for `os.stdout` and `os.stderr` unless they are +redirected to a file. +*/ +@(require_results) +is_terminal :: proc(handle: os.Handle) -> bool { + return _is_terminal(handle) +} + +/* +This is true if the terminal is accepting any form of colored text output. +*/ +color_enabled: bool + +/* +This value reports the color depth support as reported by the terminal at the +start of the program. +*/ +color_depth: Color_Depth diff --git a/core/terminal/terminal_posix.odin b/core/terminal/terminal_posix.odin new file mode 100644 index 000000000..f578e12c6 --- /dev/null +++ b/core/terminal/terminal_posix.odin @@ -0,0 +1,16 @@ +#+private +#+build linux, darwin, netbsd, openbsd, freebsd, haiku +package terminal + +import "core:os" +import "core:sys/posix" + +_is_terminal :: proc(handle: os.Handle) -> bool { + return bool(posix.isatty(posix.FD(handle))) +} + +_init_terminal :: proc() { + color_depth = get_environment_color() +} + +_fini_terminal :: proc() { } diff --git a/core/terminal/terminal_windows.odin b/core/terminal/terminal_windows.odin new file mode 100644 index 000000000..18ec98332 --- /dev/null +++ b/core/terminal/terminal_windows.odin @@ -0,0 +1,60 @@ +#+private +package terminal + +import "core:os" +import "core:sys/windows" + +_is_terminal :: proc(handle: os.Handle) -> bool { + is_tty := windows.GetFileType(windows.HANDLE(handle)) == windows.FILE_TYPE_CHAR + return is_tty +} + +old_modes: [2]struct{ + handle: windows.DWORD, + mode: windows.DWORD, +} = { + {windows.STD_OUTPUT_HANDLE, 0}, + {windows.STD_ERROR_HANDLE, 0}, +} + +@(init) +_init_terminal :: proc() { + vtp_enabled: bool + + for &v in old_modes { + handle := windows.GetStdHandle(v.handle) + if handle == windows.INVALID_HANDLE || handle == nil { + return + } + if windows.GetConsoleMode(handle, &v.mode) { + windows.SetConsoleMode(handle, v.mode | windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + new_mode: windows.DWORD + windows.GetConsoleMode(handle, &new_mode) + + if new_mode & (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 { + vtp_enabled = true + } + } + } + + if vtp_enabled { + // This color depth is available on Windows 10 since build 10586. + color_depth = .Four_Bit + } else { + // The user may be on a non-default terminal emulator. + color_depth = get_environment_color() + } +} + +@(fini) +_fini_terminal :: proc() { + for v in old_modes { + handle := windows.GetStdHandle(v.handle) + if handle == windows.INVALID_HANDLE || handle == nil { + return + } + + windows.SetConsoleMode(handle, v.mode) + } +} diff --git a/core/testing/reporting.odin b/core/testing/reporting.odin index 6752cd79b..7c7eb7b2d 100644 --- a/core/testing/reporting.odin +++ b/core/testing/reporting.odin @@ -10,12 +10,12 @@ package testing */ import "base:runtime" -import "core:encoding/ansi" import "core:fmt" import "core:io" import "core:mem" import "core:path/filepath" import "core:strings" +import "core:terminal/ansi" // Definitions of colors for use in the test runner. SGR_RESET :: ansi.CSI + ansi.RESET + ansi.SGR diff --git a/core/testing/runner.odin b/core/testing/runner.odin index 83a5ac4e7..56d561d3d 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -13,7 +13,6 @@ package testing import "base:intrinsics" import "base:runtime" import "core:bytes" -import "core:encoding/ansi" @require import "core:encoding/base64" @require import "core:encoding/json" import "core:fmt" @@ -25,6 +24,8 @@ import "core:os" import "core:slice" @require import "core:strings" import "core:sync/chan" +import "core:terminal" +import "core:terminal/ansi" import "core:thread" import "core:time" @@ -44,6 +45,7 @@ PER_THREAD_MEMORY : int : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_S // The format is: `package.test_name,test_name_only,...` TEST_NAMES : string : #config(ODIN_TEST_NAMES, "") // Show the fancy animated progress report. +// This requires terminal color support, as well as STDOUT to not be redirected to a file. FANCY_OUTPUT : bool : #config(ODIN_TEST_FANCY, true) // Copy failed tests to the clipboard when done. USE_CLIPBOARD : bool : #config(ODIN_TEST_CLIPBOARD, false) @@ -70,6 +72,9 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level { } } +@(private) global_log_colors_disabled: bool +@(private) global_ansi_disabled: bool + JSON :: struct { total: int, success: int, @@ -129,11 +134,16 @@ run_test_task :: proc(task: thread.Task) { context.assertion_failure_proc = test_assertion_failure_proc + logger_options := Default_Test_Logger_Opts + if global_log_colors_disabled { + logger_options -= {.Terminal_Color} + } + context.logger = { procedure = test_logger_proc, data = &data.t, lowest_level = get_log_level(), - options = Default_Test_Logger_Opts, + options = logger_options, } random_generator_state: runtime.Default_Random_State @@ -204,13 +214,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { } } - when ODIN_OS == .Windows { - console_ansi_init() - } - stdout := io.to_writer(os.stream_from_handle(os.stdout)) stderr := io.to_writer(os.stream_from_handle(os.stderr)) + // The animations are only ever shown through STDOUT; + // STDERR is used exclusively for logging regardless of error level. + global_log_colors_disabled = !terminal.color_enabled || !terminal.is_terminal(os.stderr) + global_ansi_disabled = !terminal.is_terminal(os.stdout) + + should_show_animations := FANCY_OUTPUT && terminal.color_enabled && !global_ansi_disabled + // -- Prepare test data. alloc_error: mem.Allocator_Error @@ -268,12 +281,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { total_done_count := 0 total_test_count := len(internal_tests) - when !FANCY_OUTPUT { - // This is strictly for updating the window title when the progress - // report is disabled. We're otherwise able to depend on the call to - // `needs_to_redraw`. - last_done_count := -1 - } + + // This is strictly for updating the window title when the progress + // report is disabled. We're otherwise able to depend on the call to + // `needs_to_redraw`. + last_done_count := -1 + if total_test_count == 0 { // Exit early. @@ -342,31 +355,31 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error) defer destroy_report(&report) - when FANCY_OUTPUT { - // We cannot make use of the ANSI save/restore cursor codes, because they - // work by absolute screen coordinates. This will cause unnecessary - // scrollback if we print at the bottom of someone's terminal. - ansi_redraw_string := fmt.aprintf( - // ANSI for "go up N lines then erase the screen from the cursor forward." - ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED + - // We'll combine this with the window title format string, since it - // can be printed at the same time. - "%s", - // 1 extra line for the status bar. - 1 + len(report.packages), OSC_WINDOW_TITLE) - assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.") - defer delete(ansi_redraw_string) - thread_count_status_string: string = --- - { - PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH + // We cannot make use of the ANSI save/restore cursor codes, because they + // work by absolute screen coordinates. This will cause unnecessary + // scrollback if we print at the bottom of someone's terminal. + ansi_redraw_string := fmt.aprintf( + // ANSI for "go up N lines then erase the screen from the cursor forward." + ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED + + // We'll combine this with the window title format string, since it + // can be printed at the same time. + "%s", + // 1 extra line for the status bar. + 1 + len(report.packages), OSC_WINDOW_TITLE) + assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.") + defer delete(ansi_redraw_string) - unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s") - thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING) - assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.") - } - defer delete(thread_count_status_string) + thread_count_status_string: string = --- + { + PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH + + unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s") + thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING) + assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.") } + defer delete(thread_count_status_string) + task_data_slots: []Task_Data = --- task_data_slots, alloc_error = make([]Task_Data, thread_count) @@ -442,11 +455,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { // digging through the source to divine everywhere it is used for that. shared_log_allocator := context.allocator + logger_options := Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure} + if global_log_colors_disabled { + logger_options -= {.Terminal_Color} + } + context.logger = { procedure = runner_logger_proc, data = &log_messages, lowest_level = get_log_level(), - options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure}, + options = logger_options, } run_index: int @@ -481,11 +499,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { setup_signal_handler() - fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE) + if !global_ansi_disabled { + fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE) + } - when FANCY_OUTPUT { - signals_were_raised := false + signals_were_raised := false + if should_show_animations { redraw_report(stdout, report) draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count) } @@ -703,22 +723,22 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { break main_loop } - when FANCY_OUTPUT { - // Because the bounds checking procs send directly to STDERR with - // no way to redirect or handle them, we need to at least try to - // let the user see those messages when using the animated progress - // report. This flag may be set by the block of code below if a - // signal is raised. - // - // It'll be purely by luck if the output is interleaved properly, - // given the nature of non-thread-safe printing. - // - // At worst, if Odin did not print any error for this signal, we'll - // just re-display the progress report. The fatal log error message - // should be enough to clue the user in that something dire has - // occurred. - bypass_progress_overwrite := false - } + + // Because the bounds checking procs send directly to STDERR with + // no way to redirect or handle them, we need to at least try to + // let the user see those messages when using the animated progress + // report. This flag may be set by the block of code below if a + // signal is raised. + // + // It'll be purely by luck if the output is interleaved properly, + // given the nature of non-thread-safe printing. + // + // At worst, if Odin did not print any error for this signal, we'll + // just re-display the progress report. The fatal log error message + // should be enough to clue the user in that something dire has + // occurred. + bypass_progress_overwrite := false + if test_index, reason, ok := should_stop_test(); ok { #no_bounds_check report.all_test_states[test_index] = .Failed @@ -752,7 +772,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason) } - when FANCY_OUTPUT { + if should_show_animations { bypass_progress_overwrite = true signals_were_raised = true } @@ -766,7 +786,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { // -- Redraw. - when FANCY_OUTPUT { + if should_show_animations { if len(log_messages) == 0 && !needs_to_redraw(report) { continue main_loop } @@ -776,7 +796,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { } } else { if total_done_count != last_done_count { - fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count) + if !global_ansi_disabled { + fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count) + } last_done_count = total_done_count } @@ -801,7 +823,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { clear(&log_messages) bytes.buffer_reset(&batch_buffer) - when FANCY_OUTPUT { + if should_show_animations { redraw_report(batch_writer, report) draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count) fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer)) @@ -822,7 +844,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { finished_in := time.since(start_time) - when !FANCY_OUTPUT { + if !should_show_animations || !terminal.is_terminal(os.stderr) { // One line to space out the results, since we don't have the status // bar in plain mode. fmt.wprintln(batch_writer) @@ -836,24 +858,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { if total_done_count != total_test_count { not_run_count := total_test_count - total_done_count + message := " %i %s left undone." if global_log_colors_disabled else " " + SGR_READY + "%i" + SGR_RESET + " %s left undone." fmt.wprintf(batch_writer, - " " + SGR_READY + "%i" + SGR_RESET + " %s left undone.", + message, not_run_count, "test was" if not_run_count == 1 else "tests were") } if total_success_count == total_test_count { + message := " %s successful." if global_log_colors_disabled else " %s " + SGR_SUCCESS + "successful." + SGR_RESET fmt.wprintfln(batch_writer, - " %s " + SGR_SUCCESS + "successful." + SGR_RESET, + message, "The test was" if total_test_count == 1 else "All tests were") } else if total_failure_count > 0 { if total_failure_count == total_test_count { + message := " %s failed." if global_log_colors_disabled else " %s " + SGR_FAILED + "failed." + SGR_RESET fmt.wprintfln(batch_writer, - " %s " + SGR_FAILED + "failed." + SGR_RESET, + message, "The test" if total_test_count == 1 else "All tests") } else { + message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed." fmt.wprintfln(batch_writer, - " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.", + message, total_failure_count, "" if total_failure_count == 1 else "s") } @@ -907,9 +933,11 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { } } - fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW) + if !global_ansi_disabled { + fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW) + } - when FANCY_OUTPUT { + if should_show_animations { if signals_were_raised { fmt.wprintln(batch_writer, ` Signals were raised during this test run. Log messages are likely to have collided with each other. diff --git a/core/testing/runner_windows.odin b/core/testing/runner_windows.odin deleted file mode 100644 index 401804c71..000000000 --- a/core/testing/runner_windows.odin +++ /dev/null @@ -1,22 +0,0 @@ -#+private -package testing - -import win32 "core:sys/windows" - -console_ansi_init :: proc() { - stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE) - if stdout != win32.INVALID_HANDLE && stdout != nil { - old_console_mode: u32 - if win32.GetConsoleMode(stdout, &old_console_mode) { - win32.SetConsoleMode(stdout, old_console_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING) - } - } - - stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE) - if stderr != win32.INVALID_HANDLE && stderr != nil { - old_console_mode: u32 - if win32.GetConsoleMode(stderr, &old_console_mode) { - win32.SetConsoleMode(stderr, old_console_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING) - } - } -} diff --git a/core/testing/signal_handler_libc.odin b/core/testing/signal_handler_libc.odin index 281fbde40..f9527e22f 100644 --- a/core/testing/signal_handler_libc.odin +++ b/core/testing/signal_handler_libc.odin @@ -12,9 +12,9 @@ package testing import "base:intrinsics" import "core:c/libc" -import "core:encoding/ansi" -import "core:sync" import "core:os" +import "core:sync" +import "core:terminal/ansi" @(private="file") stop_runner_flag: libc.sig_atomic_t @@ -63,9 +63,11 @@ stop_test_callback :: proc "c" (sig: libc.int) { // NOTE(Feoramund): Using these write calls in a signal handler is // undefined behavior in C99 but possibly tolerated in POSIX 2008. // Either way, we may as well try to salvage what we can. - show_cursor := ansi.CSI + ansi.DECTCEM_SHOW - libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout) - libc.fflush(libc.stdout) + if !global_ansi_disabled { + show_cursor := ansi.CSI + ansi.DECTCEM_SHOW + libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout) + libc.fflush(libc.stdout) + } // This is an attempt at being compliant by avoiding printf. sigbuf: [8]byte diff --git a/core/text/regex/regex.odin b/core/text/regex/regex.odin index 8f8efe252..c805740f7 100644 --- a/core/text/regex/regex.odin +++ b/core/text/regex/regex.odin @@ -8,6 +8,7 @@ package regex Feoramund: Initial implementation. */ +import "base:runtime" import "core:text/regex/common" import "core:text/regex/compiler" import "core:text/regex/optimizer" @@ -27,6 +28,8 @@ Creation_Error :: enum { Expected_Delimiter, // An unknown letter was supplied to `create_by_user` after the last delimiter. Unknown_Flag, + // An unsupported flag was supplied. + Unsupported_Flag, } Error :: union #shared_nil { @@ -51,7 +54,7 @@ This struct corresponds to a set of string captures from a RegEx match. such that `str[pos[0][0]:pos[0][1]] == groups[0]`. */ Capture :: struct { - pos: [][2]int, + pos: [][2]int, groups: []string, } @@ -59,11 +62,22 @@ Capture :: struct { A compiled Regular Expression value, to be used with the `match_*` procedures. */ Regular_Expression :: struct { - flags: Flags `fmt:"-"`, + flags: Flags `fmt:"-"`, class_data: []virtual_machine.Rune_Class_Data `fmt:"-"`, - program: []virtual_machine.Opcode `fmt:"-"`, + program: []virtual_machine.Opcode `fmt:"-"`, } +/* +An iterator to repeatedly match a pattern against a string, to be used with `*_iterator` procedures. +Note: Does not handle `.Multiline` properly. +*/ +Match_Iterator :: struct { + regex: Regular_Expression, + capture: Capture, + vm: virtual_machine.Machine, + idx: int, + temp: runtime.Allocator, +} /* Create a regular expression from a string pattern and a set of flags. @@ -245,6 +259,45 @@ create_by_user :: proc( return create(pattern[start:end], flags, permanent_allocator, temporary_allocator) } +/* +Create a `Match_Iterator` using a string to search, a regular expression to match against it, and a set of flags. + +*Allocates Using Provided Allocators* + +Inputs: +- str: The string to iterate over. +- pattern: The pattern to match. +- flags: A `bit_set` of RegEx flags. +- permanent_allocator: The allocator to use for the compiled regular expression. (default: context.allocator) +- temporary_allocator: The allocator to use for the intermediate compilation and iteration stages. (default: context.temp_allocator) + +Returns: +- result: The `Match_Iterator`. +- err: An error, if one occurred. +*/ +create_iterator :: proc( + str: string, + pattern: string, + flags: Flags = {}, + permanent_allocator := context.allocator, + temporary_allocator := context.temp_allocator, +) -> (result: Match_Iterator, err: Error) { + flags := flags + flags += {.Global} // We're iterating over a string, so the next match could start anywhere + + if .Multiline in flags { + return {}, .Unsupported_Flag + } + + result.regex = create(pattern, flags, permanent_allocator, temporary_allocator) or_return + result.capture = preallocate_capture() + result.temp = temporary_allocator + result.vm = virtual_machine.create(result.regex.program, str) + result.vm.class_data = result.regex.class_data + + return +} + /* Match a regular expression against a string and allocate the results into the returned `capture` structure. @@ -387,9 +440,72 @@ match_with_preallocated_capture :: proc( return } +/* +Iterate over a `Match_Iterator` and return successive captures. +Note: Does not handle `.Multiline` properly. + +Inputs: +- it: Pointer to the `Match_Iterator` to iterate over. + +Returns: +- result: `Capture` for this iteration. +- ok: A bool indicating if there was a match, stopping the iteration on `false`. +*/ +match_iterator :: proc(it: ^Match_Iterator) -> (result: Capture, index: int, ok: bool) { + assert(len(it.capture.groups) >= common.MAX_CAPTURE_GROUPS, + "Pre-allocated RegEx capture `groups` must be at least 10 elements long.") + assert(len(it.capture.pos) >= common.MAX_CAPTURE_GROUPS, + "Pre-allocated RegEx capture `pos` must be at least 10 elements long.") + + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + + saved: ^[2 * common.MAX_CAPTURE_GROUPS]int + { + context.allocator = it.temp + if .Unicode in it.regex.flags { + saved, ok = virtual_machine.run(&it.vm, true) + } else { + saved, ok = virtual_machine.run(&it.vm, false) + } + } + + str := string(it.vm.memory) + num_groups: int + + if saved != nil { + n := 0 + + #no_bounds_check for i := 0; i < len(saved); i += 2 { + a, b := saved[i], saved[i + 1] + if a == -1 || b == -1 { + continue + } + + it.capture.groups[n] = str[a:b] + it.capture.pos[n] = {a, b} + n += 1 + } + num_groups = n + } + + defer if ok { + it.idx += 1 + } + + if num_groups > 0 { + result = {it.capture.pos[:num_groups], it.capture.groups[:num_groups]} + } + return result, it.idx, ok +} + match :: proc { match_and_allocate_capture, match_with_preallocated_capture, + match_iterator, +} + +reset :: proc(it: ^Match_Iterator) { + it.idx = 0 } /* @@ -406,7 +522,7 @@ Returns: @require_results preallocate_capture :: proc(allocator := context.allocator) -> (result: Capture) { context.allocator = allocator - result.pos = make([][2]int, common.MAX_CAPTURE_GROUPS) + result.pos = make([][2]int, common.MAX_CAPTURE_GROUPS) result.groups = make([]string, common.MAX_CAPTURE_GROUPS) return } @@ -436,7 +552,7 @@ Free all data allocated by the `match_and_allocate_capture` procedure. *Frees Using Provided Allocator* Inputs: -- capture: A Capture. +- capture: A `Capture`. - allocator: (default: context.allocator) */ destroy_capture :: proc(capture: Capture, allocator := context.allocator) { @@ -445,7 +561,24 @@ destroy_capture :: proc(capture: Capture, allocator := context.allocator) { delete(capture.pos) } +/* +Free all data allocated by the `create_iterator` procedure. + +*Frees Using Provided Allocator* + +Inputs: +- it: A `Match_Iterator` +- allocator: (default: context.allocator) +*/ +destroy_iterator :: proc(it: Match_Iterator, allocator := context.allocator) { + context.allocator = allocator + destroy(it.regex) + destroy(it.capture) + virtual_machine.destroy(it.vm) +} + destroy :: proc { destroy_regex, destroy_capture, + destroy_iterator, } diff --git a/core/text/regex/virtual_machine/virtual_machine.odin b/core/text/regex/virtual_machine/virtual_machine.odin index a4fca6c4d..ab1dfbec1 100644 --- a/core/text/regex/virtual_machine/virtual_machine.odin +++ b/core/text/regex/virtual_machine/virtual_machine.odin @@ -627,8 +627,9 @@ opcode_count :: proc(code: Program) -> (opcodes: int) { return } -create :: proc(code: Program, str: string) -> (vm: Machine) { +create :: proc(code: Program, str: string, allocator := context.allocator) -> (vm: Machine) { assert(len(code) > 0, "RegEx VM has no instructions.") + context.allocator = allocator vm.memory = str vm.code = code @@ -644,3 +645,11 @@ create :: proc(code: Program, str: string) -> (vm: Machine) { return } + +destroy :: proc(vm: Machine, allocator := context.allocator) { + context.allocator = allocator + + delete(vm.busy_map) + free(vm.threads) + free(vm.next_threads) +} \ No newline at end of file diff --git a/core/thread/thread.odin b/core/thread/thread.odin index c1cbceb42..c7fd082ac 100644 --- a/core/thread/thread.odin +++ b/core/thread/thread.odin @@ -264,12 +264,14 @@ flag is specified. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()` in order to free the resources associated with the temporary allocations. */ -create_and_start :: proc(fn: proc(), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> ^Thread { +create_and_start :: proc(fn: proc(), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) { thread_proc :: proc(t: ^Thread) { fn := cast(proc())t.data fn() } - t := create(thread_proc, priority) + if t = create(thread_proc, priority); t == nil { + return + } t.data = rawptr(fn) if self_cleanup { intrinsics.atomic_or(&t.flags, {.Self_Cleanup}) @@ -295,14 +297,16 @@ flag is specified. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()` in order to free the resources associated with the temporary allocations. */ -create_and_start_with_data :: proc(data: rawptr, fn: proc(data: rawptr), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> ^Thread { +create_and_start_with_data :: proc(data: rawptr, fn: proc(data: rawptr), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) { thread_proc :: proc(t: ^Thread) { fn := cast(proc(rawptr))t.data assert(t.user_index >= 1) data := t.user_args[0] fn(data) } - t := create(thread_proc, priority) + if t = create(thread_proc, priority); t == nil { + return + } t.data = rawptr(fn) t.user_index = 1 t.user_args[0] = data @@ -330,7 +334,7 @@ flag is specified. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()` in order to free the resources associated with the temporary allocations. */ -create_and_start_with_poly_data :: proc(data: $T, fn: proc(data: T), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> ^Thread +create_and_start_with_poly_data :: proc(data: $T, fn: proc(data: T), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { thread_proc :: proc(t: ^Thread) { fn := cast(proc(T))t.data @@ -338,7 +342,9 @@ create_and_start_with_poly_data :: proc(data: $T, fn: proc(data: T), init_contex data := (^T)(&t.user_args[0])^ fn(data) } - t := create(thread_proc, priority) + if t = create(thread_proc, priority); t == nil { + return + } t.data = rawptr(fn) t.user_index = 1 @@ -371,7 +377,7 @@ flag is specified. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()` in order to free the resources associated with the temporary allocations. */ -create_and_start_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> ^Thread +create_and_start_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) where size_of(T1) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { thread_proc :: proc(t: ^Thread) { fn := cast(proc(T1, T2))t.data @@ -383,7 +389,9 @@ create_and_start_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), fn(arg1, arg2) } - t := create(thread_proc, priority) + if t = create(thread_proc, priority); t == nil { + return + } t.data = rawptr(fn) t.user_index = 2 @@ -418,7 +426,7 @@ flag is specified. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()` in order to free the resources associated with the temporary allocations. */ -create_and_start_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: proc(arg1: T1, arg2: T2, arg3: T3), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> ^Thread +create_and_start_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: proc(arg1: T1, arg2: T2, arg3: T3), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) where size_of(T1) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { thread_proc :: proc(t: ^Thread) { fn := cast(proc(T1, T2, T3))t.data @@ -431,7 +439,9 @@ create_and_start_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: pr fn(arg1, arg2, arg3) } - t := create(thread_proc, priority) + if t = create(thread_proc, priority); t == nil { + return + } t.data = rawptr(fn) t.user_index = 3 @@ -467,7 +477,7 @@ flag is specified. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()` in order to free the resources associated with the temporary allocations. */ -create_and_start_with_poly_data4 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, arg4: $T4, fn: proc(arg1: T1, arg2: T2, arg3: T3, arg4: T4), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> ^Thread +create_and_start_with_poly_data4 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, arg4: $T4, fn: proc(arg1: T1, arg2: T2, arg3: T3, arg4: T4), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) where size_of(T1) + size_of(T2) + size_of(T3) + size_of(T4) <= size_of(rawptr) * MAX_USER_ARGUMENTS { thread_proc :: proc(t: ^Thread) { fn := cast(proc(T1, T2, T3, T4))t.data @@ -481,7 +491,9 @@ create_and_start_with_poly_data4 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, arg4: fn(arg1, arg2, arg3, arg4) } - t := create(thread_proc, priority) + if t = create(thread_proc, priority); t == nil { + return + } t.data = rawptr(fn) t.user_index = 4 @@ -513,8 +525,10 @@ _select_context_for_thread :: proc(init_context: Maybe(runtime.Context)) -> runt Ensure that the temp allocator is thread-safe when the user provides a specific initial context to use. Without this, the thread will use the same temp allocator state as the parent thread, and thus, bork it up. */ - if ctx.temp_allocator.procedure == runtime.default_temp_allocator_proc { - ctx.temp_allocator.data = &runtime.global_default_temp_allocator_data + when !ODIN_DEFAULT_TO_NIL_ALLOCATOR { + if ctx.temp_allocator.procedure == runtime.default_temp_allocator_proc { + ctx.temp_allocator.data = &runtime.global_default_temp_allocator_data + } } return ctx } @@ -529,4 +543,4 @@ _maybe_destroy_default_temp_allocator :: proc(init_context: Maybe(runtime.Contex if context.temp_allocator.procedure == runtime.default_temp_allocator_proc { runtime.default_temp_allocator_destroy(auto_cast context.temp_allocator.data) } -} +} \ No newline at end of file diff --git a/core/time/perf.odin b/core/time/perf.odin index 784d7acd6..265a20edf 100644 --- a/core/time/perf.odin +++ b/core/time/perf.odin @@ -17,6 +17,13 @@ tick_now :: proc "contextless" () -> Tick { return _tick_now() } +/* +Add duration to a tick. +*/ +tick_add :: proc "contextless" (t: Tick, d: Duration) -> Tick { + return Tick{t._nsec + i64(d)} +} + /* Obtain the difference between ticks. */ diff --git a/core/time/timezone/tzdate.odin b/core/time/timezone/tzdate.odin index e62400889..8e900ec11 100644 --- a/core/time/timezone/tzdate.odin +++ b/core/time/timezone/tzdate.odin @@ -224,7 +224,7 @@ datetime_to_tz :: proc(dt: datetime.DateTime, tz: ^datetime.TZ_Region) -> (out: record := region_get_nearest(tz, tm) or_return secs := time.time_to_unix(tm) - adj_time := time.unix(secs + record.utc_offset, 0) + adj_time := time.unix(secs + record.utc_offset, i64(dt.nano)) adj_dt := time.time_to_datetime(adj_time) or_return adj_dt.tz = tz diff --git a/examples/all/all_linux.odin b/examples/all/all_linux.odin index ca51d6562..dde712b8d 100644 --- a/examples/all/all_linux.odin +++ b/examples/all/all_linux.odin @@ -3,4 +3,8 @@ package all import linux "core:sys/linux" -_ :: linux \ No newline at end of file +import xlib "vendor:x11/xlib" + +_ :: linux + +_ :: xlib diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index 0e7648f96..de037f6cd 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -58,7 +58,6 @@ import trace "core:debug/trace" import dynlib "core:dynlib" import net "core:net" -import ansi "core:encoding/ansi" import base32 "core:encoding/base32" import base64 "core:encoding/base64" import cbor "core:encoding/cbor" @@ -118,6 +117,7 @@ import relative "core:relative" import reflect "core:reflect" import runtime "base:runtime" +import sanitizer "base:sanitizer" import simd "core:simd" import x86 "core:simd/x86" import slice "core:slice" @@ -128,6 +128,9 @@ import strings "core:strings" import sync "core:sync" import testing "core:testing" +import terminal "core:terminal" +import ansi "core:terminal/ansi" + import edit "core:text/edit" import i18n "core:text/i18n" import match "core:text/match" @@ -200,7 +203,6 @@ _ :: pe _ :: trace _ :: dynlib _ :: net -_ :: ansi _ :: base32 _ :: base64 _ :: csv @@ -256,6 +258,8 @@ _ :: strconv _ :: strings _ :: sync _ :: testing +_ :: terminal +_ :: ansi _ :: scanner _ :: i18n _ :: match @@ -275,3 +279,4 @@ _ :: uuid_legacy _ :: utf8 _ :: utf8string _ :: utf16 +_ :: sanitizer diff --git a/examples/all/all_vendor.odin b/examples/all/all_vendor.odin index b224a3bbe..ebbfe786b 100644 --- a/examples/all/all_vendor.odin +++ b/examples/all/all_vendor.odin @@ -28,8 +28,6 @@ import nvg "vendor:nanovg" import nvg_gl "vendor:nanovg/gl" import fontstash "vendor:fontstash" -import xlib "vendor:x11/xlib" - _ :: cgltf // _ :: commonmark _ :: ENet @@ -57,8 +55,6 @@ _ :: nvg _ :: nvg_gl _ :: fontstash -_ :: xlib - // NOTE: needed for doc generator diff --git a/examples/all/all_vendor_windows.odin b/examples/all/all_vendor_windows.odin index 5087bac07..df6542cdd 100644 --- a/examples/all/all_vendor_windows.odin +++ b/examples/all/all_vendor_windows.odin @@ -3,8 +3,10 @@ package all import wgpu "vendor:wgpu" import b2 "vendor:box2d" import game_input "vendor:windows/GameInput" +import XAudio2 "vendor:windows/XAudio2" _ :: wgpu _ :: b2 _ :: game_input +_ :: XAudio2 diff --git a/misc/get-date.c b/misc/get-date.c index bf5b32738..b3eb1be78 100644 --- a/misc/get-date.c +++ b/misc/get-date.c @@ -9,5 +9,5 @@ int main(int arg_count, char const **arg_ptr) { time_t t = time(NULL); struct tm* now = localtime(&t); - printf("%04d%02d%02d", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday); + printf("%04d-%02d-%02d", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday); } \ No newline at end of file diff --git a/src/bug_report.cpp b/src/bug_report.cpp index c44c4be33..32210c23e 100644 --- a/src/bug_report.cpp +++ b/src/bug_report.cpp @@ -527,7 +527,7 @@ gb_internal void report_os_info() { #elif defined(GB_SYSTEM_OSX) gbString sw_vers = gb_string_make(heap_allocator(), ""); - if (!system_exec_command_line_app_output("sw_vers --productVersion", &sw_vers)) { + if (!system_exec_command_line_app_output("sw_vers -productVersion", &sw_vers)) { gb_printf("macOS Unknown\n"); return; } @@ -667,8 +667,14 @@ gb_internal void print_bug_report_help() { gb_printf("-nightly"); #endif + String version = {}; + #ifdef GIT_SHA - gb_printf(":%s", GIT_SHA); + version.text = cast(u8 *)GIT_SHA; + version.len = gb_strlen(GIT_SHA); + if (version != "") { + gb_printf(":%.*s", LIT(version)); + } #endif gb_printf("\n"); diff --git a/src/build_settings.cpp b/src/build_settings.cpp index 1f5aba254..b3bbf726b 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -459,6 +459,7 @@ struct BuildContext { bool ignore_unknown_attributes; bool no_bounds_check; bool no_type_assert; + bool dynamic_literals; // Opt-in to `#+feature dynamic-literals` project-wide. bool no_output_files; bool no_crt; bool no_rpath; @@ -551,10 +552,9 @@ struct BuildContext { String ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL; String ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT; - String ODIN_ANDROID_JAR_SIGNER; String android_keystore; String android_keystore_alias; - String android_manifest; + String android_keystore_password; }; gb_global BuildContext build_context = {0}; @@ -848,13 +848,39 @@ gb_global NamedTargetMetrics *selected_target_metrics; gb_global Subtarget selected_subtarget; -gb_internal TargetOsKind get_target_os_from_string(String str) { +gb_internal TargetOsKind get_target_os_from_string(String str, Subtarget *subtarget_ = nullptr) { + String os_name = str; + String subtarget = {}; + auto part = string_partition(str, str_lit(":")); + if (part.match.len == 1) { + os_name = part.head; + subtarget = part.tail; + } + + TargetOsKind kind = TargetOs_Invalid; + for (isize i = 0; i < TargetOs_COUNT; i++) { - if (str_eq_ignore_case(target_os_names[i], str)) { - return cast(TargetOsKind)i; + if (str_eq_ignore_case(target_os_names[i], os_name)) { + kind = cast(TargetOsKind)i; + break; } } - return TargetOs_Invalid; + if (subtarget_) *subtarget_ = Subtarget_Default; + + if (subtarget.len != 0) { + if (str_eq_ignore_case(subtarget, "generic") || str_eq_ignore_case(subtarget, "default")) { + if (subtarget_) *subtarget_ = Subtarget_Default; + } else { + for (isize i = 1; i < Subtarget_COUNT; i++) { + if (str_eq_ignore_case(subtarget_strings[i], subtarget)) { + if (subtarget_) *subtarget_ = cast(Subtarget)i; + break; + } + } + } + } + + return kind; } gb_internal TargetArchKind get_target_arch_from_string(String str) { @@ -1573,24 +1599,15 @@ gb_internal void init_android_values(bool with_sdk) { bc->ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT = concatenate_strings(permanent_allocator(), bc->ODIN_ANDROID_NDK_TOOLCHAIN, str_lit("sysroot/")); - bc->ODIN_ANDROID_JAR_SIGNER = normalize_path(permanent_allocator(), make_string_c(gb_get_env("ODIN_ANDROID_JAR_SIGNER", permanent_allocator())), NIX_SEPARATOR_STRING); if (with_sdk) { if (bc->ODIN_ANDROID_SDK.len == 0) { gb_printf_err("Error: ODIN_ANDROID_SDK not set, which is required for -build-mode:executable for -subtarget:android"); gb_exit(1); } - if (bc->ODIN_ANDROID_JAR_SIGNER.len == 0) { - gb_printf_err("Error: ODIN_ANDROID_JAR_SIGNER not set, which is required for -build-mode:executable for -subtarget:android"); - gb_exit(1); - } if (bc->android_keystore.len == 0) { gb_printf_err("Error: -android-keystore: has not been set\n"); gb_exit(1); } - if (bc->android_keystore_alias.len == 0) { - gb_printf_err("Error: -android-keystore_alias: has not been set\n"); - gb_exit(1); - } } } @@ -1774,6 +1791,30 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta bc->ODIN_WINDOWS_SUBSYSTEM = windows_subsystem_names[Windows_Subsystem_CONSOLE]; } + if (subtarget == Subtarget_Android) { + switch (build_context.build_mode) { + case BuildMode_DynamicLibrary: + case BuildMode_Object: + case BuildMode_Assembly: + case BuildMode_LLVM_IR: + break; + default: + case BuildMode_Executable: + case BuildMode_StaticLibrary: + if ((build_context.command_kind & Command__does_build) != 0) { + gb_printf_err("Unsupported -build-mode for -subtarget:android\n"); + gb_printf_err("\tCurrently only supporting: \n"); + // gb_printf_err("\t\texe\n"); + gb_printf_err("\t\tshared\n"); + gb_printf_err("\t\tobject\n"); + gb_printf_err("\t\tassembly\n"); + gb_printf_err("\t\tllvm-ir\n"); + gb_exit(1); + } + break; + } + } + if (metrics->os == TargetOs_darwin && subtarget == Subtarget_iOS) { switch (metrics->arch) { case TargetArch_arm64: @@ -1849,7 +1890,7 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta bc->metrics.target_triplet = concatenate_strings(permanent_allocator(), bc->metrics.target_triplet, bc->minimum_os_version_string); } } else if (selected_subtarget == Subtarget_Android) { - init_android_values(bc->build_mode == BuildMode_Executable); + init_android_values(bc->build_mode == BuildMode_Executable && (bc->command_kind & Command__does_build) != 0); } if (!bc->custom_optimization_level) { @@ -1875,12 +1916,6 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta } - // TODO: Static map calls are bugged on `amd64sysv` abi. - if (bc->metrics.os != TargetOs_windows && bc->metrics.arch == TargetArch_amd64) { - // ENFORCE DYNAMIC MAP CALLS - bc->dynamic_map_calls = true; - } - bc->ODIN_VALGRIND_SUPPORT = false; if (build_context.metrics.os != TargetOs_windows) { switch (bc->metrics.arch) { @@ -1893,30 +1928,6 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta if (bc->metrics.os == TargetOs_freestanding) { bc->ODIN_DEFAULT_TO_NIL_ALLOCATOR = !bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR; } - - if (subtarget == Subtarget_Android) { - switch (build_context.build_mode) { - case BuildMode_DynamicLibrary: - case BuildMode_Object: - case BuildMode_Assembly: - case BuildMode_LLVM_IR: - break; - default: - case BuildMode_Executable: - case BuildMode_StaticLibrary: - if ((build_context.command_kind & Command__does_build) != 0) { - gb_printf_err("Unsupported -build-mode for -subtarget:android\n"); - gb_printf_err("\tCurrently only supporting: \n"); - // gb_printf_err("\t\texe\n"); - gb_printf_err("\t\tshared\n"); - gb_printf_err("\t\tobject\n"); - gb_printf_err("\t\tassembly\n"); - gb_printf_err("\t\tllvm-ir\n"); - gb_exit(1); - } - break; - } - } } #if defined(GB_SYSTEM_WINDOWS) @@ -2198,11 +2209,34 @@ gb_internal bool init_build_paths(String init_filename) { while (output_name.len > 0 && (output_name[output_name.len-1] == '/' || output_name[output_name.len-1] == '\\')) { output_name.len -= 1; } + // Only trim the extension if it's an Odin source file. + // This lets people build folders with extensions or files beginning with dots. + if (path_extension(output_name) == ".odin" && !path_is_directory(output_name)) { + output_name = remove_extension_from_path(output_name); + } output_name = remove_directory_from_path(output_name); - output_name = remove_extension_from_path(output_name); output_name = copy_string(ha, string_trim_whitespace(output_name)); - output_path = path_from_string(ha, output_name); - + // This is `path_from_string` without the extension trimming. + Path res = {}; + if (output_name.len > 0) { + String fullpath = path_to_full_path(ha, output_name); + defer (gb_free(ha, fullpath.text)); + + res.basename = directory_from_path(fullpath); + res.basename = copy_string(ha, res.basename); + + if (path_is_directory(fullpath)) { + if (res.basename.len > 0 && res.basename.text[res.basename.len - 1] == '/') { + res.basename.len--; + } + } else { + isize name_start = (res.basename.len > 0) ? res.basename.len + 1 : res.basename.len; + res.name = substring(fullpath, name_start, fullpath.len); + res.name = copy_string(ha, res.name); + } + } + output_path = res; + // Note(Dragos): This is a fix for empty filenames // Turn the trailing folder into the file name if (output_path.name.len == 0) { diff --git a/src/bundle_command.cpp b/src/bundle_command.cpp index b3bca2b51..cd0cd589f 100644 --- a/src/bundle_command.cpp +++ b/src/bundle_command.cpp @@ -126,16 +126,6 @@ i32 bundle_android(String original_init_directory) { defer (gb_string_free(cmd)); - String current_directory = normalize_path(temporary_allocator(), get_working_directory(temporary_allocator()), NIX_SEPARATOR_STRING); - defer (set_working_directory(current_directory)); - - if (current_directory.len != 0) { - bool ok = set_working_directory(init_directory); - if (!ok) { - gb_printf_err("Error: Unable to currectly set the current working directory to '%.*s'\n", LIT(init_directory)); - } - } - String output_filename = str_lit("test"); String output_apk = path_remove_extension(output_filename); @@ -144,51 +134,36 @@ i32 bundle_android(String original_init_directory) { TEMPORARY_ALLOCATOR_GUARD(); gb_string_clear(cmd); - String manifest = {}; - if (build_context.android_manifest.len != 0) { - manifest = concatenate_strings(temporary_allocator(), current_directory, build_context.android_manifest); - } else { - manifest = concatenate_strings(temporary_allocator(), init_directory, str_lit("AndroidManifest.xml")); - } + String manifest = concatenate_strings(temporary_allocator(), init_directory, str_lit("AndroidManifest.xml")); cmd = gb_string_append_length(cmd, android_sdk_build_tools.text, android_sdk_build_tools.len); cmd = gb_string_appendc(cmd, "aapt"); cmd = gb_string_appendc(cmd, " package -f"); - if (manifest.len != 0) { - cmd = gb_string_append_fmt(cmd, " -M \"%.*s\"", LIT(manifest)); - } + cmd = gb_string_append_fmt(cmd, " -M \"%.*s\"", LIT(manifest)); cmd = gb_string_append_fmt(cmd, " -I \"%.*sandroid.jar\"", LIT(android_sdk_platforms)); cmd = gb_string_append_fmt(cmd, " -F \"%.*s.apk-build\"", LIT(output_apk)); + String resources_dir = concatenate_strings(temporary_allocator(), init_directory, str_lit("res")); + if (gb_file_exists((const char *)resources_dir.text)) { + cmd = gb_string_append_fmt(cmd, " -S \"%.*s\"", LIT(resources_dir)); + } + + String assets_dir = concatenate_strings(temporary_allocator(), init_directory, str_lit("assets")); + if (gb_file_exists((const char *)assets_dir.text)) { + cmd = gb_string_append_fmt(cmd, " -A \"%.*s\"", LIT(assets_dir)); + } + + String lib_dir = concatenate_strings(temporary_allocator(), init_directory, str_lit("lib")); + if (gb_file_exists((const char *)lib_dir.text)) { + cmd = gb_string_append_fmt(cmd, " \"%.*s\"", LIT(lib_dir)); + } + result = system_exec_command_line_app("android-aapt", cmd); if (result) { return result; } } - TIME_SECTION("Android jarsigner"); - { - TEMPORARY_ALLOCATOR_GUARD(); - gb_string_clear(cmd); - - cmd = gb_string_append_length(cmd, build_context.ODIN_ANDROID_JAR_SIGNER.text, build_context.ODIN_ANDROID_JAR_SIGNER.len); - cmd = gb_string_append_fmt(cmd, " -storepass android"); - if (build_context.android_keystore.len != 0) { - String keystore = concatenate_strings(temporary_allocator(), current_directory, build_context.android_keystore); - cmd = gb_string_append_fmt(cmd, " -keystore \"%.*s\"", LIT(keystore)); - } - cmd = gb_string_append_fmt(cmd, " \"%.*s.apk-build\"", LIT(output_apk)); - if (build_context.android_keystore_alias.len != 0) { - String keystore_alias = build_context.android_keystore_alias; - cmd = gb_string_append_fmt(cmd, " \"%.*s\"", LIT(keystore_alias)); - } - - result = system_exec_command_line_app("android-jarsigner", cmd); - if (result) { - return result; - } - } - TIME_SECTION("Android zipalign"); { TEMPORARY_ALLOCATOR_GUARD(); @@ -205,5 +180,33 @@ i32 bundle_android(String original_init_directory) { } } + TIME_SECTION("Android apksigner"); + { + TEMPORARY_ALLOCATOR_GUARD(); + gb_string_clear(cmd); + + cmd = gb_string_append_length(cmd, android_sdk_build_tools.text, android_sdk_build_tools.len); + cmd = gb_string_appendc(cmd, "apksigner"); + cmd = gb_string_appendc(cmd, " sign"); + + String keystore = normalize_path(temporary_allocator(), build_context.android_keystore, NIX_SEPARATOR_STRING); + keystore = substring(keystore, 0, keystore.len - 1); + cmd = gb_string_append_fmt(cmd, " --ks \"%.*s\"", LIT(keystore)); + + if (build_context.android_keystore_alias.len != 0) { + cmd = gb_string_append_fmt(cmd, " --ks-key-alias \"%.*s\"", LIT(build_context.android_keystore_alias)); + } + if (build_context.android_keystore_password.len != 0) { + cmd = gb_string_append_fmt(cmd, " --ks-pass pass:\"%.*s\"", LIT(build_context.android_keystore_password)); + } + + cmd = gb_string_append_fmt(cmd, " \"%.*s.apk\"", LIT(output_apk)); + + result = system_exec_command_line_app("android-apksigner", cmd); + if (result) { + return result; + } + } + return 0; } diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 05f0ac7d7..71906d9c6 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -223,9 +223,9 @@ gb_internal void add_objc_proc_type(CheckerContext *c, Ast *call, Type *return_t data.kind = kind; data.proc_type = alloc_type_proc(scope, params, param_types.count, results, results->Tuple.variables.count, false, ProcCC_CDecl); - mutex_lock(&c->info->objc_types_mutex); + mutex_lock(&c->info->objc_objc_msgSend_mutex); map_set(&c->info->objc_msgSend_types, call, data); - mutex_unlock(&c->info->objc_types_mutex); + mutex_unlock(&c->info->objc_objc_msgSend_mutex); try_to_add_package_dependency(c, "runtime", "objc_msgSend"); try_to_add_package_dependency(c, "runtime", "objc_msgSend_fpret"); @@ -387,6 +387,59 @@ gb_internal bool check_builtin_objc_procedure(CheckerContext *c, Operand *operan try_to_add_package_dependency(c, "runtime", "objc_allocateClassPair"); return true; } break; + + case BuiltinProc_objc_ivar_get: + { + Type *self_type = nullptr; + + Operand self = {}; + check_expr_or_type(c, &self, ce->args[0]); + + if (!is_operand_value(self) || !check_is_assignable_to(c, &self, t_objc_id)) { + gbString e = expr_to_string(self.expr); + gbString t = type_to_string(self.type); + error(self.expr, "'%.*s' expected a type or value derived from intrinsics.objc_object, got '%s' of type %s", LIT(builtin_name), e, t); + gb_string_free(t); + gb_string_free(e); + return false; + } else if (!is_type_pointer(self.type)) { + gbString e = expr_to_string(self.expr); + gbString t = type_to_string(self.type); + error(self.expr, "'%.*s' expected a pointer of a value derived from intrinsics.objc_object, got '%s' of type %s", LIT(builtin_name), e, t); + gb_string_free(t); + gb_string_free(e); + return false; + } + + self_type = type_deref(self.type); + + if (!(self_type->kind == Type_Named && + self_type->Named.type_name != nullptr && + self_type->Named.type_name->TypeName.objc_class_name != "")) { + gbString t = type_to_string(self_type); + error(self.expr, "'%.*s' expected a named type with the attribute @(objc_class=) , got type %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + Type *ivar_type = self_type->Named.type_name->TypeName.objc_ivar; + if (ivar_type == nullptr) { + gbString t = type_to_string(self_type); + error(self.expr, "'%.*s' requires that type %s have the attribute @(objc_ivar=).", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + if (type_hint != nullptr && type_hint->kind == Type_Pointer && type_hint->Pointer.elem == ivar_type) { + operand->type = type_hint; + } else { + operand->type = alloc_type_pointer(ivar_type); + } + + operand->mode = Addressing_Value; + return true; + + } break; } } @@ -760,6 +813,36 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan return true; } + case BuiltinProc_simd_indices: + { + Operand x = {}; + check_expr_or_type(c, &x, ce->args[0], nullptr); + if (x.mode == Addressing_Invalid) return false; + if (x.mode != Addressing_Type) { + gbString s = expr_to_string(x.expr); + error(x.expr, "'%.*s' expected a simd vector type, got '%s'", LIT(builtin_name), s); + gb_string_free(s); + return false; + } + if (!is_type_simd_vector(x.type)) { + gbString s = type_to_string(x.type); + error(x.expr, "'%.*s' expected a simd vector type, got '%s'", LIT(builtin_name), s); + gb_string_free(s); + return false; + } + + Type *elem = base_array_type(x.type); + if (!is_type_numeric(elem)) { + gbString s = type_to_string(x.type); + error(x.expr, "'%.*s' expected a simd vector type with a numeric element type, got '%s'", LIT(builtin_name), s); + gb_string_free(s); + } + + operand->mode = Addressing_Value; + operand->type = x.type; + return true; + } + case BuiltinProc_simd_extract: { Operand x = {}; @@ -823,8 +906,12 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan } break; + case BuiltinProc_simd_reduce_add_bisect: + case BuiltinProc_simd_reduce_mul_bisect: case BuiltinProc_simd_reduce_add_ordered: case BuiltinProc_simd_reduce_mul_ordered: + case BuiltinProc_simd_reduce_add_pairs: + case BuiltinProc_simd_reduce_mul_pairs: case BuiltinProc_simd_reduce_min: case BuiltinProc_simd_reduce_max: { @@ -2073,6 +2160,7 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As case BuiltinProc_atomic_type_is_lock_free: case BuiltinProc_has_target_feature: case BuiltinProc_procedure_of: + case BuiltinProc_simd_indices: // NOTE(bill): The first arg may be a Type, this will be checked case by case break; @@ -2146,7 +2234,8 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As case BuiltinProc_objc_find_selector: case BuiltinProc_objc_find_class: case BuiltinProc_objc_register_selector: - case BuiltinProc_objc_register_class: + case BuiltinProc_objc_register_class: + case BuiltinProc_objc_ivar_get: return check_builtin_objc_procedure(c, operand, call, id, type_hint); case BuiltinProc___entry_point: @@ -3168,6 +3257,194 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As break; } + case BuiltinProc_compress_values: { + Operand *ops = gb_alloc_array(temporary_allocator(), Operand, ce->args.count); + + isize value_count = 0; + + for_array(i, ce->args) { + Ast *arg = ce->args[i]; + Operand *op = &ops[i]; + check_multi_expr(c, op, arg); + if (op->mode == Addressing_Invalid) { + return false; + } + + if (op->type == nullptr || op->type == t_invalid) { + gbString s = expr_to_string(op->expr); + error(op->expr, "Invalid expression to '%.*s', got %s", LIT(builtin_name), s); + gb_string_free(s); + } + if (is_type_tuple(op->type)) { + value_count += op->type->Tuple.variables.count; + } else { + value_count += 1; + } + } + + GB_ASSERT(value_count >= 1); + + if (value_count == 1) { + *operand = ops[0]; + break; + } + + if (type_hint != nullptr) { + Type *th = base_type(type_hint); + if (th->kind == Type_Struct) { + if (value_count == th->Struct.fields.count) { + isize index = 0; + for_array(i, ce->args) { + Operand *op = &ops[i]; + if (is_type_tuple(op->type)) { + for (Entity *v : op->type->Tuple.variables) { + Operand x = {}; + x.mode = Addressing_Value; + x.type = v->type; + check_assignment(c, &x, th->Struct.fields[index++]->type, builtin_name); + if (x.mode == Addressing_Invalid) { + return false; + } + } + } else { + check_assignment(c, op, th->Struct.fields[index++]->type, builtin_name); + if (op->mode == Addressing_Invalid) { + return false; + } + } + } + + operand->type = type_hint; + operand->mode = Addressing_Value; + break; + } + } else if (is_type_array_like(th)) { + if (cast(i64)value_count == get_array_type_count(th)) { + Type *elem = base_array_type(th); + for_array(i, ce->args) { + Operand *op = &ops[i]; + if (is_type_tuple(op->type)) { + for (Entity *v : op->type->Tuple.variables) { + Operand x = {}; + x.mode = Addressing_Value; + x.type = v->type; + check_assignment(c, &x, elem, builtin_name); + if (x.mode == Addressing_Invalid) { + return false; + } + } + } else { + check_assignment(c, op, elem, builtin_name); + if (op->mode == Addressing_Invalid) { + return false; + } + } + } + + operand->type = type_hint; + operand->mode = Addressing_Value; + break; + } + } + } + + bool all_types_the_same = true; + Type *last_type = nullptr; + for_array(i, ce->args) { + Operand *op = &ops[i]; + if (is_type_tuple(op->type)) { + if (last_type == nullptr) { + op->type->Tuple.variables[0]->type; + } + for (Entity *v : op->type->Tuple.variables) { + if (!are_types_identical(last_type, v->type)) { + all_types_the_same = false; + break; + } + last_type = v->type; + } + } else { + if (last_type == nullptr) { + last_type = op->type; + } else { + if (!are_types_identical(last_type, op->type)) { + all_types_the_same = false; + break; + } + last_type = op->type; + } + } + } + + if (all_types_the_same) { + Type *elem_type = default_type(last_type); + if (is_type_untyped(elem_type)) { + gbString s = expr_to_string(call); + error(call, "Invalid use of '%s' in '%.*s'", s, LIT(builtin_name)); + gb_string_free(s); + return false; + } + + operand->type = alloc_type_array(elem_type, value_count); + operand->mode = Addressing_Value; + } else { + Type *st = alloc_type_struct_complete(); + st->Struct.fields = slice_make(permanent_allocator(), value_count); + st->Struct.tags = gb_alloc_array(permanent_allocator(), String, value_count); + st->Struct.offsets = gb_alloc_array(permanent_allocator(), i64, value_count); + + Scope *scope = create_scope(c->info, nullptr); + + Token token = {}; + token.kind = Token_Ident; + token.pos = ast_token(call).pos; + + isize index = 0; + for_array(i, ce->args) { + Operand *op = &ops[i]; + if (is_type_tuple(op->type)) { + for (Entity *v : op->type->Tuple.variables) { + Type *t = default_type(v->type); + if (is_type_untyped(t)) { + gbString s = expr_to_string(op->expr); + error(op->expr, "Invalid use of '%s' in '%.*s'", s, LIT(builtin_name)); + gb_string_free(s); + return false; + } + + gbString s = gb_string_make_reserve(permanent_allocator(), 32); + s = gb_string_append_fmt(s, "v%lld", cast(long long)index); + token.string = make_string_c(s); + Entity *e = alloc_entity_field(scope, token, t, false, cast(i32)index, EntityState_Resolved); + st->Struct.fields[index++] = e; + } + } else { + Type *t = default_type(op->type); + if (is_type_untyped(t)) { + gbString s = expr_to_string(op->expr); + error(op->expr, "Invalid use of '%s' in '%.*s'", s, LIT(builtin_name)); + gb_string_free(s); + return false; + } + + gbString s = gb_string_make_reserve(permanent_allocator(), 32); + s = gb_string_append_fmt(s, "v%lld", cast(long long)index); + token.string = make_string_c(s); + Entity *e = alloc_entity_field(scope, token, t, false, cast(i32)index, EntityState_Resolved); + st->Struct.fields[index++] = e; + } + } + + + gb_unused(type_size_of(st)); + + operand->type = st; + operand->mode = Addressing_Value; + } + break; + } + + case BuiltinProc_min: { // min :: proc($T: typeid) -> ordered // min :: proc(a: ..ordered) -> ordered @@ -5564,6 +5841,7 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As case Type_EnumeratedArray: operand->type = bt->EnumeratedArray.elem; break; case Type_Slice: operand->type = bt->Slice.elem; break; case Type_DynamicArray: operand->type = bt->DynamicArray.elem; break; + case Type_SimdVector: operand->type = bt->SimdVector.elem; break; } } operand->mode = Addressing_Type; @@ -5613,6 +5891,87 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As } operand->mode = Addressing_Type; break; + case BuiltinProc_type_integer_to_unsigned: + if (operand->mode != Addressing_Type) { + error(operand->expr, "Expected a type for '%.*s'", LIT(builtin_name)); + return false; + } + + if (is_type_polymorphic(operand->type)) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Expected a non-polymorphic type for '%.*s', got %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + { + Type *bt = base_type(operand->type); + + if (bt->kind != Type_Basic || + (bt->Basic.flags & BasicFlag_Unsigned) != 0 || + (bt->Basic.flags & BasicFlag_Integer) == 0) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Expected a signed integer type for '%.*s', got %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + if ((bt->Basic.flags & BasicFlag_Untyped) != 0) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Expected a non-untyped integer type for '%.*s', got %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + Type *u_type = &basic_types[bt->Basic.kind + 1]; + + operand->type = u_type; + } + break; + case BuiltinProc_type_integer_to_signed: + if (operand->mode != Addressing_Type) { + error(operand->expr, "Expected a type for '%.*s'", LIT(builtin_name)); + return false; + } + + if (is_type_polymorphic(operand->type)) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Expected a non-polymorphic type for '%.*s', got %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + { + Type *bt = base_type(operand->type); + + if (bt->kind != Type_Basic || + (bt->Basic.flags & BasicFlag_Unsigned) == 0 || + (bt->Basic.flags & BasicFlag_Integer) == 0) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Expected an unsigned integer type for '%.*s', got %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + if ((bt->Basic.flags & BasicFlag_Untyped) != 0) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Expected a non-untyped integer type for '%.*s', got %s", LIT(builtin_name), t); + gb_string_free(t); + return false; + } + + if (bt->Basic.kind == Basic_uintptr) { + gbString t = type_to_string(operand->type); + error(operand->expr, "Type %s does not have a signed integer mapping for '%.*s'", t, LIT(builtin_name)); + gb_string_free(t); + return false; + } + + Type *u_type = &basic_types[bt->Basic.kind - 1]; + + operand->type = u_type; + } + break; case BuiltinProc_type_merge: { operand->mode = Addressing_Type; @@ -6014,12 +6373,13 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As // NOTE(jakubtomsu): forces calculation of variant_block_size type_size_of(u); - i64 tag_offset = u->Union.variant_block_size; - GB_ASSERT(tag_offset > 0); + // NOTE(Jeroen): A tag offset of zero is perfectly fine if all members of the union are empty structs. + // What matters is that the tag size is > 0. + GB_ASSERT(u->Union.tag_size > 0); operand->mode = Addressing_Constant; operand->type = t_untyped_integer; - operand->value = exact_value_i64(tag_offset); + operand->value = exact_value_i64(u->Union.variant_block_size); } break; diff --git a/src/check_decl.cpp b/src/check_decl.cpp index 250e8b854..d53c3c6b7 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -468,6 +468,10 @@ gb_internal void check_type_decl(CheckerContext *ctx, Entity *e, Ast *init_expr, } e->type = named; + if (!is_distinct) { + e->TypeName.is_type_alias = true; + } + check_type_path_push(ctx, e); Type *bt = check_type_expr(ctx, te, named); check_type_path_pop(ctx); @@ -502,9 +506,9 @@ gb_internal void check_type_decl(CheckerContext *ctx, Entity *e, Ast *init_expr, if (!is_distinct) { e->type = bt; named->Named.base = bt; - e->TypeName.is_type_alias = true; } + e->TypeName.is_type_alias = !is_distinct; if (decl->type_expr != nullptr) { Type *t = check_type(ctx, decl->type_expr); @@ -520,12 +524,90 @@ gb_internal void check_type_decl(CheckerContext *ctx, Entity *e, Ast *init_expr, if (decl != nullptr) { AttributeContext ac = {}; check_decl_attributes(ctx, decl->attributes, type_decl_attribute, &ac); + if (e->kind == Entity_TypeName && ac.objc_class != "") { + e->TypeName.objc_class_name = ac.objc_class; + if (ac.objc_is_implementation) { + e->TypeName.objc_is_implementation = ac.objc_is_implementation; + e->TypeName.objc_superclass = ac.objc_superclass; + e->TypeName.objc_ivar = ac.objc_ivar; + e->TypeName.objc_context_provider = ac.objc_context_provider; + + mutex_lock(&ctx->info->objc_class_name_mutex); + bool class_exists = string_set_update(&ctx->info->obcj_class_name_set, ac.objc_class); + mutex_unlock(&ctx->info->objc_class_name_mutex); + if (class_exists) { + error(e->token, "@(objc_class) name '%.*s' has already been used elsewhere", LIT(ac.objc_class)); + } + + mpsc_enqueue(&ctx->info->objc_class_implementations, e); + + GB_ASSERT(e->TypeName.objc_ivar == nullptr || e->TypeName.objc_ivar->kind == Type_Named); + + // Enqueue the contex_provider proc to be checked after it is resolved + if (e->TypeName.objc_context_provider != nullptr) { + mpsc_enqueue(&ctx->checker->procs_with_objc_context_provider_to_check, e); + } + + // TODO(harold): I think there's a Check elsewhere in the checker for checking cycles. + // See about moving this to the right location. + // Ensure superclass hierarchy are all Objective-C classes and does not cycle + + // NOTE(harold): We check for superclass unconditionally (before checking if super is null) + // because this should be the case 99.99% of the time. Not subclassing something that + // is, or is the child of, NSObject means the objc runtime messaging will not properly work on this type. + TypeSet super_set{}; + type_set_init(&super_set, 8); + defer (type_set_destroy(&super_set)); + + type_set_update(&super_set, e->type); + + Type *super = ac.objc_superclass; + while (super != nullptr) { + if (type_set_update(&super_set, super)) { + error(e->token, "@(objc_superclass) Superclass hierarchy cycle encountered"); + break; + } + + check_single_global_entity(ctx->checker, super->Named.type_name, super->Named.type_name->decl_info); + + if (super->kind != Type_Named) { + error(e->token, "@(objc_superclass) Referenced type must be a named struct"); + break; + } + + Type* named_type = base_named_type(super); + GB_ASSERT(named_type->kind == Type_Named); + + if (!is_type_objc_object(named_type)) { + error(e->token, "@(objc_superclass) Superclass '%.*s' must be an Objective-C class", LIT(named_type->Named.name)); + break; + } + + if (named_type->Named.type_name->TypeName.objc_class_name == "") { + error(e->token, "@(objc_superclass) Superclass '%.*s' must have a valid @(objc_class) attribute", LIT(named_type->Named.name)); + break; + } + + super = named_type->Named.type_name->TypeName.objc_superclass; + } + } else { + if (ac.objc_superclass != nullptr) { + error(e->token, "@(objc_superclass) may only be applied when the @(obj_implement) attribute is also applied"); + } else if (ac.objc_ivar != nullptr) { + error(e->token, "@(objc_ivar) may only be applied when the @(obj_implement) attribute is also applied"); + } else if (ac.objc_context_provider != nullptr) { + error(e->token, "@(objc_context_provider) may only be applied when the @(obj_implement) attribute is also applied"); + } + } + if (type_size_of(e->type) > 0) { error(e->token, "@(objc_class) marked type must be of zero size"); } + } else if (ac.objc_is_implementation) { + error(e->token, "@(objc_implement) may only be applied when the @(objc_class) attribute is also applied"); } } @@ -918,7 +1000,7 @@ gb_internal String handle_link_name(CheckerContext *ctx, Token token, String lin } -gb_internal void check_objc_methods(CheckerContext *ctx, Entity *e, AttributeContext const &ac) { +gb_internal void check_objc_methods(CheckerContext *ctx, Entity *e, AttributeContext &ac) { if (!(ac.objc_name.len || ac.objc_is_class_method || ac.objc_type)) { return; } @@ -930,48 +1012,107 @@ gb_internal void check_objc_methods(CheckerContext *ctx, Entity *e, AttributeCon error(e->token, "@(objc_name) is required with @(objc_type)"); } else { Type *t = ac.objc_type; - if (t->kind == Type_Named) { - Entity *tn = t->Named.type_name; - GB_ASSERT(tn->kind == Entity_TypeName); + GB_ASSERT(t->kind == Type_Named); // NOTE(harold): This is already checked for at the attribute resolution stage. + Entity *tn = t->Named.type_name; - if (tn->scope != e->scope) { - error(e->token, "@(objc_name) attribute may only be applied to procedures and types within the same scope"); - } else { - mutex_lock(&global_type_name_objc_metadata_mutex); - defer (mutex_unlock(&global_type_name_objc_metadata_mutex)); + GB_ASSERT(tn->kind == Entity_TypeName); - if (!tn->TypeName.objc_metadata) { - tn->TypeName.objc_metadata = create_type_name_obj_c_metadata(); - } - auto *md = tn->TypeName.objc_metadata; - mutex_lock(md->mutex); - defer (mutex_unlock(md->mutex)); + if (tn->scope != e->scope) { + error(e->token, "@(objc_name) attribute may only be applied to procedures and types within the same scope"); + } else { - if (!ac.objc_is_class_method) { - bool ok = true; - for (TypeNameObjCMetadataEntry const &entry : md->value_entries) { - if (entry.name == ac.objc_name) { - error(e->token, "Previous declaration of @(objc_name=\"%.*s\")", LIT(ac.objc_name)); - ok = false; - break; - } - } - if (ok) { - array_add(&md->value_entries, TypeNameObjCMetadataEntry{ac.objc_name, e}); - } + // Enable implementation by default if the class is an implementer too and + // @objc_implement was not set to false explicitly in this proc. + bool implement = tn->TypeName.objc_is_implementation; + if (ac.objc_is_disabled_implement) { + implement = false; + } + + if (implement) { + GB_ASSERT(e->kind == Entity_Procedure); + + auto &proc = e->type->Proc; + Type *first_param = proc.param_count > 0 ? proc.params->Tuple.variables[0]->type : t_untyped_nil; + + if (!tn->TypeName.objc_is_implementation) { + error(e->token, "@(objc_is_implement) attribute may only be applied to procedures whose class also have @(objc_is_implement) applied"); + } else if (!ac.objc_is_class_method && !(first_param->kind == Type_Pointer && internal_check_is_assignable_to(t, first_param->Pointer.elem))) { + error(e->token, "Objective-C instance methods implementations require the first parameter to be a pointer to the class type set by @(objc_type)"); + } else if (proc.calling_convention == ProcCC_Odin && !tn->TypeName.objc_context_provider) { + error(e->token, "Objective-C methods with Odin calling convention can only be used with classes that have @(objc_context_provider) set"); + } else if (ac.objc_is_class_method && proc.calling_convention != ProcCC_CDecl) { + error(e->token, "Objective-C class methods (objc_is_class_method=true) that have @objc_is_implementation can only use \"c\" calling convention"); + } else if (proc.result_count > 1) { + error(e->token, "Objective-C method implementations may return at most 1 value"); } else { - bool ok = true; - for (TypeNameObjCMetadataEntry const &entry : md->type_entries) { - if (entry.name == ac.objc_name) { - error(e->token, "Previous declaration of @(objc_name=\"%.*s\")", LIT(ac.objc_name)); - ok = false; - break; - } + // Always export unconditionally + // NOTE(harold): This means check_objc_methods() MUST be called before + // e->Procedure.is_export is set in check_proc_decl()! + if (ac.is_export) { + error(e->token, "Explicit export not allowed when @(objc_implement) is set. It set exported implicitly"); } - if (ok) { - array_add(&md->type_entries, TypeNameObjCMetadataEntry{ac.objc_name, e}); + if (ac.link_name != "") { + error(e->token, "Explicit linkage not allowed when @(objc_implement) is set. It set to \"strong\" implicitly"); } + + ac.is_export = true; + ac.linkage = STR_LIT("strong"); + + auto method = ObjcMethodData{ ac, e }; + method.ac.objc_selector = ac.objc_selector != "" ? ac.objc_selector : ac.objc_name; + + CheckerInfo *info = ctx->info; + mutex_lock(&info->objc_method_mutex); + defer (mutex_unlock(&info->objc_method_mutex)); + + Array* method_list = map_get(&info->objc_method_implementations, t); + if (method_list) { + array_add(method_list, method); + } else { + auto list = array_make(permanent_allocator(), 1, 8); + list[0] = method; + + map_set(&info->objc_method_implementations, t, list); + } + } + } else if (ac.objc_selector != "") { + error(e->token, "@(objc_selector) may only be applied to procedures that are Objective-C implementations."); + } + + mutex_lock(&global_type_name_objc_metadata_mutex); + defer (mutex_unlock(&global_type_name_objc_metadata_mutex)); + + if (!tn->TypeName.objc_metadata) { + tn->TypeName.objc_metadata = create_type_name_obj_c_metadata(); + } + auto *md = tn->TypeName.objc_metadata; + mutex_lock(md->mutex); + defer (mutex_unlock(md->mutex)); + + if (!ac.objc_is_class_method) { + bool ok = true; + for (TypeNameObjCMetadataEntry const &entry : md->value_entries) { + if (entry.name == ac.objc_name) { + error(e->token, "Previous declaration of @(objc_name=\"%.*s\")", LIT(ac.objc_name)); + ok = false; + break; + } + } + if (ok) { + array_add(&md->value_entries, TypeNameObjCMetadataEntry{ac.objc_name, e}); + } + } else { + bool ok = true; + for (TypeNameObjCMetadataEntry const &entry : md->type_entries) { + if (entry.name == ac.objc_name) { + error(e->token, "Previous declaration of @(objc_name=\"%.*s\")", LIT(ac.objc_name)); + ok = false; + break; + } + } + if (ok) { + array_add(&md->type_entries, TypeNameObjCMetadataEntry{ac.objc_name, e}); } } } @@ -1141,6 +1282,9 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) { break; } + // NOTE(harold): For Objective-C method implementations, this must happen after + // check_objc_methods() is called as it re-sets ac.is_export to true unconditionally. + // The same is true for the linkage, set below. e->Procedure.entry_point_only = ac.entry_point_only; e->Procedure.is_export = ac.is_export; @@ -1225,6 +1369,7 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) { e->Procedure.has_instrumentation = has_instrumentation; + e->Procedure.no_sanitize_address = ac.no_sanitize_address; e->deprecated_message = ac.deprecated_message; e->warning_message = ac.warning_message; @@ -1240,6 +1385,7 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) { } } + // NOTE(harold): See export/linkage note above(where is_export is assigned) regarding Objective-C method implementations bool is_foreign = e->Procedure.is_foreign; bool is_export = e->Procedure.is_export; diff --git a/src/check_expr.cpp b/src/check_expr.cpp index da193a4cc..72aa07e42 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -643,7 +643,7 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E gb_internal bool check_polymorphic_procedure_assignment(CheckerContext *c, Operand *operand, Type *type, Ast *poly_def_node, PolyProcData *poly_proc_data) { if (operand->expr == nullptr) return false; - Entity *base_entity = entity_of_node(operand->expr); + Entity *base_entity = entity_from_expr(operand->expr); if (base_entity == nullptr) return false; return find_or_generate_polymorphic_procedure(c, base_entity, type, nullptr, poly_def_node, poly_proc_data); } @@ -995,14 +995,34 @@ gb_internal i64 assign_score_function(i64 distance, bool is_variadic=false) { gb_internal bool check_is_assignable_to_with_score(CheckerContext *c, Operand *operand, Type *type, i64 *score_, bool is_variadic=false, bool allow_array_programming=true) { - i64 score = 0; - i64 distance = check_distance_between_types(c, operand, type, allow_array_programming); - bool ok = distance >= 0; - if (ok) { - score = assign_score_function(distance, is_variadic); + if (c == nullptr) { + GB_ASSERT(operand->mode == Addressing_Value); + GB_ASSERT(is_type_typed(operand->type)); } - if (score_) *score_ = score; - return ok; + if (operand->mode == Addressing_Invalid || type == t_invalid) { + if (score_) *score_ = 0; + return false; + } + + // Handle polymorphic procedure used as default parameter + if (operand->mode == Addressing_Value && is_type_proc(type) && is_type_proc(operand->type)) { + Entity *e = entity_from_expr(operand->expr); + if (e != nullptr && e->kind == Entity_Procedure && is_type_polymorphic(e->type) && !is_type_polymorphic(type)) { + // Special case: Allow a polymorphic procedure to be used as default value for concrete proc type + // during the initial check. It will be properly instantiated when actually used. + if (score_) *score_ = assign_score_function(1); + return true; + } + } + + i64 score = check_distance_between_types(c, operand, type, allow_array_programming); + if (score >= 0) { + if (score_) *score_ = assign_score_function(score, is_variadic); + return true; + } + + if (score_) *score_ = 0; + return false; } @@ -1854,7 +1874,10 @@ gb_internal Entity *check_ident(CheckerContext *c, Operand *o, Ast *n, Type *nam o->type = t_invalid; } if (o->type != nullptr && o->type->kind == Type_Named && o->type->Named.type_name->TypeName.is_type_alias) { - o->type = base_type(o->type); + Type *bt = base_type(o->type); + if (bt != nullptr) { + o->type = bt; + } } break; @@ -2907,9 +2930,20 @@ gb_internal void check_comparison(CheckerContext *c, Ast *node, Operand *x, Oper if (!defined) { gbString xs = type_to_string(x->type, temporary_allocator()); gbString ys = type_to_string(y->type, temporary_allocator()); - err_str = gb_string_make(temporary_allocator(), - gb_bprintf("operator '%.*s' not defined between the types '%s' and '%s'", LIT(token_strings[op]), xs, ys) - ); + + if (!is_type_comparable(x->type)) { + err_str = gb_string_make(temporary_allocator(), + gb_bprintf("Type '%s' is not simply comparable, so operator '%.*s' is not defined for it", xs, LIT(token_strings[op])) + ); + } else if (!is_type_comparable(y->type)) { + err_str = gb_string_make(temporary_allocator(), + gb_bprintf("Type '%s' is not simply comparable, so operator '%.*s' is not defined for it", ys, LIT(token_strings[op])) + ); + } else { + err_str = gb_string_make(temporary_allocator(), + gb_bprintf("Operator '%.*s' not defined between the types '%s' and '%s'", LIT(token_strings[op]), xs, ys) + ); + } } else { Type *comparison_type = x->type; if (x->type == err_type && is_operand_nil(*x)) { @@ -2930,11 +2964,11 @@ gb_internal void check_comparison(CheckerContext *c, Ast *node, Operand *x, Oper } else { yt = type_to_string(y->type); } - err_str = gb_string_make(temporary_allocator(), gb_bprintf("mismatched types '%s' and '%s'", xt, yt)); + err_str = gb_string_make(temporary_allocator(), gb_bprintf("Mismatched types '%s' and '%s'", xt, yt)); } if (err_str != nullptr) { - error(node, "Cannot compare expression, %s", err_str); + error(node, "Cannot compare expression. %s.", err_str); x->type = t_untyped_bool; } else { if (x->mode == Addressing_Constant && @@ -5427,8 +5461,18 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod } } + if (operand->type && is_type_simd_vector(type_deref(operand->type))) { + String field_name = selector->Ident.token.string; + if (field_name.len == 1) { + error(op_expr, "Extracting an element from a #simd array using .%.*s syntax is disallowed, prefer `simd.extract`", LIT(field_name)); + } else { + error(op_expr, "Extracting elements from a #simd array using .%.*s syntax is disallowed, prefer `swizzle`", LIT(field_name)); + } + return nullptr; + } + if (entity == nullptr && selector->kind == Ast_Ident && operand->type != nullptr && - (is_type_array(type_deref(operand->type)) || is_type_simd_vector(type_deref(operand->type)))) { + (is_type_array(type_deref(operand->type)))) { String field_name = selector->Ident.token.string; if (1 < field_name.len && field_name.len <= 4) { u8 swizzles_xyzw[4] = {'x', 'y', 'z', 'w'}; @@ -5483,7 +5527,7 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod Type *original_type = operand->type; Type *array_type = base_type(type_deref(original_type)); - GB_ASSERT(array_type->kind == Type_Array || array_type->kind == Type_SimdVector); + GB_ASSERT(array_type->kind == Type_Array); i64 array_count = get_array_type_count(array_type); @@ -5524,10 +5568,6 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod break; } - if (array_type->kind == Type_SimdVector) { - operand->mode = Addressing_Value; - } - Entity *swizzle_entity = alloc_entity_variable(nullptr, make_token_ident(field_name), operand->type, EntityState_Resolved); add_type_and_value(c, operand->expr, operand->mode, operand->type, operand->value); return swizzle_entity; @@ -8142,7 +8182,7 @@ gb_internal ExprKind check_call_expr(CheckerContext *c, Operand *operand, Ast *c type = pt; } type = base_type(type); - if (type->kind == Type_Proc && type->Proc.optional_ok) { + if (type->kind == Type_Proc && type->Proc.optional_ok && type->Proc.result_count > 0) { operand->mode = Addressing_OptionalOk; operand->type = type->Proc.results->Tuple.variables[0]->type; if (operand->expr != nullptr && operand->expr->kind == Ast_CallExpr) { @@ -9013,7 +9053,19 @@ gb_internal ExprKind check_or_else_expr(CheckerContext *c, Operand *o, Ast *node if (left_type != nullptr) { if (!y_is_diverging) { - check_assignment(c, &y, left_type, name); + if (is_type_tuple(left_type)) { + if (!is_type_tuple(y.type)) { + error(y.expr, "Found a single value where a %td-valued expression was expected", left_type->Tuple.variables.count); + } else if (!are_types_identical(left_type, y.type)) { + gbString xt = type_to_string(left_type); + gbString yt = type_to_string(y.type); + error(y.expr, "Mismatched types, expected (%s), got (%s)", xt, yt); + gb_string_free(yt); + gb_string_free(xt); + } + } else { + check_assignment(c, &y, left_type, name); + } } } else { check_or_else_expr_no_value_error(c, name, x, type_hint); @@ -9389,7 +9441,7 @@ gb_internal bool is_expr_inferred_fixed_array(Ast *type_expr) { } gb_internal bool check_for_dynamic_literals(CheckerContext *c, Ast *node, AstCompoundLit *cl) { - if (cl->elems.count > 0 && (check_feature_flags(c, node) & OptInFeatureFlag_DynamicLiterals) == 0) { + if (cl->elems.count > 0 && (check_feature_flags(c, node) & OptInFeatureFlag_DynamicLiterals) == 0 && !build_context.dynamic_literals) { ERROR_BLOCK(); error(node, "Compound literals of dynamic types are disabled by default"); error_line("\tSuggestion: If you want to enable them for this specific file, add '#+feature dynamic-literals' at the top of the file\n"); @@ -10972,7 +11024,7 @@ gb_internal ExprKind check_expr_base_internal(CheckerContext *c, Operand *o, Ast return kind; case_end; - case_ast_node(i, Implicit, node) + case_ast_node(i, Implicit, node); switch (i->kind) { case Token_context: { diff --git a/src/check_stmt.cpp b/src/check_stmt.cpp index e81996566..0460f5bec 100644 --- a/src/check_stmt.cpp +++ b/src/check_stmt.cpp @@ -2108,10 +2108,12 @@ gb_internal void check_value_decl_stmt(CheckerContext *ctx, Ast *node, u32 mod_f if (init_type == nullptr) { init_type = t_invalid; } else if (is_type_polymorphic(base_type(init_type))) { + /* DISABLED: This error seems too aggressive for instantiated generic types. gbString str = type_to_string(init_type); error(vd->type, "Invalid use of a polymorphic type '%s' in variable declaration", str); gb_string_free(str); init_type = t_invalid; + */ } if (init_type == t_invalid && entity_count == 1 && (mod_flags & (Stmt_BreakAllowed|Stmt_FallthroughAllowed))) { Entity *e = entities[0]; @@ -2755,6 +2757,47 @@ gb_internal void check_stmt_internal(CheckerContext *ctx, Ast *node, u32 flags) if (ctx->decl) { ctx->decl->defer_used += 1; } + + // NOTE(bill): Handling errors/warnings + + Ast *stmt = ds->stmt; + Ast *original_stmt = stmt; + + bool is_singular = true; + while (is_singular && stmt->kind == Ast_BlockStmt) { + Ast *inner_stmt = nullptr; + for (Ast *s : stmt->BlockStmt.stmts) { + if (s->kind == Ast_EmptyStmt) { + continue; + } + if (inner_stmt != nullptr) { + is_singular = false; + break; + } + inner_stmt = s; + } + + if (inner_stmt != nullptr) { + stmt = inner_stmt; + } + } + if (!is_singular) { + stmt = original_stmt; + } + + switch (stmt->kind) { + case_ast_node(as, AssignStmt, stmt); + if (as->op.kind != Token_Eq) { + break; + } + for (Ast *lhs : as->lhs) { + Entity *e = entity_of_node(lhs); + if (e && e->flags & EntityFlag_Result) { + error(lhs, "Assignments to named return values within 'defer' will not affect the value that is returned"); + } + } + case_end; + } } case_end; diff --git a/src/check_type.cpp b/src/check_type.cpp index 9d4defbb2..431698459 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -1155,8 +1155,7 @@ gb_internal void check_bit_field_type(CheckerContext *ctx, Type *bit_field_type, } } - - + #if 0 // Reconsider at a later date if (bit_sizes.count > 0 && is_type_integer(backing_type)) { bool all_booleans = is_type_boolean(fields[0]->type); bool all_ones = bit_sizes[0] == 1; @@ -1182,7 +1181,7 @@ gb_internal void check_bit_field_type(CheckerContext *ctx, Type *bit_field_type, } } } - + #endif bit_field_type->BitField.fields = slice_from_array(fields); bit_field_type->BitField.bit_sizes = slice_from_array(bit_sizes); @@ -1911,9 +1910,18 @@ gb_internal Type *check_get_params(CheckerContext *ctx, Scope *scope, Ast *_para case ParameterValue_Location: case ParameterValue_Expression: case ParameterValue_Value: + // Special case for polymorphic procedures as default values + if (param_value.ast_value != nullptr) { + Entity *e = entity_from_expr(param_value.ast_value); + if (e != nullptr && e->kind == Entity_Procedure && is_type_polymorphic(e->type)) { + // Allow polymorphic procedures as default parameter values + // The type will be correctly determined at call site + break; + } + } gbString str = type_to_string(type); error(params[i], "A default value for a parameter must not be a polymorphic constant type, got %s", str); - gb_string_free(str); + gb_string_free(str); break; } } @@ -2082,7 +2090,9 @@ gb_internal Type *check_get_params(CheckerContext *ctx, Scope *scope, Ast *_para if (type != t_invalid && !check_is_assignable_to(ctx, &op, type, allow_array_programming)) { bool ok = true; if (p->flags&FieldFlag_any_int) { - if ((!is_type_integer(op.type) && !is_type_enum(op.type)) || (!is_type_integer(type) && !is_type_enum(type))) { + if (op.type == nullptr) { + ok = false; + } else if ((!is_type_integer(op.type) && !is_type_enum(op.type)) || (!is_type_integer(type) && !is_type_enum(type))) { ok = false; } else if (!check_is_castable_to(ctx, &op, type)) { ok = false; @@ -2773,6 +2783,21 @@ gb_internal void add_map_key_type_dependencies(CheckerContext *ctx, Type *key) { return; } + if (key->kind == Type_Basic) { + if (key->Basic.flags & BasicFlag_Quaternion) { + add_package_dependency(ctx, "runtime", "default_hasher_f64"); + add_package_dependency(ctx, "runtime", "default_hasher_quaternion256"); + return; + } else if (key->Basic.flags & BasicFlag_Complex) { + add_package_dependency(ctx, "runtime", "default_hasher_f64"); + add_package_dependency(ctx, "runtime", "default_hasher_complex128"); + return; + } else if (key->Basic.flags & BasicFlag_Float) { + add_package_dependency(ctx, "runtime", "default_hasher_f64"); + return; + } + } + if (key->kind == Type_Struct) { add_package_dependency(ctx, "runtime", "default_hasher"); for_array(i, key->Struct.fields) { @@ -3281,8 +3306,11 @@ gb_internal void check_array_type_internal(CheckerContext *ctx, Ast *e, Type **t if (generic_type != nullptr) { // Ignore } else if (count < 1 || !is_power_of_two(count)) { - error(at->count, "Invalid length for #simd, expected a power of two length, got '%lld'", cast(long long)count); *type = alloc_type_array(elem, count, generic_type); + if (ctx->disallow_polymorphic_return_types && count == 0) { + return; + } + error(at->count, "Invalid length for #simd, expected a power of two length, got '%lld'", cast(long long)count); return; } @@ -3502,6 +3530,17 @@ gb_internal bool check_type_internal(CheckerContext *ctx, Ast *e, Type **type, T elem = o.type; } + if (!ctx->in_polymorphic_specialization && ctx->disallow_polymorphic_return_types) { + Type *t = base_type(elem); + if (t != nullptr && + unparen_expr(pt->type)->kind == Ast_Ident && + is_type_polymorphic_record_unspecialized(t)) { + gbString err_str = expr_to_string(e); + error(e, "Invalid use of a non-specialized polymorphic type '%s'", err_str); + gb_string_free(err_str); + } + } + if (pt->tag != nullptr) { GB_ASSERT(pt->tag->kind == Ast_BasicDirective); diff --git a/src/checker.cpp b/src/checker.cpp index c44c6ce5b..aaa815365 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -728,12 +728,17 @@ gb_internal void check_scope_usage_internal(Checker *c, Scope *scope, u64 vet_fl bool is_unused = false; if (vet_unused && check_vet_unused(c, e, &ve_unused)) { is_unused = true; - } else if (vet_unused_procedures && - e->kind == Entity_Procedure) { + } else if (vet_unused_procedures && e->kind == Entity_Procedure) { if (e->flags&EntityFlag_Used) { is_unused = false; } else if (e->flags & EntityFlag_Require) { is_unused = false; + } else if (e->flags & EntityFlag_Init) { + is_unused = false; + } else if (e->flags & EntityFlag_Fini) { + is_unused = false; + } else if (e->Procedure.is_export) { + is_unused = false; } else if (e->pkg && e->pkg->kind == Package_Init && e->token.string == "main") { is_unused = false; } else { @@ -751,7 +756,7 @@ gb_internal void check_scope_usage_internal(Checker *c, Scope *scope, u64 vet_fl array_add(&vetted_entities, ve_unused); } else if (is_shadowed) { array_add(&vetted_entities, ve_shadowed); - } else if (e->kind == Entity_Variable && (e->flags & (EntityFlag_Param|EntityFlag_Using|EntityFlag_Static)) == 0 && !e->Variable.is_global) { + } else if (e->kind == Entity_Variable && (e->flags & (EntityFlag_Param|EntityFlag_Using|EntityFlag_Static|EntityFlag_Field)) == 0 && !e->Variable.is_global) { i64 sz = type_size_of(e->type); // TODO(bill): When is a good size warn? // Is >256 KiB good enough? @@ -1351,10 +1356,12 @@ gb_internal void init_universal(void) { t_objc_object = add_global_type_name(intrinsics_pkg->scope, str_lit("objc_object"), alloc_type_struct_complete()); t_objc_selector = add_global_type_name(intrinsics_pkg->scope, str_lit("objc_selector"), alloc_type_struct_complete()); t_objc_class = add_global_type_name(intrinsics_pkg->scope, str_lit("objc_class"), alloc_type_struct_complete()); + t_objc_ivar = add_global_type_name(intrinsics_pkg->scope, str_lit("objc_ivar"), alloc_type_struct_complete()); t_objc_id = alloc_type_pointer(t_objc_object); t_objc_SEL = alloc_type_pointer(t_objc_selector); t_objc_Class = alloc_type_pointer(t_objc_class); + t_objc_Ivar = alloc_type_pointer(t_objc_ivar); } } @@ -1387,6 +1394,10 @@ gb_internal void init_checker_info(CheckerInfo *i) { array_init(&i->defineables, a); map_init(&i->objc_msgSend_types); + mpsc_init(&i->objc_class_implementations, a); + string_set_init(&i->obcj_class_name_set, 0); + map_init(&i->objc_method_implementations); + string_map_init(&i->load_file_cache); array_init(&i->all_procedures, heap_allocator()); @@ -1497,6 +1508,8 @@ gb_internal void init_checker(Checker *c) { TIME_SECTION("init proc queues"); mpsc_init(&c->procs_with_deferred_to_check, a); //, 1<<10); + mpsc_init(&c->procs_with_objc_context_provider_to_check, a); + // NOTE(bill): 1 Mi elements should be enough on average array_init(&c->procs_to_check, heap_allocator(), 0, 1<<20); @@ -3662,6 +3675,33 @@ gb_internal DECL_ATTRIBUTE_PROC(proc_decl_attribute) { } } return true; + } else if (name == "objc_implement") { + ExactValue ev = check_decl_attribute_value(c, value); + if (ev.kind == ExactValue_Bool) { + ac->objc_is_implementation = ev.value_bool; + + if (!ac->objc_is_implementation) { + ac->objc_is_disabled_implement = true; + } + } else if (ev.kind == ExactValue_Invalid) { + ac->objc_is_implementation = true; + } else { + error(elem, "Expected a boolean value, or no value, for '%.*s'", LIT(name)); + } + + return true; + } else if (name == "objc_selector") { + ExactValue ev = check_decl_attribute_value(c, value); + if (ev.kind == ExactValue_String) { + if (string_is_valid_identifier(ev.value_string)) { + ac->objc_selector = ev.value_string; + } else { + error(elem, "Invalid identifier for '%.*s', got '%.*s'", LIT(name), LIT(ev.value_string)); + } + } else { + error(elem, "Expected a string value for '%.*s'", LIT(name)); + } + return true; } else if (name == "require_target_feature") { ExactValue ev = check_decl_attribute_value(c, value); if (ev.kind == ExactValue_String) { @@ -3711,6 +3751,12 @@ gb_internal DECL_ATTRIBUTE_PROC(proc_decl_attribute) { } ac->instrumentation_exit = true; return true; + } else if (name == "no_sanitize_address") { + if (value != nullptr) { + error(value, "'%.*s' expects no parameter", LIT(name)); + } + ac->no_sanitize_address = true; + return true; } return false; } @@ -3901,6 +3947,51 @@ gb_internal DECL_ATTRIBUTE_PROC(type_decl_attribute) { ac->objc_class = ev.value_string; } return true; + } else if (name == "objc_implement") { + ExactValue ev = check_decl_attribute_value(c, value); + if (ev.kind == ExactValue_Bool) { + ac->objc_is_implementation = ev.value_bool; + } else if (ev.kind == ExactValue_Invalid) { + ac->objc_is_implementation = true; + } else { + error(elem, "Expected a boolean value, or no value, for '%.*s'", LIT(name)); + } + return true; + } else if (name == "objc_superclass") { + Type *objc_superclass = check_type(c, value); + + if (objc_superclass != nullptr) { + ac->objc_superclass = objc_superclass; + } else { + error(value, "'%.*s' expected a named type", LIT(name)); + } + return true; + } else if (name == "objc_ivar") { + Type *objc_ivar = check_type(c, value); + + if (objc_ivar != nullptr && objc_ivar->kind == Type_Named) { + ac->objc_ivar = objc_ivar; + } else { + error(value, "'%.*s' expected a named type", LIT(name)); + } + return true; + } else if (name == "objc_context_provider") { + Operand o = {}; + check_expr(c, &o, value); + Entity *e = entity_of_node(o.expr); + + if (e != nullptr) { + if (ac->objc_context_provider != nullptr) { + error(elem, "Previous usage of a 'objc_context_provider' attribute"); + } + if (e->kind != Entity_Procedure) { + error(elem, "'objc_context_provider' must refer to a procedure"); + } else { + ac->objc_context_provider = e; + } + + return true; + } } return false; } @@ -6234,6 +6325,12 @@ gb_internal void check_deferred_procedures(Checker *c) { continue; } + if (dst->flags & EntityFlag_Disabled) { + // Prevent procedures that have been disabled from acting as deferrals. + src->Procedure.deferred_procedure = {}; + continue; + } + GB_ASSERT(is_type_proc(src->type)); GB_ASSERT(is_type_proc(dst->type)); Type *src_params = base_type(src->type)->Proc.params; @@ -6389,6 +6486,44 @@ gb_internal void check_deferred_procedures(Checker *c) { } +gb_internal void check_objc_context_provider_procedures(Checker *c) { + for (Entity *e = nullptr; mpsc_dequeue(&c->procs_with_objc_context_provider_to_check, &e); /**/) { + GB_ASSERT(e->kind == Entity_TypeName); + + Entity *proc_entity = e->TypeName.objc_context_provider; + GB_ASSERT(proc_entity->kind == Entity_Procedure); + + auto &proc = proc_entity->type->Proc; + + Type *return_type = proc.result_count != 1 ? t_untyped_nil : base_named_type(proc.results->Tuple.variables[0]->type); + if (return_type != t_context) { + error(proc_entity->token, "The @(objc_context_provider) procedure must only return a context."); + } + + const char *self_param_err = "The @(objc_context_provider) procedure must take as a parameter a single pointer to the @(objc_type) value."; + if (proc.param_count != 1) { + error(proc_entity->token, self_param_err); + } + + Type *self_param = base_type(proc.params->Tuple.variables[0]->type); + if (self_param->kind != Type_Pointer) { + error(proc_entity->token, self_param_err); + } + + Type *self_type = base_named_type(self_param->Pointer.elem); + if (!internal_check_is_assignable_to(self_type, e->type) && + !(e->TypeName.objc_ivar && internal_check_is_assignable_to(self_type, e->TypeName.objc_ivar))) { + error(proc_entity->token, self_param_err); + } + if (proc.calling_convention != ProcCC_CDecl && proc.calling_convention != ProcCC_Contextless) { + error(e->token, self_param_err); + } + if (proc.is_polymorphic) { + error(e->token, self_param_err); + } + } +} + gb_internal void check_unique_package_names(Checker *c) { ERROR_BLOCK(); @@ -6423,6 +6558,19 @@ gb_internal void check_unique_package_names(Checker *c) { "\tThere is no relation between a package name and the directory that contains it, so they can be completely different\n" "\tA package name is required for link name prefixing to have a consistent ABI\n"); error_line("%s found at previous location\n", token_pos_to_string(ast_token(prev).pos)); + + // NOTE(Jeroen): Check if the conflicting imports are the same case-folded directory + // See https://github.com/odin-lang/Odin/issues/5080 + #if defined(GB_SYSTEM_WINDOWS) + String dir_a = pkg->files[0]->directory; + String dir_b = (*found)->files[0]->directory; + + if (str_eq_ignore_case(dir_a, dir_b)) { + error_line("\tRemember that Windows case-folds paths, and so %.*s and %.*s are the same directory.\n", LIT(dir_a), LIT(dir_b)); + // Could also perform a FS lookup to check which of the two is the actual directory and suggest it, but this should be enough. + } + #endif + end_error_block(); } } @@ -6536,6 +6684,7 @@ gb_internal void check_update_dependency_tree_for_procedures(Checker *c) { } } + gb_internal void check_parsed_files(Checker *c) { TIME_SECTION("map full filepaths to scope"); add_type_info_type(&c->builtin_ctx, t_invalid); @@ -6645,6 +6794,9 @@ gb_internal void check_parsed_files(Checker *c) { TIME_SECTION("check deferred procedures"); check_deferred_procedures(c); + TIME_SECTION("check objc context provider procedures"); + check_objc_context_provider_procedures(c); + TIME_SECTION("calculate global init order"); calculate_global_init_order(c); diff --git a/src/checker.hpp b/src/checker.hpp index d3b2d7d89..0cdfd69ab 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -139,6 +139,7 @@ struct AttributeContext { bool entry_point_only : 1; bool instrumentation_enter : 1; bool instrumentation_exit : 1; + bool no_sanitize_address : 1; bool rodata : 1; bool ignore_duplicates : 1; u32 optimization_mode; // ProcedureOptimizationMode @@ -148,8 +149,14 @@ struct AttributeContext { String objc_class; String objc_name; - bool objc_is_class_method; + String objc_selector; Type * objc_type; + Type * objc_superclass; + Type * objc_ivar; + Entity *objc_context_provider; + bool objc_is_class_method; + bool objc_is_implementation; // This struct or proc provides a class/method implementation, not a binding to an existing type. + bool objc_is_disabled_implement; // This means the method explicitly set @objc_implement to false so it won't be inferred from the class' attribute. String require_target_feature; // required by the target micro-architecture String enable_target_feature; // will be enabled for the procedure only @@ -365,6 +372,11 @@ struct ObjcMsgData { Type *proc_type; }; +struct ObjcMethodData { + AttributeContext ac; + Entity *proc_entity; +}; + enum LoadFileTier { LoadFileTier_Invalid, LoadFileTier_Exists, @@ -476,9 +488,17 @@ struct CheckerInfo { MPSCQueue intrinsics_entry_point_usage; - BlockingMutex objc_types_mutex; + BlockingMutex objc_objc_msgSend_mutex; PtrMap objc_msgSend_types; + BlockingMutex objc_class_name_mutex; + StringSet obcj_class_name_set; + MPSCQueue objc_class_implementations; + + BlockingMutex objc_method_mutex; + PtrMap> objc_method_implementations; + + BlockingMutex load_file_mutex; StringMap load_file_cache; @@ -555,6 +575,7 @@ struct Checker { CheckerContext builtin_ctx; MPSCQueue procs_with_deferred_to_check; + MPSCQueue procs_with_objc_context_provider_to_check; Array procs_to_check; BlockingMutex nested_proc_lits_mutex; @@ -629,4 +650,4 @@ gb_internal void add_untyped_expressions(CheckerInfo *cinfo, UntypedExprInfoMap gb_internal GenTypesData *ensure_polymorphic_record_entity_has_gen_types(CheckerContext *ctx, Type *original_type); -gb_internal void init_map_internal_types(Type *type); \ No newline at end of file +gb_internal void init_map_internal_types(Type *type); diff --git a/src/checker_builtin_procs.hpp b/src/checker_builtin_procs.hpp index 40dde8240..f3b55daa4 100644 --- a/src/checker_builtin_procs.hpp +++ b/src/checker_builtin_procs.hpp @@ -26,6 +26,7 @@ enum BuiltinProcId { BuiltinProc_conj, BuiltinProc_expand_values, + BuiltinProc_compress_values, BuiltinProc_min, BuiltinProc_max, @@ -170,8 +171,12 @@ BuiltinProc__simd_begin, BuiltinProc_simd_extract, BuiltinProc_simd_replace, + BuiltinProc_simd_reduce_add_bisect, + BuiltinProc_simd_reduce_mul_bisect, BuiltinProc_simd_reduce_add_ordered, BuiltinProc_simd_reduce_mul_ordered, + BuiltinProc_simd_reduce_add_pairs, + BuiltinProc_simd_reduce_mul_pairs, BuiltinProc_simd_reduce_min, BuiltinProc_simd_reduce_max, BuiltinProc_simd_reduce_and, @@ -205,6 +210,9 @@ BuiltinProc__simd_begin, BuiltinProc_simd_masked_expand_load, BuiltinProc_simd_masked_compress_store, + BuiltinProc_simd_indices, + + // Platform specific SIMD intrinsics BuiltinProc_simd_x86__MM_SHUFFLE, BuiltinProc__simd_end, @@ -227,6 +235,9 @@ BuiltinProc__type_begin, BuiltinProc_type_convert_variants_to_pointers, BuiltinProc_type_merge, + BuiltinProc_type_integer_to_unsigned, + BuiltinProc_type_integer_to_signed, + BuiltinProc__type_simple_boolean_begin, BuiltinProc_type_is_boolean, BuiltinProc_type_is_integer, @@ -331,6 +342,7 @@ BuiltinProc__type_end, BuiltinProc_objc_find_class, BuiltinProc_objc_register_selector, BuiltinProc_objc_register_class, + BuiltinProc_objc_ivar_get, BuiltinProc_constant_utf16_cstring, @@ -368,6 +380,7 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = { {STR_LIT("conj"), 1, false, Expr_Expr, BuiltinProcPkg_builtin}, {STR_LIT("expand_values"), 1, false, Expr_Expr, BuiltinProcPkg_builtin}, + {STR_LIT("compress_values"), 1, true, Expr_Expr, BuiltinProcPkg_builtin}, {STR_LIT("min"), 1, true, Expr_Expr, BuiltinProcPkg_builtin}, {STR_LIT("max"), 1, true, Expr_Expr, BuiltinProcPkg_builtin}, @@ -515,8 +528,12 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = { {STR_LIT("simd_extract"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_replace"), 3, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("simd_reduce_add_bisect"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("simd_reduce_mul_bisect"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_reduce_add_ordered"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_reduce_mul_ordered"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("simd_reduce_add_pairs"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("simd_reduce_mul_pairs"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_reduce_min"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_reduce_max"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_reduce_and"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, @@ -551,6 +568,8 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = { {STR_LIT("simd_masked_expand_load"), 3, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("simd_masked_compress_store"), 3, false, Expr_Stmt, BuiltinProcPkg_intrinsics}, + {STR_LIT("simd_indices"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("simd_x86__MM_SHUFFLE"), 4, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT(""), 0, false, Expr_Stmt, BuiltinProcPkg_intrinsics}, @@ -569,6 +588,9 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = { {STR_LIT("type_convert_variants_to_pointers"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("type_merge"), 2, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("type_integer_to_unsigned"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT("type_integer_to_signed"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, + {STR_LIT(""), 0, false, Expr_Stmt, BuiltinProcPkg_intrinsics}, {STR_LIT("type_is_boolean"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("type_is_integer"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, @@ -673,6 +695,7 @@ gb_global BuiltinProc builtin_procs[BuiltinProc_COUNT] = { {STR_LIT("objc_find_class"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, {STR_LIT("objc_register_selector"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics, false, true}, {STR_LIT("objc_register_class"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics, false, true}, + {STR_LIT("objc_ivar_get"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics, false, true}, {STR_LIT("constant_utf16_cstring"), 1, false, Expr_Expr, BuiltinProcPkg_intrinsics}, diff --git a/src/entity.cpp b/src/entity.cpp index b2148aa7b..a16779419 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -235,6 +235,10 @@ struct Entity { Type * type_parameter_specialization; String ir_mangled_name; bool is_type_alias; + bool objc_is_implementation; + Type* objc_superclass; + Type* objc_ivar; + Entity*objc_context_provider; String objc_class_name; TypeNameObjCMetadata *objc_metadata; } TypeName; @@ -258,6 +262,7 @@ struct Entity { bool is_memcpy_like : 1; bool uses_branch_location : 1; bool is_anonymous : 1; + bool no_sanitize_address : 1; } Procedure; struct { Array entities; diff --git a/src/exact_value.cpp b/src/exact_value.cpp index ceaed84c1..37751c8f1 100644 --- a/src/exact_value.cpp +++ b/src/exact_value.cpp @@ -954,6 +954,10 @@ gb_internal bool compare_exact_values(TokenKind op, ExactValue x, ExactValue y) case ExactValue_Float: { f64 a = x.value_float; f64 b = y.value_float; + if (isnan(a) || isnan(b)) { + return op == Token_NotEq; + } + switch (op) { case Token_CmpEq: return cmp_f64(a, b) == 0; case Token_NotEq: return cmp_f64(a, b) != 0; diff --git a/src/gb/gb.h b/src/gb/gb.h index 98c362e93..a1b659637 100644 --- a/src/gb/gb.h +++ b/src/gb/gb.h @@ -5838,18 +5838,25 @@ gb_inline isize gb_printf_err_va(char const *fmt, va_list va) { gb_inline isize gb_fprintf_va(struct gbFile *f, char const *fmt, va_list va) { char buf[4096]; - isize len = gb_snprintf_va(buf, gb_size_of(buf), fmt, va); + va_list va_save; + va_copy(va_save, va); + isize len = gb_snprintf_va(buf, gb_size_of(buf), fmt, va_save); + va_end(va_save); char *new_buf = NULL; isize n = gb_size_of(buf); while (len < 0) { + va_copy(va_save, va); + defer (va_end(va_save)); n <<= 1; gb_free(gb_heap_allocator(), new_buf); new_buf = gb_alloc_array(gb_heap_allocator(), char, n);; - len = gb_snprintf_va(new_buf, n, fmt, va); + len = gb_snprintf_va(new_buf, n, fmt, va_save); } - gb_file_write(f, buf, len-1); // NOTE(bill): prevent extra whitespace if (new_buf != NULL) { + gb_file_write(f, new_buf, len-1); // NOTE(bill): prevent extra whitespace gb_free(gb_heap_allocator(), new_buf); + } else { + gb_file_write(f, buf, len-1); // NOTE(bill): prevent extra whitespace } return len; } @@ -5912,7 +5919,7 @@ gb_internal isize gb__print_string(char *text, isize max_len, gbprivFmtInfo *inf len = info->precision < len ? info->precision : len; } - res += gb_strlcpy(text, str, len); + res += gb_strlcpy(text, str, gb_min(len, remaining)); if (info->width > res) { isize padding = info->width - len; @@ -5930,7 +5937,7 @@ gb_internal isize gb__print_string(char *text, isize max_len, gbprivFmtInfo *inf } } - res += gb_strlcpy(text, str, len); + res += gb_strlcpy(text, str, gb_min(len, remaining)); } @@ -6066,15 +6073,16 @@ gb_internal isize gb__print_f64(char *text, isize max_len, gbprivFmtInfo *info, gb_no_inline isize gb_snprintf_va(char *text, isize max_len, char const *fmt, va_list va) { char const *text_begin = text; - isize remaining = max_len, res; + isize remaining = max_len - 1, res; - while (*fmt) { + while (*fmt && remaining > 0) { gbprivFmtInfo info = {0}; isize len = 0; info.precision = -1; - while (*fmt && *fmt != '%' && remaining) { + while (remaining > 0 && *fmt && *fmt != '%') { *text++ = *fmt++; + remaining--; } if (*fmt == '%') { @@ -6240,7 +6248,7 @@ gb_no_inline isize gb_snprintf_va(char *text, isize max_len, char const *fmt, va text += len; if (len >= remaining) { - remaining = gb_min(remaining, 1); + remaining = 0; } else { remaining -= len; } diff --git a/src/linker.cpp b/src/linker.cpp index 5c0fe446f..41d4a13a1 100644 --- a/src/linker.cpp +++ b/src/linker.cpp @@ -143,7 +143,7 @@ gb_internal i32 linker_stage(LinkerData *gen) { LIT(target_arch_names[build_context.metrics.arch]) ); #endif - } else if (build_context.cross_compiling && build_context.different_os) { + } else if (build_context.cross_compiling && (build_context.different_os || selected_subtarget != Subtarget_Default)) { switch (selected_subtarget) { case Subtarget_Android: is_cross_linking = true; @@ -603,7 +603,7 @@ try_cross_linking:; // object // dynamic lib // static libs, absolute full path relative to the file in which the lib was imported from - lib_str = gb_string_append_fmt(lib_str, " %.*s ", LIT(lib)); + lib_str = gb_string_append_fmt(lib_str, " \"%.*s\" ", LIT(lib)); } else { // dynamic or static system lib, just link regularly searching system library paths lib_str = gb_string_append_fmt(lib_str, " -l%.*s ", LIT(lib)); @@ -643,9 +643,10 @@ try_cross_linking:; android_glue_object = concatenate4_strings(temporary_allocator(), temp_dir, str_lit("android_native_app_glue-"), hash, str_lit(".o")); android_glue_static_lib = concatenate4_strings(permanent_allocator(), temp_dir, str_lit("libandroid_native_app_glue-"), hash, str_lit(".a")); - gbString glue = gb_string_make(heap_allocator(), clang_path); + gbString glue = gb_string_make_length(heap_allocator(), ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len); defer (gb_string_free(glue)); + glue = gb_string_append_fmt(glue, "bin/clang"); glue = gb_string_append_fmt(glue, " --target=aarch64-linux-android%d ", ODIN_ANDROID_API_LEVEL); glue = gb_string_appendc(glue, "-c \""); glue = gb_string_append_length(glue, ODIN_ANDROID_NDK.text, ODIN_ANDROID_NDK.len); @@ -655,6 +656,11 @@ try_cross_linking:; glue = gb_string_append_length(glue, android_glue_object.text, android_glue_object.len); glue = gb_string_appendc(glue, "\" "); + glue = gb_string_appendc(glue, "--sysroot \""); + glue = gb_string_append_length(glue, ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len); + glue = gb_string_appendc(glue, "sysroot"); + glue = gb_string_appendc(glue, "\" "); + glue = gb_string_appendc(glue, "\"-I"); glue = gb_string_append_length(glue, ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len); glue = gb_string_appendc(glue, "sysroot/usr/include/"); @@ -763,7 +769,17 @@ try_cross_linking:; gbString platform_lib_str = gb_string_make(heap_allocator(), ""); defer (gb_string_free(platform_lib_str)); if (build_context.metrics.os == TargetOs_darwin) { - platform_lib_str = gb_string_appendc(platform_lib_str, "-Wl,-syslibroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk -L/usr/local/lib "); + // Get the MacOSX SDK path. + gbString darwin_sdk_path = gb_string_make(temporary_allocator(), ""); + if (!system_exec_command_line_app_output("xcrun --sdk macosx --show-sdk-path", &darwin_sdk_path)) { + darwin_sdk_path = gb_string_set(darwin_sdk_path, "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"); + } else { + // Trim the trailing newline. + darwin_sdk_path = gb_string_trim_space(darwin_sdk_path); + } + platform_lib_str = gb_string_append_fmt(platform_lib_str, "--sysroot %s ", darwin_sdk_path); + + platform_lib_str = gb_string_appendc(platform_lib_str, "-L/usr/local/lib "); // Homebrew's default library path, checking if it exists to avoid linking warnings. if (gb_file_exists("/opt/homebrew/lib")) { @@ -785,6 +801,9 @@ try_cross_linking:; // This points the linker to where the entry point is link_settings = gb_string_appendc(link_settings, "-e _main "); } + } else if (build_context.metrics.os == TargetOs_freebsd) { + // FreeBSD pkg installs third-party shared libraries in /usr/local/lib. + platform_lib_str = gb_string_appendc(platform_lib_str, "-Wl,-L/usr/local/lib "); } else if (build_context.metrics.os == TargetOs_openbsd) { // OpenBSD ports install shared libraries in /usr/local/lib. Also, we must explicitly link libpthread. platform_lib_str = gb_string_appendc(platform_lib_str, "-lpthread -Wl,-L/usr/local/lib "); @@ -803,6 +822,9 @@ try_cross_linking:; platform_lib_str = gb_string_append_length(platform_lib_str, ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL.text, ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL.len); platform_lib_str = gb_string_appendc(platform_lib_str, "\" "); + platform_lib_str = gb_string_appendc(platform_lib_str, "-landroid "); + platform_lib_str = gb_string_appendc(platform_lib_str, "-llog "); + platform_lib_str = gb_string_appendc(platform_lib_str, "\"--sysroot="); platform_lib_str = gb_string_append_length(platform_lib_str, ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT.text, ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT.len); platform_lib_str = gb_string_appendc(platform_lib_str, "\" "); @@ -835,11 +857,16 @@ try_cross_linking:; } } - gbString link_command_line = gb_string_make(heap_allocator(), clang_path); + gbString link_command_line = gb_string_make(heap_allocator(), ""); defer (gb_string_free(link_command_line)); if (is_android) { + gbString ndk_bin_directory = gb_string_make_length(temporary_allocator(), ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len); + link_command_line = gb_string_appendc(link_command_line, ndk_bin_directory); + link_command_line = gb_string_appendc(link_command_line, "bin/clang"); link_command_line = gb_string_append_fmt(link_command_line, " --target=aarch64-linux-android%d ", ODIN_ANDROID_API_LEVEL); + } else { + link_command_line = gb_string_appendc(link_command_line, clang_path); } link_command_line = gb_string_appendc(link_command_line, " -Wno-unused-command-line-argument "); link_command_line = gb_string_appendc(link_command_line, object_files); diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 6d9f6d958..af08722c3 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -573,7 +573,9 @@ namespace lbAbiAmd64SysV { gb_internal void classify_with(LLVMTypeRef t, Array *cls, i64 ix, i64 off); gb_internal void fixup(LLVMTypeRef t, Array *cls); - gb_internal lbArgType amd64_type(LLVMContextRef c, LLVMTypeRef type, Amd64TypeAttributeKind attribute_kind, ProcCallingConvention calling_convention); + gb_internal lbArgType amd64_type(LLVMContextRef c, LLVMTypeRef type, Amd64TypeAttributeKind attribute_kind, ProcCallingConvention calling_convention, + bool is_arg, + i32 *int_regs, i32 *sse_regs); gb_internal Array classify(LLVMTypeRef t); gb_internal LLVMTypeRef llreg(LLVMContextRef c, Array const ®_classes, LLVMTypeRef type); @@ -583,7 +585,9 @@ namespace lbAbiAmd64SysV { } LB_ABI_MODIFY_RETURN_IF_TUPLE_MACRO(); - return amd64_type(c, return_type, Amd64TypeAttribute_StructRect, ft->calling_convention); + return amd64_type(c, return_type, Amd64TypeAttribute_StructRect, ft->calling_convention, + false, + nullptr, nullptr); } gb_internal LB_ABI_INFO(abi_info) { @@ -592,10 +596,16 @@ namespace lbAbiAmd64SysV { ft->ctx = c; ft->calling_convention = calling_convention; + i32 int_regs = 6; // rdi, rsi, rdx, rcx, r8, r9 + i32 sse_regs = 8; // xmm0-xmm7 + ft->args = array_make(lb_function_type_args_allocator(), arg_count); for (unsigned i = 0; i < arg_count; i++) { - ft->args[i] = amd64_type(c, arg_types[i], Amd64TypeAttribute_ByVal, calling_convention); + ft->args[i] = amd64_type(c, arg_types[i], Amd64TypeAttribute_ByVal, calling_convention, + true, + &int_regs, &sse_regs); } + ft->ret = compute_return_type(ft, c, return_type, return_is_defined, return_is_tuple); return ft; @@ -654,17 +664,79 @@ namespace lbAbiAmd64SysV { } - gb_internal lbArgType amd64_type(LLVMContextRef c, LLVMTypeRef type, Amd64TypeAttributeKind attribute_kind, ProcCallingConvention calling_convention) { + + gb_internal bool is_aggregate(LLVMTypeRef type) { + LLVMTypeKind kind = LLVMGetTypeKind(type); + switch (kind) { + case LLVMStructTypeKind: + if (LLVMCountStructElementTypes(type) == 1) { + return is_aggregate(LLVMStructGetTypeAtIndex(type, 0)); + } + return true; + case LLVMArrayTypeKind: + if (LLVMGetArrayLength(type) == 1) { + return is_aggregate(LLVMGetElementType(type)); + } + return true; + } + return false; + }; + + gb_internal lbArgType amd64_type(LLVMContextRef c, LLVMTypeRef type, Amd64TypeAttributeKind attribute_kind, ProcCallingConvention calling_convention, + bool is_arg, + i32 *int_regs, i32 *sse_regs) { + auto cls = classify(type); + i32 needed_int = 0; + i32 needed_sse = 0; + for (auto c : cls) { + switch (c) { + case RegClass_Int: + needed_int += 1; + break; + case RegClass_SSEFs: + case RegClass_SSEFv: + case RegClass_SSEDs: + case RegClass_SSEDv: + case RegClass_SSEInt8: + case RegClass_SSEInt16: + case RegClass_SSEInt32: + case RegClass_SSEInt64: + case RegClass_SSEInt128: + case RegClass_SSEUp: + needed_sse += 1; + break; + } + } + + bool ran_out_of_regs = false; + if (int_regs && sse_regs) { + *int_regs -= needed_int; + *sse_regs -= needed_sse; + bool int_ok = *int_regs >= 0; + bool sse_ok = *sse_regs >= 0; + + *int_regs = gb_max(*int_regs, 0); + *sse_regs = gb_max(*sse_regs, 0); + + if ((!int_ok || !sse_ok) && is_aggregate(type)) { + ran_out_of_regs = true; + } + } + if (is_register(type)) { LLVMAttributeRef attribute = nullptr; if (type == LLVMInt1TypeInContext(c)) { attribute = lb_create_enum_attribute(c, "zeroext"); } return lb_arg_type_direct(type, nullptr, nullptr, attribute); - } - - auto cls = classify(type); - if (is_mem_cls(cls, attribute_kind)) { + } else if (ran_out_of_regs) { + if (is_arg) { + return lb_arg_type_indirect_byval(c, type); + } else { + LLVMAttributeRef attribute = lb_create_enum_attribute_with_type(c, "sret", type); + return lb_arg_type_indirect(type, attribute); + } + } else if (is_mem_cls(cls, attribute_kind)) { LLVMAttributeRef attribute = nullptr; if (attribute_kind == Amd64TypeAttribute_ByVal) { if (is_calling_convention_odin(calling_convention)) { @@ -905,7 +977,7 @@ namespace lbAbiAmd64SysV { return types[0]; } - return LLVMStructTypeInContext(c, types.data, cast(unsigned)types.count, sz == 0); + return LLVMStructTypeInContext(c, types.data, cast(unsigned)types.count, false); } gb_internal void classify_with(LLVMTypeRef t, Array *cls, i64 ix, i64 off) { @@ -1159,38 +1231,24 @@ namespace lbAbiArm64 { } } else { i64 size = lb_sizeof(return_type); - if (size <= 16) { - LLVMTypeRef cast_type = nullptr; - - if (size == 0) { - cast_type = LLVMStructTypeInContext(c, nullptr, 0, false); - } else if (size <= 8) { - cast_type = LLVMIntTypeInContext(c, cast(unsigned)(size*8)); - } else { - unsigned count = cast(unsigned)((size+7)/8); - - LLVMTypeRef llvm_i64 = LLVMIntTypeInContext(c, 64); - LLVMTypeRef *types = gb_alloc_array(temporary_allocator(), LLVMTypeRef, count); - - i64 size_copy = size; - for (unsigned i = 0; i < count; i++) { - if (size_copy >= 8) { - types[i] = llvm_i64; - } else { - types[i] = LLVMIntTypeInContext(c, 8*cast(unsigned)size_copy); - } - size_copy -= 8; - } - GB_ASSERT(size_copy <= 0); - cast_type = LLVMStructTypeInContext(c, types, count, true); - } - return lb_arg_type_direct(return_type, cast_type, nullptr, nullptr); - } else { + if (size > 16) { LB_ABI_MODIFY_RETURN_IF_TUPLE_MACRO(); LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); return lb_arg_type_indirect(return_type, attr); } + + GB_ASSERT(size <= 16); + LLVMTypeRef cast_type = nullptr; + if (size == 0) { + cast_type = LLVMStructTypeInContext(c, nullptr, 0, false); + } else if (size <= 8) { + cast_type = LLVMIntTypeInContext(c, cast(unsigned)(size*8)); + } else { + LLVMTypeRef llvm_i64 = LLVMIntTypeInContext(c, 64); + cast_type = llvm_array_type(llvm_i64, 2); + } + return lb_arg_type_direct(return_type, cast_type, nullptr, nullptr); } } @@ -1818,7 +1876,8 @@ gb_internal LB_ABI_INFO(lb_get_abi_info) { return_type, return_is_defined, ALLOW_SPLIT_MULTI_RETURNS && return_is_tuple && is_calling_convention_odin(calling_convention), calling_convention, - base_type(original_type)); + base_type(original_type) + ); // NOTE(bill): this is handled here rather than when developing the type in `lb_type_internal_for_procedures_raw` diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index 396b94f98..c73552d57 100644 --- a/src/llvm_backend.cpp +++ b/src/llvm_backend.cpp @@ -563,6 +563,53 @@ gb_internal lbValue lb_hasher_proc_for_type(lbModule *m, Type *type) { lbValue res = lb_emit_runtime_call(p, "default_hasher_string", args); lb_add_callsite_force_inline(p, res); LLVMBuildRet(p->builder, res.value); + } else if (is_type_float(type)) { + lbValue ptr = lb_emit_conv(p, data, pt); + lbValue v = lb_emit_load(p, ptr); + v = lb_emit_conv(p, v, t_f64); + + auto args = array_make(temporary_allocator(), 2); + args[0] = v; + args[1] = seed; + lbValue res = lb_emit_runtime_call(p, "default_hasher_f64", args); + lb_add_callsite_force_inline(p, res); + LLVMBuildRet(p->builder, res.value); + } else if (is_type_complex(type)) { + lbValue ptr = lb_emit_conv(p, data, pt); + lbValue xp = lb_emit_struct_ep(p, ptr, 0); + lbValue yp = lb_emit_struct_ep(p, ptr, 1); + + lbValue x = lb_emit_conv(p, lb_emit_load(p, xp), t_f64); + lbValue y = lb_emit_conv(p, lb_emit_load(p, yp), t_f64); + + auto args = array_make(temporary_allocator(), 3); + args[0] = x; + args[1] = y; + args[2] = seed; + lbValue res = lb_emit_runtime_call(p, "default_hasher_complex128", args); + lb_add_callsite_force_inline(p, res); + LLVMBuildRet(p->builder, res.value); + } else if (is_type_quaternion(type)) { + lbValue ptr = lb_emit_conv(p, data, pt); + lbValue xp = lb_emit_struct_ep(p, ptr, 0); + lbValue yp = lb_emit_struct_ep(p, ptr, 1); + lbValue zp = lb_emit_struct_ep(p, ptr, 2); + lbValue wp = lb_emit_struct_ep(p, ptr, 3); + + lbValue x = lb_emit_conv(p, lb_emit_load(p, xp), t_f64); + lbValue y = lb_emit_conv(p, lb_emit_load(p, yp), t_f64); + lbValue z = lb_emit_conv(p, lb_emit_load(p, zp), t_f64); + lbValue w = lb_emit_conv(p, lb_emit_load(p, wp), t_f64); + + auto args = array_make(temporary_allocator(), 5); + args[0] = x; + args[1] = y; + args[2] = z; + args[3] = w; + args[4] = seed; + lbValue res = lb_emit_runtime_call(p, "default_hasher_quaternion256", args); + lb_add_callsite_force_inline(p, res); + LLVMBuildRet(p->builder, res.value); } else { GB_PANIC("Unhandled type for hasher: %s", type_to_string(type)); } @@ -1126,6 +1173,344 @@ gb_internal lbProcedure *lb_create_objc_names(lbModule *main_module) { return p; } +String lb_get_objc_type_encoding(Type *t, isize pointer_depth = 0) { + // NOTE(harold): See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100 + + // NOTE(harold): Darwin targets are always 64-bit. Should we drop this and assume "q" always? + #define INT_SIZE_ENCODING (build_context.metrics.int_size == 4 ? "i" : "q") + switch (t->kind) { + case Type_Basic: { + switch (t->Basic.kind) { + case Basic_Invalid: + return str_lit("?"); + + case Basic_llvm_bool: + case Basic_bool: + case Basic_b8: + return str_lit("B"); + + case Basic_b16: + return str_lit("C"); + case Basic_b32: + return str_lit("I"); + case Basic_b64: + return str_lit("q"); + case Basic_i8: + return str_lit("c"); + case Basic_u8: + return str_lit("C"); + case Basic_i16: + case Basic_i16le: + case Basic_i16be: + return str_lit("s"); + case Basic_u16: + case Basic_u16le: + case Basic_u16be: + return str_lit("S"); + case Basic_i32: + case Basic_i32le: + case Basic_i32be: + return str_lit("i"); + case Basic_u32le: + case Basic_u32: + case Basic_u32be: + return str_lit("I"); + case Basic_i64: + case Basic_i64le: + case Basic_i64be: + return str_lit("q"); + case Basic_u64: + case Basic_u64le: + case Basic_u64be: + return str_lit("Q"); + case Basic_i128: + case Basic_i128le: + case Basic_i128be: + return str_lit("t"); + case Basic_u128: + case Basic_u128le: + case Basic_u128be: + return str_lit("T"); + case Basic_rune: + return str_lit("I"); + case Basic_f16: + case Basic_f16le: + case Basic_f16be: + return str_lit("s"); // @harold: Closest we've got? + case Basic_f32: + case Basic_f32le: + case Basic_f32be: + return str_lit("f"); + case Basic_f64: + case Basic_f64le: + case Basic_f64be: + return str_lit("d"); + + case Basic_complex32: return str_lit("{complex32=ss}"); // No f16 encoding, so fallback to i16, as above in Basic_f16* + case Basic_complex64: return str_lit("{complex64=ff}"); + case Basic_complex128: return str_lit("{complex128=dd}"); + case Basic_quaternion64: return str_lit("{quaternion64=ssss}"); + case Basic_quaternion128: return str_lit("{quaternion128=ffff}"); + case Basic_quaternion256: return str_lit("{quaternion256=dddd}"); + + case Basic_int: + return str_lit(INT_SIZE_ENCODING); + case Basic_uint: + return build_context.metrics.int_size == 4 ? str_lit("I") : str_lit("Q"); + case Basic_uintptr: + case Basic_rawptr: + return str_lit("^v"); + + case Basic_string: + return build_context.metrics.int_size == 4 ? str_lit("{string=*i}") : str_lit("{string=*q}"); + + case Basic_cstring: return str_lit("*"); + case Basic_any: return str_lit("{any=^v^v}"); // rawptr + ^Type_Info + + case Basic_typeid: + GB_ASSERT(t->Basic.size == 8); + return str_lit("q"); + + // Untyped types + case Basic_UntypedBool: + case Basic_UntypedInteger: + case Basic_UntypedFloat: + case Basic_UntypedComplex: + case Basic_UntypedQuaternion: + case Basic_UntypedString: + case Basic_UntypedRune: + case Basic_UntypedNil: + case Basic_UntypedUninit: + GB_PANIC("Untyped types cannot be @encoded()"); + return str_lit("?"); + } + break; + } + + case Type_Named: + case Type_Struct: + case Type_Union: { + Type* base = t; + if (base->kind == Type_Named) { + base = base_type(base); + if(base->kind != Type_Struct && base->kind != Type_Union) { + return lb_get_objc_type_encoding(base, pointer_depth); + } + } + + const bool is_union = base->kind == Type_Union; + if (!is_union) { + // Check for objc_SEL + if (internal_check_is_assignable_to(base, t_objc_SEL)) { + return str_lit(":"); + } + + // Check for objc_Class + if (internal_check_is_assignable_to(base, t_objc_SEL)) { + return str_lit("#"); + } + + // Treat struct as an Objective-C Class? + if (has_type_got_objc_class_attribute(base) && pointer_depth == 0) { + return str_lit("#"); + } + } + + if (is_type_objc_object(base)) { + return str_lit("@"); + } + + + gbString s = gb_string_make_reserve(temporary_allocator(), 16); + s = gb_string_append_length(s, is_union ? "(" :"{", 1); + if (t->kind == Type_Named) { + s = gb_string_append_length(s, t->Named.name.text, t->Named.name.len); + } + + // Write fields + if (pointer_depth < 2) { + s = gb_string_append_length(s, "=", 1); + + if (!is_union) { + for( auto& f : base->Struct.fields ) { + String field_type = lb_get_objc_type_encoding(f->type, pointer_depth); + s = gb_string_append_length(s, field_type.text, field_type.len); + } + } else { + for( auto& v : base->Union.variants ) { + String variant_type = lb_get_objc_type_encoding(v, pointer_depth); + s = gb_string_append_length(s, variant_type.text, variant_type.len); + } + } + } + + s = gb_string_append_length(s, is_union ? ")" :"}", 1); + + return make_string_c(s); + } + + case Type_Generic: + GB_PANIC("Generic types cannot be @encoded()"); + return str_lit("?"); + + case Type_Pointer: { + String pointee = lb_get_objc_type_encoding(t->Pointer.elem, pointer_depth +1); + // Special case for Objective-C Objects + if (pointer_depth == 0 && pointee == "@") { + return pointee; + } + + return concatenate_strings(temporary_allocator(), str_lit("^"), pointee); + } + + case Type_MultiPointer: + return concatenate_strings(temporary_allocator(), str_lit("^"), lb_get_objc_type_encoding(t->Pointer.elem, pointer_depth +1)); + + case Type_Array: { + String type_str = lb_get_objc_type_encoding(t->Array.elem, pointer_depth); + + gbString s = gb_string_make_reserve(temporary_allocator(), type_str.len + 8); + s = gb_string_append_fmt(s, "[%lld%.*s]", t->Array.count, LIT(type_str)); + return make_string_c(s); + } + + case Type_EnumeratedArray: { + String type_str = lb_get_objc_type_encoding(t->EnumeratedArray.elem, pointer_depth); + + gbString s = gb_string_make_reserve(temporary_allocator(), type_str.len + 8); + s = gb_string_append_fmt(s, "[%lld%.*s]", t->EnumeratedArray.count, LIT(type_str)); + return make_string_c(s); + } + + case Type_Slice: { + String type_str = lb_get_objc_type_encoding(t->Slice.elem, pointer_depth); + gbString s = gb_string_make_reserve(temporary_allocator(), type_str.len + 8); + s = gb_string_append_fmt(s, "{slice=^%.*s%s}", LIT(type_str), INT_SIZE_ENCODING); + return make_string_c(s); + } + + case Type_DynamicArray: { + String type_str = lb_get_objc_type_encoding(t->DynamicArray.elem, pointer_depth); + gbString s = gb_string_make_reserve(temporary_allocator(), type_str.len + 8); + s = gb_string_append_fmt(s, "{dynamic=^%.*s%s%sAllocator={?^v}}", LIT(type_str), INT_SIZE_ENCODING, INT_SIZE_ENCODING); + return make_string_c(s); + } + + case Type_Map: + return str_lit("{^v^v{Allocator=?^v}}"); + case Type_Enum: + return lb_get_objc_type_encoding(t->Enum.base_type, pointer_depth); + case Type_Tuple: + // NOTE(harold): Is this type allowed here? + return str_lit("?"); + case Type_Proc: + return str_lit("?"); + case Type_BitSet: + return lb_get_objc_type_encoding(t->BitSet.underlying, pointer_depth); + + case Type_SimdVector: { + String type_str = lb_get_objc_type_encoding(t->SimdVector.elem, pointer_depth); + gbString s = gb_string_make_reserve(temporary_allocator(), type_str.len + 5); + gb_string_append_fmt(s, "[%lld%.*s]", t->SimdVector.count, LIT(type_str)); + return make_string_c(s); + } + + case Type_Matrix: { + String type_str = lb_get_objc_type_encoding(t->Matrix.elem, pointer_depth); + gbString s = gb_string_make_reserve(temporary_allocator(), type_str.len + 5); + i64 element_count = t->Matrix.column_count * t->Matrix.row_count; + gb_string_append_fmt(s, "[%lld%.*s]", element_count, LIT(type_str)); + return make_string_c(s); + } + + case Type_BitField: + return lb_get_objc_type_encoding(t->BitField.backing_type, pointer_depth); + case Type_SoaPointer: { + gbString s = gb_string_make_reserve(temporary_allocator(), 8); + s = gb_string_append_fmt(s, "{=^v%s}", INT_SIZE_ENCODING); + return make_string_c(s); + } + + } // End switch t->kind + #undef INT_SIZE_ENCODING + + GB_PANIC("Unreachable"); + return str_lit(""); +} + +struct lbObjCGlobalClass { + lbObjCGlobal g; + lbValue class_value; // Local registered class value +}; + +gb_internal void lb_register_objc_thing( + StringSet &handled, + lbModule *m, + Array &args, + Array &class_impls, + StringMap &class_map, + lbProcedure *p, + lbObjCGlobal const &g, + char const *call +) { + if (string_set_update(&handled, g.name)) { + return; + } + + lbAddr addr = {}; + lbValue *found = string_map_get(&m->members, g.global_name); + if (found) { + addr = lb_addr(*found); + } else { + lbValue v = {}; + LLVMTypeRef t = lb_type(m, g.type); + v.value = LLVMAddGlobal(m->mod, t, g.global_name); + v.type = alloc_type_pointer(g.type); + addr = lb_addr(v); + LLVMSetInitializer(v.value, LLVMConstNull(t)); + } + + lbValue class_ptr = {}; + lbValue class_name = lb_const_value(m, t_cstring, exact_value_string(g.name)); + + // If this class requires an implementation, save it for registration below. + if (g.class_impl_type != nullptr) { + + // Make sure the superclass has been initialized before us + lbValue superclass_value = lb_const_nil(m, t_objc_Class); + + auto &tn = g.class_impl_type->Named.type_name->TypeName; + Type *superclass = tn.objc_superclass; + if (superclass != nullptr) { + auto& superclass_global = string_map_must_get(&class_map, superclass->Named.type_name->TypeName.objc_class_name); + lb_register_objc_thing(handled, m, args, class_impls, class_map, p, superclass_global.g, call); + GB_ASSERT(superclass_global.class_value.value); + + superclass_value = superclass_global.class_value; + } + + args.count = 3; + args[0] = superclass_value; + args[1] = class_name; + args[2] = lb_const_int(m, t_uint, 0); + class_ptr = lb_emit_runtime_call(p, "objc_allocateClassPair", args); + + array_add(&class_impls, lbObjCGlobalClass{g, class_ptr}); + } + else { + args.count = 1; + args[0] = class_name; + class_ptr = lb_emit_runtime_call(p, call, args); + } + + lb_addr_store(p, addr, class_ptr); + + lbObjCGlobalClass* class_global = string_map_get(&class_map, g.name); + if (class_global != nullptr) { + class_global->class_value = class_ptr; + } +} + gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { if (p == nullptr) { return; @@ -1139,38 +1524,329 @@ gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { string_set_init(&handled); defer (string_set_destroy(&handled)); - auto args = array_make(temporary_allocator(), 1); + auto args = array_make(temporary_allocator(), 3, 8); + auto class_impls = array_make(temporary_allocator(), 0, 16); + + // Register all class implementations unconditionally, even if not statically referenced + for (Entity *e = {}; mpsc_dequeue(&gen->info->objc_class_implementations, &e); /**/) { + GB_ASSERT(e->kind == Entity_TypeName && e->TypeName.objc_is_implementation); + lb_handle_objc_find_or_register_class(p, e->TypeName.objc_class_name, e->type); + } + + // Ensure classes that have been implicitly referenced through + // the objc_superclass attribute have a global variable available for them. + TypeSet class_set{}; + type_set_init(&class_set, gen->objc_classes.count+16); + defer (type_set_destroy(&class_set)); + + auto referenced_classes = array_make(temporary_allocator()); + for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_classes, &g); /**/) { + array_add(&referenced_classes, g); + + Type *cls = g.class_impl_type; + while (cls) { + if (type_set_update(&class_set, cls)) { + break; + } + GB_ASSERT(cls->kind == Type_Named); + + cls = cls->Named.type_name->TypeName.objc_superclass; + } + } + + for (auto pair : class_set) { + auto& tn = pair.type->Named.type_name->TypeName; + Type *class_impl = !tn.objc_is_implementation ? nullptr : pair.type; + lb_handle_objc_find_or_register_class(p, tn.objc_class_name, class_impl); + } + for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_classes, &g); /**/) { + array_add( &referenced_classes, g ); + } + + // Add all class globals to a map so that we can look them up dynamically + // in order to resolve out-of-order because classes that are being implemented + // require their superclasses to be registered before them. + StringMap global_class_map{}; + string_map_init(&global_class_map, (usize)gen->objc_classes.count); + defer (string_map_destroy(&global_class_map)); + + for (lbObjCGlobal g :referenced_classes) { + string_map_set(&global_class_map, g.name, lbObjCGlobalClass{g}); + } LLVMSetLinkage(p->value, LLVMInternalLinkage); lb_begin_procedure_body(p); - auto register_thing = [&handled, &m, &args](lbProcedure *p, lbObjCGlobal const &g, char const *call) { - if (!string_set_update(&handled, g.name)) { - lbAddr addr = {}; - lbValue *found = string_map_get(&m->members, g.global_name); - if (found) { - addr = lb_addr(*found); - } else { - lbValue v = {}; - LLVMTypeRef t = lb_type(m, g.type); - v.value = LLVMAddGlobal(m->mod, t, g.global_name); - v.type = alloc_type_pointer(g.type); - addr = lb_addr(v); - LLVMSetInitializer(v.value, LLVMConstNull(t)); - } - - args[0] = lb_const_value(m, t_cstring, exact_value_string(g.name)); - lbValue ptr = lb_emit_runtime_call(p, call, args); - lb_addr_store(p, addr, ptr); - } - }; - - for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_classes, &g); /**/) { - register_thing(p, g, "objc_lookUpClass"); + // Register class globals, gathering classes that must be implemented + for (auto& kv : global_class_map) { + lb_register_objc_thing(handled, m, args, class_impls, global_class_map, p, kv.value.g, "objc_lookUpClass"); } + // Prefetch selectors for implemented methods so that they can also be registered. + for (const auto& cd : class_impls) { + auto& g = cd.g; + Type *class_type = g.class_impl_type; + + Array* methods = map_get(&m->info->objc_method_implementations, class_type); + if (!methods) { + continue; + } + + for (const ObjcMethodData& md : *methods) { + lb_handle_objc_find_or_register_selector(p, md.ac.objc_selector); + } + } + + // Now we can register all referenced selectors for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_selectors, &g); /**/) { - register_thing(p, g, "sel_registerName"); + lb_register_objc_thing(handled, m, args, class_impls, global_class_map, p, g, "sel_registerName"); + } + + + // Emit method wrapper implementations and registration + auto wrapper_args = array_make(temporary_allocator(), 2, 8); + auto get_context_args = array_make(temporary_allocator(), 1); + + + PtrMap ivar_map{}; + map_init(&ivar_map, gen->objc_ivars.count); + + for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_ivars, &g); /**/) { + map_set(&ivar_map, g.class_impl_type, g); + } + + for (const auto &cd : class_impls) { + auto &g = cd.g; + Type *class_type = g.class_impl_type; + Type *class_ptr_type = alloc_type_pointer(class_type); + lbValue class_value = cd.class_value; + + Type *ivar_type = class_type->Named.type_name->TypeName.objc_ivar; + + Entity *context_provider = class_type->Named.type_name->TypeName.objc_context_provider; + Type *contex_provider_self_ptr_type = nullptr; + Type *contex_provider_self_named_type = nullptr; + bool is_context_provider_ivar = false; + lbValue context_provider_proc_value{}; + + if (context_provider) { + context_provider_proc_value = lb_find_procedure_value_from_entity(m, context_provider); + + contex_provider_self_ptr_type = base_type(context_provider->type->Proc.params->Tuple.variables[0]->type); + GB_ASSERT(contex_provider_self_ptr_type->kind == Type_Pointer); + contex_provider_self_named_type = base_named_type(type_deref(contex_provider_self_ptr_type)); + + is_context_provider_ivar = ivar_type != nullptr && internal_check_is_assignable_to(contex_provider_self_named_type, ivar_type); + } + + + Array *methods = map_get(&m->info->objc_method_implementations, class_type); + if (!methods) { + continue; + } + + for (const ObjcMethodData &md : *methods) { + GB_ASSERT( md.proc_entity->kind == Entity_Procedure); + Type *method_type = md.proc_entity->type; + + String proc_name = make_string_c("__$objc_method::"); + proc_name = concatenate_strings(temporary_allocator(), proc_name, g.name); + proc_name = concatenate_strings(temporary_allocator(), proc_name, str_lit("::")); + proc_name = concatenate_strings( permanent_allocator(), proc_name, md.ac.objc_name); + + wrapper_args.count = 2; + wrapper_args[0] = md.ac.objc_is_class_method ? t_objc_Class : class_ptr_type; + wrapper_args[1] = t_objc_SEL; + + isize method_param_count = method_type->Proc.param_count; + isize method_param_offset = 0; + + if (!md.ac.objc_is_class_method) { + GB_ASSERT(method_param_count >= 1); + method_param_count -= 1; + method_param_offset = 1; + } + + for (isize i = 0; i < method_param_count; i++) { + array_add(&wrapper_args, method_type->Proc.params->Tuple.variables[method_param_offset+i]->type); + } + + Type *wrapper_args_tuple = alloc_type_tuple_from_field_types(wrapper_args.data, wrapper_args.count, false, true); + Type *wrapper_results_tuple = nullptr; + + if (method_type->Proc.result_count > 0) { + GB_ASSERT(method_type->Proc.result_count == 1); + wrapper_results_tuple = alloc_type_tuple_from_field_types(&method_type->Proc.results->Tuple.variables[0]->type, 1, false, true); + } + + Type *wrapper_proc_type = alloc_type_proc(nullptr, wrapper_args_tuple, wrapper_args_tuple->Tuple.variables.count, + wrapper_results_tuple, method_type->Proc.result_count, false, ProcCC_CDecl); + + lbProcedure *wrapper_proc = lb_create_dummy_procedure(m, proc_name, wrapper_proc_type); + lb_add_attribute_to_proc(wrapper_proc->module, wrapper_proc->value, "nounwind"); + + // Emit the wrapper + LLVMSetLinkage(wrapper_proc->value, LLVMExternalLinkage); + lb_begin_procedure_body(wrapper_proc); + { + if (method_type->Proc.calling_convention == ProcCC_Odin) { + GB_ASSERT(context_provider); + + // Emit the get odin context call + + get_context_args[0] = lbValue { + wrapper_proc->raw_input_parameters[0], + contex_provider_self_ptr_type, + }; + + if (is_context_provider_ivar) { + // The context provider takes the ivar's type. + // Emit an objc_ivar_get call and use that pointer for 'self' instead. + lbValue real_self { + wrapper_proc->raw_input_parameters[0], + class_ptr_type + }; + get_context_args[0] = lb_handle_objc_ivar_for_objc_object_pointer(wrapper_proc, real_self); + } + + lbValue context = lb_emit_call(wrapper_proc, context_provider_proc_value, get_context_args); + lbAddr context_addr = lb_addr(lb_address_from_load_or_generate_local(wrapper_proc, context)); + lb_push_context_onto_stack(wrapper_proc, context_addr); + } + + + auto method_call_args = array_make(temporary_allocator(), method_param_count + method_param_offset); + + if (!md.ac.objc_is_class_method) { + method_call_args[0] = lbValue { + wrapper_proc->raw_input_parameters[0], + class_ptr_type, + }; + } + + for (isize i = 0; i < method_param_count; i++) { + method_call_args[i+method_param_offset] = lbValue { + wrapper_proc->raw_input_parameters[i+2], + method_type->Proc.params->Tuple.variables[i+method_param_offset]->type, + }; + } + lbValue method_proc_value = lb_find_procedure_value_from_entity(m, md.proc_entity); + + // Call real procedure for method from here, passing the parameters expected, if any. + lbValue return_value = lb_emit_call(wrapper_proc, method_proc_value, method_call_args); + + if (wrapper_results_tuple != nullptr) { + auto &result_var = method_type->Proc.results->Tuple.variables[0]; + return_value = lb_emit_conv(wrapper_proc, return_value, result_var->type); + lb_build_return_stmt_internal(wrapper_proc, return_value, result_var->token.pos); + } + } + lb_end_procedure_body(wrapper_proc); + + + // Add the method to the class + String method_encoding = str_lit("v"); + // TODO (harold): Checker must ensure that objc_methods have a single return value or none! + GB_ASSERT(method_type->Proc.result_count <= 1); + if (method_type->Proc.result_count != 0) { + method_encoding = lb_get_objc_type_encoding(method_type->Proc.results->Tuple.variables[0]->type); + } + + if (!md.ac.objc_is_class_method) { + method_encoding = concatenate_strings(temporary_allocator(), method_encoding, str_lit("@:")); + } else { + method_encoding = concatenate_strings(temporary_allocator(), method_encoding, str_lit("#:")); + } + + for (isize i = method_param_offset; i < method_param_count; i++) { + Type *param_type = method_type->Proc.params->Tuple.variables[i]->type; + String param_encoding = lb_get_objc_type_encoding(param_type); + + method_encoding = concatenate_strings(temporary_allocator(), method_encoding, param_encoding); + } + + // Emit method registration + lbAddr* sel_address = string_map_get(&m->objc_selectors, md.ac.objc_selector); + GB_ASSERT(sel_address); + lbValue selector_value = lb_addr_load(p, *sel_address); + + args.count = 4; + args[0] = class_value; // Class + args[1] = selector_value; // SEL + args[2] = lbValue { wrapper_proc->value, wrapper_proc->type }; + args[3] = lb_const_value(m, t_cstring, exact_value_string(method_encoding)); + + // TODO(harold): Emit check BOOL result and panic if false. + lb_emit_runtime_call(p, "class_addMethod", args); + + } // End methods + + // Add ivar if we have one + if (ivar_type != nullptr) { + // Register a single ivar for this class + Type *ivar_base = ivar_type->Named.base; + + // @note(harold): The alignment is supposed to be passed as log2(alignment): https://developer.apple.com/documentation/objectivec/class_addivar(_:_:_:_:_:)?language=objc + const i64 size = type_size_of(ivar_base); + const i64 alignment = (i64)floor_log2((u64)type_align_of(ivar_base)); + + // NOTE(harold): I've opted to not emit the type encoding for ivars in order to keep the data private. + // If there is desire in the future to emit the type encoding for introspection through the Obj-C runtime, + // then perhaps an option can be added for it then. + // Should we pass the actual type encoding? Might not be ideal for obfuscation. + String ivar_name = str_lit("__$ivar"); + String ivar_types = str_lit("{= }"); //lb_get_objc_type_encoding(ivar_type); + args.count = 5; + args[0] = class_value; + args[1] = lb_const_value(m, t_cstring, exact_value_string(ivar_name)); + args[2] = lb_const_value(m, t_uint, exact_value_u64((u64)size)); + args[3] = lb_const_value(m, t_u8, exact_value_u64((u64)alignment)); + args[4] = lb_const_value(m, t_cstring, exact_value_string(ivar_types)); + lb_emit_runtime_call(p, "class_addIvar", args); + } + + // Complete the class registration + args.count = 1; + args[0] = class_value; + lb_emit_runtime_call(p, "objc_registerClassPair", args); + } + + // Register ivar offsets for any `objc_ivar_get` expressions emitted. + for (auto const& kv : ivar_map) { + lbObjCGlobal const& g = kv.value; + lbAddr ivar_addr = {}; + lbValue *found = string_map_get(&m->members, g.global_name); + + if (found) { + ivar_addr = lb_addr(*found); + GB_ASSERT(ivar_addr.addr.type == t_int_ptr); + } else { + // Defined in an external package, define it now in the main package + LLVMTypeRef t = lb_type(m, t_int); + + lbValue global{}; + global.value = LLVMAddGlobal(m->mod, t, g.global_name); + global.type = t_int_ptr; + + LLVMSetInitializer(global.value, LLVMConstInt(t, 0, true)); + + ivar_addr = lb_addr(global); + } + + String class_name = g.class_impl_type->Named.type_name->TypeName.objc_class_name; + lbValue class_value = string_map_must_get(&global_class_map, class_name).class_value; + + args.count = 2; + args[0] = class_value; + args[1] = lb_const_value(m, t_cstring, exact_value_string(str_lit("__$ivar"))); + lbValue ivar = lb_emit_runtime_call(p, "class_getInstanceVariable", args); + + args.count = 1; + args[0] = ivar; + lbValue ivar_offset = lb_emit_runtime_call(p, "ivar_getOffset", args); + lbValue ivar_offset_int = lb_emit_conv(p, ivar_offset, t_int); + + lb_addr_store(p, ivar_addr, ivar_offset_int); } lb_end_procedure_body(p); @@ -1231,6 +1907,10 @@ gb_internal lbProcedure *lb_create_startup_runtime(lbModule *main_module, lbProc lb_add_attribute_to_proc(p->module, p->value, "optnone"); lb_add_attribute_to_proc(p->module, p->value, "noinline"); + // Make sure shared libraries call their own runtime startup on Linux. + LLVMSetVisibility(p->value, LLVMHiddenVisibility); + LLVMSetLinkage(p->value, LLVMWeakAnyLinkage); + lb_begin_procedure_body(p); lb_setup_type_info_data(main_module); @@ -1297,14 +1977,14 @@ gb_internal lbProcedure *lb_create_startup_runtime(lbModule *main_module, lbProc gbString var_name = gb_string_make(permanent_allocator(), "__$global_any::"); gbString e_str = string_canonical_entity_name(temporary_allocator(), e); var_name = gb_string_append_length(var_name, e_str, gb_strlen(e_str)); - lbAddr g = lb_add_global_generated_with_name(main_module, var_type, var.init, make_string_c(var_name)); + lbAddr g = lb_add_global_generated_with_name(main_module, var_type, {}, make_string_c(var_name)); lb_addr_store(p, g, var.init); lbValue gp = lb_addr_get_ptr(p, g); lbValue data = lb_emit_struct_ep(p, var.var, 0); lbValue ti = lb_emit_struct_ep(p, var.var, 1); lb_emit_store(p, data, lb_emit_conv(p, gp, t_rawptr)); - lb_emit_store(p, ti, lb_type_info(p, var_type)); + lb_emit_store(p, ti, lb_typeid(p->module, var_type)); } else { LLVMTypeRef vt = llvm_addr_type(p->module, var.var); lbValue src0 = lb_emit_conv(p, var.init, t); @@ -1340,6 +2020,10 @@ gb_internal lbProcedure *lb_create_cleanup_runtime(lbModule *main_module) { // C lb_add_attribute_to_proc(p->module, p->value, "optnone"); lb_add_attribute_to_proc(p->module, p->value, "noinline"); + // Make sure shared libraries call their own runtime cleanup on Linux. + LLVMSetVisibility(p->value, LLVMHiddenVisibility); + LLVMSetLinkage(p->value, LLVMWeakAnyLinkage); + lb_begin_procedure_body(p); CheckerInfo *info = main_module->gen->info; @@ -1414,7 +2098,7 @@ gb_internal void lb_create_global_procedures_and_types(lbGenerator *gen, Checker break; case Entity_Constant: if (build_context.ODIN_DEBUG) { - add_debug_info_for_global_constant_from_entity(gen, e); + lb_add_debug_info_for_global_constant_from_entity(gen, e); } break; } @@ -2056,8 +2740,8 @@ gb_internal lbProcedure *lb_create_main_procedure(lbModule *m, lbProcedure *star if (testing_proc->pkg != nullptr) { pkg_name = testing_proc->pkg->name; } - lbValue v_pkg = lb_find_or_add_entity_string(m, pkg_name); - lbValue v_name = lb_find_or_add_entity_string(m, name); + lbValue v_pkg = lb_find_or_add_entity_string(m, pkg_name, false); + lbValue v_name = lb_find_or_add_entity_string(m, name, false); lbValue v_proc = lb_find_procedure_value_from_entity(m, testing_proc); indices[1] = LLVMConstInt(lb_type(m, t_int), testing_proc_index++, false); @@ -2138,7 +2822,6 @@ gb_internal void lb_generate_procedure(lbModule *m, lbProcedure *p) { p->is_done = true; m->curr_procedure = nullptr; } - lb_end_procedure(p); // Add Flags if (p->entity && p->entity->kind == Entity_Procedure && p->entity->Procedure.is_memcpy_like) { @@ -2447,6 +3130,7 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { LLVMSetInitializer(g, LLVMConstNull(lb_type(m, t))); LLVMSetLinkage(g, LLVMInternalLinkage); lb_make_global_private_const(g); + lb_set_odin_rtti_section(g); return lb_addr({g, alloc_type_pointer(t)}); }; @@ -2518,24 +3202,9 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { lbValue g = {}; g.value = LLVMAddGlobal(m->mod, lb_type(m, e->type), alloc_cstring(permanent_allocator(), name)); g.type = alloc_type_pointer(e->type); - if (e->Variable.thread_local_model != "") { - LLVMSetThreadLocal(g.value, true); - String m = e->Variable.thread_local_model; - LLVMThreadLocalMode mode = LLVMGeneralDynamicTLSModel; - if (m == "default") { - mode = LLVMGeneralDynamicTLSModel; - } else if (m == "localdynamic") { - mode = LLVMLocalDynamicTLSModel; - } else if (m == "initialexec") { - mode = LLVMInitialExecTLSModel; - } else if (m == "localexec") { - mode = LLVMLocalExecTLSModel; - } else { - GB_PANIC("Unhandled thread local mode %.*s", LIT(m)); - } - LLVMSetThreadLocalMode(g.value, mode); - } + lb_apply_thread_local_model(g.value, e->Variable.thread_local_model); + if (is_foreign) { LLVMSetLinkage(g.value, LLVMExternalLinkage); LLVMSetDLLStorageClass(g.value, LLVMDLLImportStorageClass); @@ -2551,6 +3220,7 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { LLVMSetLinkage(g.value, USE_SEPARATE_MODULES ? LLVMWeakAnyLinkage : LLVMInternalLinkage); } lb_set_linkage_from_entity_flags(m, g.value, e->flags); + LLVMSetAlignment(g.value, cast(u32)type_align_of(e->type)); if (e->Variable.link_section.len > 0) { LLVMSetSection(g.value, alloc_cstring(permanent_allocator(), e->Variable.link_section)); @@ -2565,12 +3235,16 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { if (!is_type_any(e->type) && !is_type_union(e->type)) { if (tav.mode != Addressing_Invalid) { if (tav.value.kind != ExactValue_Invalid) { - bool is_rodata = e->kind == Entity_Variable && e->Variable.is_rodata; + auto cc = LB_CONST_CONTEXT_DEFAULT; + cc.is_rodata = e->kind == Entity_Variable && e->Variable.is_rodata; + cc.allow_local = false; + cc.link_section = e->Variable.link_section; + ExactValue v = tav.value; - lbValue init = lb_const_value(m, tav.type, v, false, is_rodata); + lbValue init = lb_const_value(m, tav.type, v, cc); LLVMSetInitializer(g.value, init.value); var.is_initialized = true; - if (is_rodata) { + if (cc.is_rodata) { LLVMSetGlobalConstant(g.value, true); } } @@ -2585,6 +3259,11 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { } else if (e->kind == Entity_Variable && e->Variable.is_rodata) { LLVMSetGlobalConstant(g.value, true); } + + if (e->flags & EntityFlag_Require) { + lb_append_to_compiler_used(m, g.value); + } + array_add(&global_variables, var); lb_add_entity(m, e, g); @@ -2621,6 +3300,28 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { } } + if (build_context.ODIN_DEBUG) { + // Custom `.raddbg` section for its debugger + if (build_context.metrics.os == TargetOs_windows) { + lbModule *m = default_module; + LLVMModuleRef mod = m->mod; + LLVMContextRef ctx = m->ctx; + + { + LLVMTypeRef type = LLVMArrayType(LLVMInt8TypeInContext(ctx), 1); + LLVMValueRef global = LLVMAddGlobal(mod, type, "raddbg_is_attached_byte_marker"); + LLVMSetInitializer(global, LLVMConstNull(type)); + LLVMSetSection(global, ".raddbg"); + } + + if (gen->info->entry_point) { + String mangled_name = lb_get_entity_name(m, gen->info->entry_point); + char const *str = alloc_cstring(temporary_allocator(), mangled_name); + lb_add_raddbg_string(m, "entry_point: \"", str, "\""); + } + } + } + TIME_SECTION("LLVM Runtime Objective-C Names Creation"); gen->objc_names = lb_create_objc_names(default_module); @@ -2634,7 +3335,7 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { if (build_context.ODIN_DEBUG) { for (auto const &entry : builtin_pkg->scope->elements) { Entity *e = entry.value; - add_debug_info_for_global_constant_from_entity(gen, e); + lb_add_debug_info_for_global_constant_from_entity(gen, e); } } @@ -2664,6 +3365,72 @@ gb_internal bool lb_generate_code(lbGenerator *gen) { if (build_context.ODIN_DEBUG) { TIME_SECTION("LLVM Debug Info Complete Types and Finalize"); lb_debug_info_complete_types_and_finalize(gen); + + // Custom `.raddbg` section for its debugger + if (build_context.metrics.os == TargetOs_windows) { + lbModule *m = default_module; + LLVMModuleRef mod = m->mod; + LLVMContextRef ctx = m->ctx; + + lb_add_raddbg_string(m, "type_view: {type: \"[]?\", expr: \"array(data, len)\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"string\", expr: \"array(data, len)\"}"); + + // column major matrices + lb_add_raddbg_string(m, "type_view: {type: \"matrix[1, ?]?\", expr: \"table($.data, $[0])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[2, ?]?\", expr: \"table($.data, $[0], $[1])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[3, ?]?\", expr: \"table($.data, $[0], $[1], $[2])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[4, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[5, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[6, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[7, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[8, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[9, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[10, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[11, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[12, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[13, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[14, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12], $[13])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[15, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12], $[13], $[14])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"matrix[16, ?]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12], $[13], $[14], $[15])\"}"); + + // row major matrices + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 1]?\", expr: \"table($.data, $[0])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 2]?\", expr: \"table($.data, $[0], $[1])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 3]?\", expr: \"table($.data, $[0], $[1], $[2])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 4]?\", expr: \"table($.data, $[0], $[1], $[2], $[3])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 5]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 6]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 7]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 8]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 9]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 10]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 11]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 12]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 13]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 14]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12], $[13])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 15]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12], $[13], $[14])\"}"); + lb_add_raddbg_string(m, "type_view: {type: \"#row_major matrix[?, 16]?\", expr: \"table($.data, $[0], $[1], $[2], $[3], $[4], $[5], $[6], $[7], $[8], $[9], $[10], $[11], $[12], $[13], $[14], $[15])\"}"); + + + TEMPORARY_ALLOCATOR_GUARD(); + + u32 global_name_index = 0; + for (String str = {}; mpsc_dequeue(&gen->raddebug_section_strings, &str); /**/) { + LLVMValueRef data = LLVMConstStringInContext(ctx, cast(char const *)str.text, cast(unsigned)str.len, false); + LLVMTypeRef type = LLVMTypeOf(data); + + gbString global_name = gb_string_make(temporary_allocator(), "raddbg_data__"); + global_name = gb_string_append_fmt(global_name, "%u", global_name_index); + global_name_index += 1; + + LLVMValueRef global = LLVMAddGlobal(mod, type, global_name); + + LLVMSetInitializer(global, data); + LLVMSetAlignment(global, 1); + + LLVMSetSection(global, ".raddbg"); + } + } } if (do_threading) { diff --git a/src/llvm_backend.hpp b/src/llvm_backend.hpp index 3e01ada5f..fd6f50dcd 100644 --- a/src/llvm_backend.hpp +++ b/src/llvm_backend.hpp @@ -196,6 +196,7 @@ struct lbModule { StringMap objc_classes; StringMap objc_selectors; + StringMap objc_ivars; PtrMap map_cell_info_map; // address of runtime.Map_Info PtrMap map_info_map; // address of runtime.Map_Cell_Info @@ -219,6 +220,7 @@ struct lbObjCGlobal { gbString global_name; String name; Type * type; + Type * class_impl_type; // This is set when the class has the objc_implement attribute set to true. }; struct lbGenerator : LinkerData { @@ -240,6 +242,8 @@ struct lbGenerator : LinkerData { MPSCQueue entities_to_correct_linkage; MPSCQueue objc_selectors; MPSCQueue objc_classes; + MPSCQueue objc_ivars; + MPSCQueue raddebug_section_strings; }; @@ -383,6 +387,8 @@ struct lbProcedure { PtrMap selector_values; PtrMap selector_addr; PtrMap tuple_fix_map; + + Array asan_stack_locals; }; @@ -407,7 +413,6 @@ gb_internal LLVMAttributeRef lb_create_enum_attribute_with_type(LLVMContextRef c gb_internal void lb_add_proc_attribute_at_index(lbProcedure *p, isize index, char const *name, u64 value); gb_internal void lb_add_proc_attribute_at_index(lbProcedure *p, isize index, char const *name); gb_internal lbProcedure *lb_create_procedure(lbModule *module, Entity *entity, bool ignore_body=false); -gb_internal void lb_end_procedure(lbProcedure *p); gb_internal LLVMTypeRef lb_type(lbModule *m, Type *type); @@ -415,9 +420,19 @@ gb_internal LLVMTypeRef llvm_get_element_type(LLVMTypeRef type); gb_internal lbBlock *lb_create_block(lbProcedure *p, char const *name, bool append=false); +struct lbConstContext { + bool allow_local; + bool is_rodata; + String link_section; +}; + +static lbConstContext const LB_CONST_CONTEXT_DEFAULT = {true, false, {}}; +static lbConstContext const LB_CONST_CONTEXT_DEFAULT_ALLOW_LOCAL = {true, false, {}}; +static lbConstContext const LB_CONST_CONTEXT_DEFAULT_NO_LOCAL = {false, false, {}}; + gb_internal lbValue lb_const_nil(lbModule *m, Type *type); gb_internal lbValue lb_const_undef(lbModule *m, Type *type); -gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bool allow_local=true, bool is_rodata=false); +gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, lbConstContext cc = LB_CONST_CONTEXT_DEFAULT); gb_internal lbValue lb_const_bool(lbModule *m, Type *type, bool value); gb_internal lbValue lb_const_int(lbModule *m, Type *type, u64 value); @@ -514,7 +529,7 @@ gb_internal void lb_fill_slice(lbProcedure *p, lbAddr const &slice, lbValue base gb_internal lbValue lb_type_info(lbProcedure *p, Type *type); -gb_internal lbValue lb_find_or_add_entity_string(lbModule *m, String const &str); +gb_internal lbValue lb_find_or_add_entity_string(lbModule *m, String const &str, bool custom_link_section); gb_internal lbValue lb_generate_anonymous_proc_lit(lbModule *m, String const &prefix_name, Ast *expr, lbProcedure *parent = nullptr); gb_internal bool lb_is_const(lbValue value); diff --git a/src/llvm_backend_const.cpp b/src/llvm_backend_const.cpp index 9401e4d55..51c8a4449 100644 --- a/src/llvm_backend_const.cpp +++ b/src/llvm_backend_const.cpp @@ -301,10 +301,10 @@ gb_internal lbValue lb_const_source_code_location_const(lbModule *m, String cons } LLVMValueRef fields[4] = {}; - fields[0]/*file*/ = lb_find_or_add_entity_string(m, file).value; + fields[0]/*file*/ = lb_find_or_add_entity_string(m, file, false).value; fields[1]/*line*/ = lb_const_int(m, t_i32, line).value; fields[2]/*column*/ = lb_const_int(m, t_i32, column).value; - fields[3]/*procedure*/ = lb_find_or_add_entity_string(m, procedure).value; + fields[3]/*procedure*/ = lb_find_or_add_entity_string(m, procedure, false).value; lbValue res = {}; res.value = llvm_const_named_struct(m, t_source_code_location, fields, gb_count_of(fields)); @@ -391,12 +391,12 @@ gb_internal lbValue lb_emit_source_code_location_as_global(lbProcedure *p, Ast * -gb_internal LLVMValueRef lb_build_constant_array_values(lbModule *m, Type *type, Type *elem_type, isize count, LLVMValueRef *values, bool allow_local, bool is_rodata) { - if (allow_local) { - is_rodata = false; +gb_internal LLVMValueRef lb_build_constant_array_values(lbModule *m, Type *type, Type *elem_type, isize count, LLVMValueRef *values, lbConstContext cc) { + if (cc.allow_local) { + cc.is_rodata = false; } - bool is_local = allow_local && m->curr_procedure != nullptr; + bool is_local = cc.allow_local && m->curr_procedure != nullptr; bool is_const = true; if (is_local) { for (isize i = 0; i < count; i++) { @@ -500,9 +500,9 @@ gb_internal bool lb_is_nested_possibly_constant(Type *ft, Selection const &sel, return lb_is_elem_const(elem, ft); } -gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bool allow_local, bool is_rodata) { - if (allow_local) { - is_rodata = false; +gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, lbConstContext cc) { + if (cc.allow_local) { + cc.is_rodata = false; } LLVMContextRef ctx = m->ctx; @@ -533,7 +533,10 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo Entity *e = entity_from_expr(expr); res = lb_find_procedure_value_from_entity(m, e); } - GB_ASSERT(res.value != nullptr); + if (res.value == nullptr) { + // This is an unspecialized polymorphic procedure, return nil or dummy value + return lb_const_nil(m, original_type); + } GB_ASSERT(LLVMGetValueKind(res.value) == LLVMFunctionValueKind); if (LLVMGetIntrinsicID(res.value) == 0) { @@ -543,7 +546,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo return res; } - bool is_local = allow_local && m->curr_procedure != nullptr; + bool is_local = cc.allow_local && m->curr_procedure != nullptr; // GB_ASSERT_MSG(is_type_typed(type), "%s", type_to_string(type)); @@ -562,7 +565,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo count = gb_max(cast(isize)cl->max_count, count); Type *elem = base_type(type)->Slice.elem; Type *t = alloc_type_array(elem, count); - lbValue backing_array = lb_const_value(m, t, value, allow_local, is_rodata); + lbValue backing_array = lb_const_value(m, t, value, cc); LLVMValueRef array_data = nullptr; @@ -599,7 +602,10 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo array_data = LLVMAddGlobal(m->mod, lb_type(m, t), str); LLVMSetInitializer(array_data, backing_array.value); - if (is_rodata) { + if (cc.link_section.len > 0) { + LLVMSetSection(array_data, alloc_cstring(permanent_allocator(), cc.link_section)); + } + if (cc.is_rodata) { LLVMSetGlobalConstant(array_data, true); } @@ -650,7 +656,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } // NOTE(bill, 2021-10-07): Allow for array programming value constants Type *core_elem = core_array_type(type); - return lb_const_value(m, core_elem, value, allow_local, is_rodata); + return lb_const_value(m, core_elem, value, cc); } else if (is_type_u8_array(type) && value.kind == ExactValue_String) { GB_ASSERT(type->Array.count == value.value_string.len); LLVMValueRef data = LLVMConstStringInContext(ctx, @@ -668,7 +674,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo Type *elem = type->Array.elem; - lbValue single_elem = lb_const_value(m, elem, value, allow_local, is_rodata); + lbValue single_elem = lb_const_value(m, elem, value, cc); LLVMValueRef *elems = gb_alloc_array(permanent_allocator(), LLVMValueRef, cast(isize)count); for (i64 i = 0; i < count; i++) { @@ -686,7 +692,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo Type *elem = type->Matrix.elem; - lbValue single_elem = lb_const_value(m, elem, value, allow_local, is_rodata); + lbValue single_elem = lb_const_value(m, elem, value, cc); single_elem.value = llvm_const_cast(single_elem.value, lb_type(m, elem)); i64 total_elem_count = matrix_type_total_internal_elems(type); @@ -708,7 +714,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i64 count = type->SimdVector.count; Type *elem = type->SimdVector.elem; - lbValue single_elem = lb_const_value(m, elem, value, allow_local, is_rodata); + lbValue single_elem = lb_const_value(m, elem, value, cc); single_elem.value = llvm_const_cast(single_elem.value, lb_type(m, elem)); LLVMValueRef *elems = gb_alloc_array(permanent_allocator(), LLVMValueRef, count); @@ -729,9 +735,16 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo return res; case ExactValue_String: { - LLVMValueRef ptr = lb_find_or_add_entity_string_ptr(m, value.value_string); + bool custom_link_section = cc.link_section.len > 0; + + LLVMValueRef ptr = lb_find_or_add_entity_string_ptr(m, value.value_string, custom_link_section); lbValue res = {}; res.type = default_type(original_type); + + if (custom_link_section) { + LLVMSetSection(ptr, alloc_cstring(permanent_allocator(), cc.link_section)); + } + if (is_type_cstring(res.type)) { res.value = ptr; } else { @@ -837,7 +850,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo case ExactValue_Compound: if (is_type_slice(type)) { - return lb_const_value(m, type, value, allow_local, is_rodata); + return lb_const_value(m, type, value, cc); } else if (is_type_array(type)) { ast_node(cl, CompoundLit, value.value_compound); Type *elem_type = type->Array.elem; @@ -871,7 +884,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } if (lo == i) { TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; for (i64 k = lo; k < hi; k++) { values[value_index++] = val; } @@ -886,7 +899,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i64 index = exact_value_to_i64(index_tav.value); if (index == i) { TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; values[value_index++] = val; found = true; break; @@ -899,7 +912,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } } - res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->Array.count, values, allow_local, is_rodata); + res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->Array.count, values, cc); return res; } else { GB_ASSERT_MSG(elem_count == type->Array.count, "%td != %td", elem_count, type->Array.count); @@ -909,13 +922,13 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo for (isize i = 0; i < elem_count; i++) { TypeAndValue tav = cl->elems[i]->tav; GB_ASSERT(tav.mode != Addressing_Invalid); - values[i] = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + values[i] = lb_const_value(m, elem_type, tav.value, cc).value; } for (isize i = elem_count; i < type->Array.count; i++) { values[i] = LLVMConstNull(lb_type(m, elem_type)); } - res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->Array.count, values, allow_local, is_rodata); + res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->Array.count, values, cc); return res; } } else if (is_type_enumerated_array(type)) { @@ -955,7 +968,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } if (lo == i) { TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; for (i64 k = lo; k < hi; k++) { values[value_index++] = val; } @@ -970,7 +983,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i64 index = exact_value_to_i64(index_tav.value); if (index == i) { TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; values[value_index++] = val; found = true; break; @@ -983,7 +996,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } } - res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->EnumeratedArray.count, values, allow_local, is_rodata); + res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->EnumeratedArray.count, values, cc); return res; } else { GB_ASSERT_MSG(elem_count == type->EnumeratedArray.count, "%td != %td", elem_count, type->EnumeratedArray.count); @@ -993,13 +1006,13 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo for (isize i = 0; i < elem_count; i++) { TypeAndValue tav = cl->elems[i]->tav; GB_ASSERT(tav.mode != Addressing_Invalid); - values[i] = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + values[i] = lb_const_value(m, elem_type, tav.value, cc).value; } for (isize i = elem_count; i < type->EnumeratedArray.count; i++) { values[i] = LLVMConstNull(lb_type(m, elem_type)); } - res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->EnumeratedArray.count, values, allow_local, is_rodata); + res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)type->EnumeratedArray.count, values, cc); return res; } } else if (is_type_simd_vector(type)) { @@ -1038,7 +1051,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } if (lo == i) { TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; for (i64 k = lo; k < hi; k++) { values[value_index++] = val; } @@ -1053,7 +1066,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i64 index = exact_value_to_i64(index_tav.value); if (index == i) { TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; values[value_index++] = val; found = true; break; @@ -1072,7 +1085,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo for (isize i = 0; i < elem_count; i++) { TypeAndValue tav = cl->elems[i]->tav; GB_ASSERT(tav.mode != Addressing_Invalid); - values[i] = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + values[i] = lb_const_value(m, elem_type, tav.value, cc).value; } LLVMTypeRef et = lb_type(m, elem_type); @@ -1121,11 +1134,13 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i32 index = field_remapping[f->Variable.field_index]; if (elem_type_can_be_constant(f->type)) { if (sel.index.count == 1) { - values[index] = lb_const_value(m, f->type, tav.value, allow_local, is_rodata).value; + values[index] = lb_const_value(m, f->type, tav.value, cc).value; visited[index] = true; } else { if (!visited[index]) { - values[index] = lb_const_value(m, f->type, {}, /*allow_local*/false, is_rodata).value; + auto new_cc = cc; + new_cc.allow_local = false; + values[index] = lb_const_value(m, f->type, {}, new_cc).value; visited[index] = true; } @@ -1165,7 +1180,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } } if (is_constant) { - LLVMValueRef elem_value = lb_const_value(m, tav.type, tav.value, allow_local, is_rodata).value; + LLVMValueRef elem_value = lb_const_value(m, tav.type, tav.value, cc).value; if (LLVMIsConstant(elem_value) && LLVMIsConstant(values[index])) { values[index] = llvm_const_insert_value(m, values[index], elem_value, idx_list, idx_list_len); } else if (is_local) { @@ -1219,7 +1234,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i32 index = field_remapping[f->Variable.field_index]; if (elem_type_can_be_constant(f->type)) { - values[index] = lb_const_value(m, f->type, val, allow_local, is_rodata).value; + values[index] = lb_const_value(m, f->type, val, cc).value; visited[index] = true; } } @@ -1353,7 +1368,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; for (i64 k = lo; k < hi; k++) { i64 offset = matrix_row_major_index_to_offset(type, k); GB_ASSERT(values[offset] == nullptr); @@ -1365,7 +1380,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo i64 index = exact_value_to_i64(index_tav.value); GB_ASSERT(index < max_count); TypeAndValue tav = fv->value->tav; - LLVMValueRef val = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + LLVMValueRef val = lb_const_value(m, elem_type, tav.value, cc).value; i64 offset = matrix_row_major_index_to_offset(type, index); GB_ASSERT(values[offset] == nullptr); values[offset] = val; @@ -1378,7 +1393,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } } - res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)total_count, values, allow_local, is_rodata); + res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)total_count, values, cc); return res; } else { GB_ASSERT_MSG(elem_count == max_count, "%td != %td", elem_count, max_count); @@ -1389,7 +1404,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo GB_ASSERT(tav.mode != Addressing_Invalid); i64 offset = 0; offset = matrix_row_major_index_to_offset(type, i); - values[offset] = lb_const_value(m, elem_type, tav.value, allow_local, is_rodata).value; + values[offset] = lb_const_value(m, elem_type, tav.value, cc).value; } for (isize i = 0; i < total_count; i++) { if (values[i] == nullptr) { @@ -1397,7 +1412,7 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, bo } } - res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)total_count, values, allow_local, is_rodata); + res.value = lb_build_constant_array_values(m, type, elem_type, cast(isize)total_count, values, cc); return res; } } else { diff --git a/src/llvm_backend_debug.cpp b/src/llvm_backend_debug.cpp index 53c007d8d..024c5564e 100644 --- a/src/llvm_backend_debug.cpp +++ b/src/llvm_backend_debug.cpp @@ -18,6 +18,25 @@ gb_internal void lb_set_llvm_metadata(lbModule *m, void *key, LLVMMetadataRef va } } +gb_internal void lb_add_raddbg_string(lbModule *m, String const &str) { + mpsc_enqueue(&m->gen->raddebug_section_strings, copy_string(permanent_allocator(), str)); +} + +gb_internal void lb_add_raddbg_string(lbModule *m, char const *cstr) { + mpsc_enqueue(&m->gen->raddebug_section_strings, copy_string(permanent_allocator(), make_string_c(cstr))); +} + +gb_internal void lb_add_raddbg_string(lbModule *m, char const *a, char const *b) { + String str = concatenate_strings(permanent_allocator(), make_string_c(a), make_string_c(b)); + mpsc_enqueue(&m->gen->raddebug_section_strings, str); +} + +gb_internal void lb_add_raddbg_string(lbModule *m, char const *a, char const *b, char const *c) { + String str = concatenate3_strings(permanent_allocator(), make_string_c(a), make_string_c(b), make_string_c(c)); + mpsc_enqueue(&m->gen->raddebug_section_strings, str); +} + + gb_internal LLVMMetadataRef lb_get_current_debug_scope(lbProcedure *p) { GB_ASSERT_MSG(p->debug_info != nullptr, "missing debug information for %.*s", LIT(p->name)); @@ -564,22 +583,21 @@ gb_internal LLVMMetadataRef lb_debug_bitfield(lbModule *m, Type *type, String na u64 size_in_bits = 8*type_size_of(bt); u32 align_in_bits = 8*cast(u32)type_align_of(bt); - unsigned element_count = cast(unsigned)bt->BitField.fields.count; - LLVMMetadataRef *elements = gb_alloc_array(permanent_allocator(), LLVMMetadataRef, element_count); + unsigned element_count = cast(unsigned)bt->BitField.fields.count; + LLVMMetadataRef *elements = gb_alloc_array(permanent_allocator(), LLVMMetadataRef, element_count); - u64 offset_in_bits = 0; - for (unsigned i = 0; i < element_count; i++) { - Entity *f = bt->BitField.fields[i]; - u8 bit_size = bt->BitField.bit_sizes[i]; - GB_ASSERT(f->kind == Entity_Variable); - String name = f->token.string; - elements[i] = LLVMDIBuilderCreateBitFieldMemberType(m->debug_builder, scope, cast(char const *)name.text, name.len, file, line, - bit_size, offset_in_bits, 0, - LLVMDIFlagZero, lb_debug_type(m, f->type) - ); - - offset_in_bits += bit_size; - } + u64 offset_in_bits = 0; + for (unsigned i = 0; i < element_count; i++) { + Entity *f = bt->BitField.fields[i]; + u8 bit_size = bt->BitField.bit_sizes[i]; + GB_ASSERT(f->kind == Entity_Variable); + String name = f->token.string; + elements[i] = LLVMDIBuilderCreateBitFieldMemberType(m->debug_builder, scope, cast(char const *)name.text, name.len, file, line, + bit_size, offset_in_bits, 0, + LLVMDIFlagZero, lb_debug_type(m, f->type) + ); + offset_in_bits += bit_size; + } LLVMMetadataRef final_decl = LLVMDIBuilderCreateStructType( m->debug_builder, scope, @@ -924,6 +942,7 @@ gb_internal LLVMMetadataRef lb_debug_type_internal(lbModule *m, Type *type) { } case Type_Matrix: { + #if 0 LLVMMetadataRef subscripts[1] = {}; subscripts[0] = LLVMDIBuilderGetOrCreateSubrange(m->debug_builder, 0ll, @@ -935,6 +954,66 @@ gb_internal LLVMMetadataRef lb_debug_type_internal(lbModule *m, Type *type) { 8*cast(unsigned)type_align_of(type), lb_debug_type(m, type->Matrix.elem), subscripts, gb_count_of(subscripts)); + #else + LLVMMetadataRef subscripts[2] = {}; + subscripts[0] = LLVMDIBuilderGetOrCreateSubrange(m->debug_builder, 0ll, type->Matrix.row_count); + subscripts[1] = LLVMDIBuilderGetOrCreateSubrange(m->debug_builder, 0ll, type->Matrix.column_count); + + LLVMMetadataRef scope = nullptr; + LLVMMetadataRef array_type = nullptr; + + uint64_t size_in_bits = 8*cast(uint64_t)(type_size_of(type)); + unsigned align_in_bits = 8*cast(unsigned)(type_align_of(type)); + + if (type->Matrix.is_row_major) { + LLVMMetadataRef base = LLVMDIBuilderCreateArrayType(m->debug_builder, + 8*cast(uint64_t)(type_size_of(type->Matrix.elem) * type->Matrix.column_count), + 8*cast(unsigned)type_align_of(type->Matrix.elem), + lb_debug_type(m, type->Matrix.elem), + subscripts+1, 1); + array_type = LLVMDIBuilderCreateArrayType(m->debug_builder, + size_in_bits, + align_in_bits, + base, + subscripts+0, 1); + } else { + LLVMMetadataRef base = LLVMDIBuilderCreateArrayType(m->debug_builder, + 8*cast(uint64_t)(type_size_of(type->Matrix.elem) * type->Matrix.row_count), + 8*cast(unsigned)type_align_of(type->Matrix.elem), + lb_debug_type(m, type->Matrix.elem), + subscripts+0, 1); + array_type = LLVMDIBuilderCreateArrayType(m->debug_builder, + size_in_bits, + align_in_bits, + base, + subscripts+1, 1); + } + + LLVMMetadataRef elements[1] = {}; + elements[0] = LLVMDIBuilderCreateMemberType(m->debug_builder, scope, + "data", 4, + nullptr, 0, + size_in_bits, align_in_bits, 0, LLVMDIFlagZero, + array_type + ); + + gbString name = temp_canonical_string(type); + + LLVMMetadataRef final_decl = LLVMDIBuilderCreateStructType( + m->debug_builder, scope, + name, gb_string_length(name), + nullptr, 0, + size_in_bits, align_in_bits, + LLVMDIFlagZero, + nullptr, + elements, 1, + 0, + nullptr, + "", 0 + ); + + return final_decl; + #endif } } @@ -1186,7 +1265,7 @@ gb_internal void lb_add_debug_context_variable(lbProcedure *p, lbAddr const &ctx } -gb_internal String debug_info_mangle_constant_name(Entity *e, gbAllocator const &allocator, bool *did_allocate_) { +gb_internal String lb_debug_info_mangle_constant_name(Entity *e, gbAllocator const &allocator, bool *did_allocate_) { String name = e->token.string; if (e->pkg && e->pkg->name.len > 0) { gbString s = string_canonical_entity_name(allocator, e); @@ -1196,7 +1275,7 @@ gb_internal String debug_info_mangle_constant_name(Entity *e, gbAllocator const return name; } -gb_internal void add_debug_info_global_variable_expr(lbModule *m, String const &name, LLVMMetadataRef dtype, LLVMMetadataRef expr) { +gb_internal void lb_add_debug_info_global_variable_expr(lbModule *m, String const &name, LLVMMetadataRef dtype, LLVMMetadataRef expr) { LLVMMetadataRef scope = nullptr; LLVMMetadataRef file = nullptr; unsigned line = 0; @@ -1212,20 +1291,20 @@ gb_internal void add_debug_info_global_variable_expr(lbModule *m, String const & expr, decl, 8/*AlignInBits*/); } -gb_internal void add_debug_info_for_global_constant_internal_i64(lbModule *m, Entity *e, LLVMMetadataRef dtype, i64 v) { +gb_internal void lb_add_debug_info_for_global_constant_internal_i64(lbModule *m, Entity *e, LLVMMetadataRef dtype, i64 v) { LLVMMetadataRef expr = LLVMDIBuilderCreateConstantValueExpression(m->debug_builder, v); TEMPORARY_ALLOCATOR_GUARD(); - String name = debug_info_mangle_constant_name(e, temporary_allocator(), nullptr); + String name = lb_debug_info_mangle_constant_name(e, temporary_allocator(), nullptr); - add_debug_info_global_variable_expr(m, name, dtype, expr); + lb_add_debug_info_global_variable_expr(m, name, dtype, expr); if ((e->pkg && e->pkg->kind == Package_Init) || (e->scope && (e->scope->flags & ScopeFlag_Global))) { - add_debug_info_global_variable_expr(m, e->token.string, dtype, expr); + lb_add_debug_info_global_variable_expr(m, e->token.string, dtype, expr); } } -gb_internal void add_debug_info_for_global_constant_from_entity(lbGenerator *gen, Entity *e) { +gb_internal void lb_add_debug_info_for_global_constant_from_entity(lbGenerator *gen, Entity *e) { if (e == nullptr || e->kind != Entity_Constant) { return; } @@ -1256,14 +1335,14 @@ gb_internal void add_debug_info_for_global_constant_from_entity(lbGenerator *gen dtype = lb_debug_type(m, e->type); } - add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); + lb_add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); } } else if (is_type_rune(e->type)) { ExactValue const &value = e->Constant.value; if (value.kind == ExactValue_Integer) { LLVMMetadataRef dtype = lb_debug_type(m, t_rune); i64 v = exact_value_to_i64(value); - add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); + lb_add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); } } else if (is_type_boolean(e->type)) { ExactValue const &value = e->Constant.value; @@ -1271,7 +1350,7 @@ gb_internal void add_debug_info_for_global_constant_from_entity(lbGenerator *gen LLVMMetadataRef dtype = lb_debug_type(m, default_type(e->type)); i64 v = cast(i64)value.value_bool; - add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); + lb_add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); } } else if (is_type_enum(e->type)) { ExactValue const &value = e->Constant.value; @@ -1284,14 +1363,70 @@ gb_internal void add_debug_info_for_global_constant_from_entity(lbGenerator *gen v = cast(i64)exact_value_to_u64(value); } - add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); + lb_add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); } } else if (is_type_pointer(e->type)) { ExactValue const &value = e->Constant.value; if (value.kind == ExactValue_Integer) { LLVMMetadataRef dtype = lb_debug_type(m, default_type(e->type)); i64 v = cast(i64)exact_value_to_u64(value); - add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); + lb_add_debug_info_for_global_constant_internal_i64(m, e, dtype, v); } } } + +gb_internal void lb_add_debug_label(lbProcedure *p, Ast *label, lbBlock *target) { +// NOTE(tf2spi): LLVM-C DILabel API used only existed for major versions 20+ +#if LLVM_VERSION_MAJOR >= 20 + if (p == nullptr || p->debug_info == nullptr) { + return; + } + if (target == nullptr || label == nullptr || label->kind != Ast_Label) { + return; + } + Token label_token = label->Label.token; + if (is_blank_ident(label_token.string)) { + return; + } + lbModule *m = p->module; + if (m == nullptr) { + return; + } + + AstFile *file = label->file(); + LLVMMetadataRef llvm_file = lb_get_llvm_metadata(m, file); + if (llvm_file == nullptr) { + debugf("llvm file not found for label\n"); + return; + } + LLVMMetadataRef llvm_scope = p->debug_info; + if(llvm_scope == nullptr) { + debugf("llvm scope not found for label\n"); + return; + } + LLVMMetadataRef llvm_debug_loc = lb_debug_location_from_token_pos(p, label_token.pos); + LLVMBasicBlockRef llvm_block = target->block; + if (llvm_block == nullptr || llvm_debug_loc == nullptr) { + return; + } + LLVMMetadataRef llvm_label = LLVMDIBuilderCreateLabel( + m->debug_builder, + llvm_scope, + (const char *)label_token.string.text, + (size_t)label_token.string.len, + llvm_file, + label_token.pos.line, + + // NOTE(tf2spi): Defaults to false in LLVM API, but I'd rather not take chances + // Always preserve the label no matter what when debugging + true + ); + GB_ASSERT(llvm_label != nullptr); + (void)LLVMDIBuilderInsertLabelAtEnd( + m->debug_builder, + llvm_label, + llvm_debug_loc, + llvm_block + ); +#endif +} diff --git a/src/llvm_backend_expr.cpp b/src/llvm_backend_expr.cpp index 0c82180ec..e17d958d7 100644 --- a/src/llvm_backend_expr.cpp +++ b/src/llvm_backend_expr.cpp @@ -222,6 +222,7 @@ gb_internal lbValue lb_emit_unary_arith(lbProcedure *p, TokenKind op, lbValue x, return lb_emit_byte_swap(p, res, type); } + Type* bt = base_type(type); lbValue res = {}; switch (op) { @@ -233,6 +234,8 @@ gb_internal lbValue lb_emit_unary_arith(lbProcedure *p, TokenKind op, lbValue x, case Token_Sub: // Number negation if (is_type_integer(x.type)) { res.value = LLVMBuildNeg(p->builder, x.value, ""); + } else if (bt->kind == Type_Enum && is_type_integer(bt->Enum.base_type)) { + res.value = LLVMBuildNeg(p->builder, x.value, ""); } else if (is_type_float(x.type)) { res.value = LLVMBuildFNeg(p->builder, x.value, ""); } else if (is_type_complex(x.type)) { @@ -2352,7 +2355,7 @@ gb_internal lbValue lb_emit_conv(lbProcedure *p, lbValue value, Type *t) { Type *elem = base_array_type(dst); lbValue e = lb_emit_conv(p, value, elem); lbAddr v = lb_add_local_generated(p, t, false); - lbValue zero = lb_const_value(p->module, elem, exact_value_i64(0), true); + lbValue zero = lb_const_value(p->module, elem, exact_value_i64(0), LB_CONST_CONTEXT_DEFAULT_ALLOW_LOCAL); for (i64 j = 0; j < dst->Matrix.column_count; j++) { for (i64 i = 0; i < dst->Matrix.row_count; i++) { lbValue ptr = lb_emit_matrix_epi(p, v.addr, i, j); @@ -2389,7 +2392,7 @@ gb_internal lbValue lb_emit_conv(lbProcedure *p, lbValue value, Type *t) { lb_emit_store(p, d, s); } else if (i == j) { lbValue d = lb_emit_matrix_epi(p, v.addr, i, j); - lbValue s = lb_const_value(p->module, dst->Matrix.elem, exact_value_i64(1), true); + lbValue s = lb_const_value(p->module, dst->Matrix.elem, exact_value_i64(1), LB_CONST_CONTEXT_DEFAULT_ALLOW_LOCAL); lb_emit_store(p, d, s); } } @@ -2514,7 +2517,7 @@ gb_internal lbValue lb_emit_c_vararg(lbProcedure *p, lbValue arg, Type *type) { } gb_internal lbValue lb_compare_records(lbProcedure *p, TokenKind op_kind, lbValue left, lbValue right, Type *type) { - GB_ASSERT((is_type_struct(type) || is_type_union(type)) && is_type_comparable(type)); + GB_ASSERT((is_type_struct(type) || is_type_soa_pointer(type) || is_type_union(type)) && is_type_comparable(type)); lbValue left_ptr = lb_address_from_load_or_generate_local(p, left); lbValue right_ptr = lb_address_from_load_or_generate_local(p, right); lbValue res = {}; @@ -2899,6 +2902,7 @@ gb_internal lbValue lb_emit_comp(lbProcedure *p, TokenKind op_kind, lbValue left is_type_proc(a) || is_type_enum(a)) { LLVMIntPredicate pred = {}; + if (is_type_unsigned(left.type)) { switch (op_kind) { case Token_Gt: pred = LLVMIntUGT; break; @@ -2941,7 +2945,7 @@ gb_internal lbValue lb_emit_comp(lbProcedure *p, TokenKind op_kind, lbValue left case Token_GtEq: pred = LLVMRealOGE; break; case Token_Lt: pred = LLVMRealOLT; break; case Token_LtEq: pred = LLVMRealOLE; break; - case Token_NotEq: pred = LLVMRealONE; break; + case Token_NotEq: pred = LLVMRealUNE; break; } if (is_type_different_to_arch_endianness(left.type)) { @@ -2969,7 +2973,7 @@ gb_internal lbValue lb_emit_comp(lbProcedure *p, TokenKind op_kind, lbValue left LLVMRealPredicate pred = {}; switch (op_kind) { case Token_CmpEq: pred = LLVMRealOEQ; break; - case Token_NotEq: pred = LLVMRealONE; break; + case Token_NotEq: pred = LLVMRealUNE; break; } mask = LLVMBuildFCmp(p->builder, pred, left.value, right.value, ""); } else { @@ -3022,6 +3026,9 @@ gb_internal lbValue lb_emit_comp(lbProcedure *p, TokenKind op_kind, lbValue left return res; + } else if (is_type_soa_pointer(a)) { + // NOTE(Jeroen): Compare data pointer and index tag as if it were a simple struct. + return lb_compare_records(p, op_kind, left, right, a); } else { GB_PANIC("Unhandled comparison kind %s (%s) %.*s %s (%s)", type_to_string(left.type), type_to_string(base_type(left.type)), LIT(token_strings[op_kind]), type_to_string(right.type), type_to_string(base_type(right.type))); } @@ -3143,6 +3150,18 @@ gb_internal lbValue lb_emit_comp_against_nil(lbProcedure *p, TokenKind op_kind, } } break; + + case Type_SoaPointer: + { + // NOTE(bill): An SoaPointer is essentially just a pointer for nil comparison + lbValue ptr = lb_emit_struct_ev(p, x, 0); // Extract the base pointer component (field 0) + if (op_kind == Token_CmpEq) { + res.value = LLVMBuildIsNull(p->builder, ptr.value, ""); + } else if (op_kind == Token_NotEq) { + res.value = LLVMBuildIsNotNull(p->builder, ptr.value, ""); + } + return res; + } case Type_Union: { @@ -3493,8 +3512,7 @@ gb_internal lbValue lb_build_expr_internal(lbProcedure *p, Ast *expr) { if (tv.value.kind != ExactValue_Invalid) { // NOTE(bill): Short on constant values - bool allow_local = true; - return lb_const_value(p->module, type, tv.value, allow_local); + return lb_const_value(p->module, type, tv.value, LB_CONST_CONTEXT_DEFAULT_ALLOW_LOCAL); } else if (tv.mode == Addressing_Type) { // NOTE(bill, 2023-01-16): is this correct? I hope so at least return lb_typeid(m, tv.type); @@ -4826,7 +4844,7 @@ gb_internal lbAddr lb_build_addr_compound_lit(lbProcedure *p, Ast *expr) { if (cl->elems.count == 0) { break; } - GB_ASSERT(expr->file()->feature_flags & OptInFeatureFlag_DynamicLiterals); + GB_ASSERT(expr->file()->feature_flags & OptInFeatureFlag_DynamicLiterals || build_context.dynamic_literals); lbValue err = lb_dynamic_map_reserve(p, v.addr, 2*cl->elems.count, pos); gb_unused(err); @@ -5136,8 +5154,6 @@ gb_internal lbAddr lb_build_addr_internal(lbProcedure *p, Ast *expr) { return lb_build_addr(p, unparen_expr(se->selector)); } - - Type *type = base_type(tav.type); if (tav.mode == Addressing_Type) { // Addressing_Type Selection sel = lookup_field(tav.type, selector, true); if (sel.pseudo_field) { @@ -5172,18 +5188,29 @@ gb_internal lbAddr lb_build_addr_internal(lbProcedure *p, Ast *expr) { return lb_addr_swizzle(a, type, swizzle_count, swizzle_indices); } - Selection sel = lookup_field(type, selector, false); + Selection sel = lookup_field(tav.type, selector, false); GB_ASSERT(sel.entity != nullptr); - if (sel.pseudo_field) { - GB_ASSERT(sel.entity->kind == Entity_Procedure || sel.entity->kind == Entity_ProcGroup); + if (sel.pseudo_field && (sel.entity->kind == Entity_Procedure || sel.entity->kind == Entity_ProcGroup)) { Entity *e = entity_of_node(sel_node); GB_ASSERT(e->kind == Entity_Procedure); return lb_addr(lb_find_value_from_entity(p->module, e)); } - if (sel.is_bit_field) { - lbAddr addr = lb_build_addr(p, se->expr); + lbAddr addr = lb_build_addr(p, se->expr); + // NOTE(harold): Only allow ivar pseudo field access on indirect selectors. + // It is incoherent otherwise as Objective-C objects are zero-sized. + Type *deref_type = type_deref(tav.type); + if (tav.type->kind == Type_Pointer && deref_type->kind == Type_Named && deref_type->Named.type_name->TypeName.objc_ivar) { + // NOTE(harold): We need to load the ivar from the current address and + // replace addr with the loaded ivar addr to apply the selector load properly. + addr = lb_addr(lb_emit_load(p, addr.addr)); + + lbValue ivar_ptr = lb_handle_objc_ivar_for_objc_object_pointer(p, addr.addr); + addr = lb_addr(ivar_ptr); + } + + if (sel.is_bit_field) { Selection sub_sel = sel; sub_sel.index.count -= 1; @@ -5209,7 +5236,6 @@ gb_internal lbAddr lb_build_addr_internal(lbProcedure *p, Ast *expr) { } { - lbAddr addr = lb_build_addr(p, se->expr); if (addr.kind == lbAddr_Map) { lbValue v = lb_addr_load(p, addr); lbValue a = lb_address_from_load_or_generate_local(p, v); diff --git a/src/llvm_backend_general.cpp b/src/llvm_backend_general.cpp index ce2c70661..3a099ec55 100644 --- a/src/llvm_backend_general.cpp +++ b/src/llvm_backend_general.cpp @@ -101,6 +101,7 @@ gb_internal void lb_init_module(lbModule *m, Checker *c) { string_map_init(&m->objc_classes); string_map_init(&m->objc_selectors); + string_map_init(&m->objc_ivars); map_init(&m->map_info_map, 0); map_init(&m->map_cell_info_map, 0); @@ -173,7 +174,9 @@ gb_internal bool lb_init_generator(lbGenerator *gen, Checker *c) { mpsc_init(&gen->entities_to_correct_linkage, heap_allocator()); mpsc_init(&gen->objc_selectors, heap_allocator()); mpsc_init(&gen->objc_classes, heap_allocator()); - + mpsc_init(&gen->objc_ivars, heap_allocator()); + mpsc_init(&gen->raddebug_section_strings, heap_allocator()); + return true; } @@ -356,7 +359,7 @@ gb_internal LLVMValueRef llvm_const_insert_value(lbModule *m, LLVMValueRef agg, gb_internal LLVMValueRef llvm_cstring(lbModule *m, String const &str) { - lbValue v = lb_find_or_add_entity_string(m, str); + lbValue v = lb_find_or_add_entity_string(m, str, false); unsigned indices[1] = {0}; return llvm_const_extract_value(m, v.value, indices, gb_count_of(indices)); } @@ -568,7 +571,7 @@ gb_internal void lb_set_file_line_col(lbProcedure *p, Array arr, TokenP col = obfuscate_i32(col); } - arr[0] = lb_find_or_add_entity_string(p->module, file); + arr[0] = lb_find_or_add_entity_string(p->module, file, false); arr[1] = lb_const_int(p->module, t_i32, line); arr[2] = lb_const_int(p->module, t_i32, col); } @@ -885,8 +888,8 @@ gb_internal void lb_addr_store(lbProcedure *p, lbAddr addr, lbValue value) { Type *t = base_type(type_deref(addr.addr.type)); GB_ASSERT(t->kind == Type_Struct && t->Struct.soa_kind != StructSoa_None); lbValue len = lb_soa_struct_len(p, addr.addr); - if (addr.soa.index_expr != nullptr) { - lb_emit_bounds_check(p, ast_token(addr.soa.index_expr), index, len); + if (addr.soa.index_expr != nullptr && (!lb_is_const(addr.soa.index) || t->Struct.soa_kind != StructSoa_Fixed)) { + lb_emit_bounds_check(p, ast_token(addr.soa.index_expr), addr.soa.index, len); } } @@ -2212,6 +2215,14 @@ gb_internal LLVMTypeRef lb_type_internal(lbModule *m, Type *type) { case Type_BitField: return lb_type_internal(m, type->BitField.backing_type); + + case Type_Generic: + if (type->Generic.specialized) { + return lb_type_internal(m, type->Generic.specialized); + } else { + // For unspecialized generics, use a pointer type as a placeholder + return LLVMPointerType(LLVMInt8TypeInContext(m->ctx), 0); + } } GB_PANIC("Invalid type %s", type_to_string(type)); @@ -2377,6 +2388,29 @@ gb_internal void lb_add_attribute_to_proc_with_string(lbModule *m, LLVMValueRef } +gb_internal bool lb_apply_thread_local_model(LLVMValueRef value, String model) { + if (model != "") { + LLVMSetThreadLocal(value, true); + + LLVMThreadLocalMode mode = LLVMGeneralDynamicTLSModel; + if (model == "default") { + mode = LLVMGeneralDynamicTLSModel; + } else if (model == "localdynamic") { + mode = LLVMLocalDynamicTLSModel; + } else if (model == "initialexec") { + mode = LLVMInitialExecTLSModel; + } else if (model == "localexec") { + mode = LLVMLocalExecTLSModel; + } else { + GB_PANIC("Unhandled thread local mode %.*s", LIT(model)); + } + LLVMSetThreadLocalMode(value, mode); + return true; + } + + return false; +} + gb_internal void lb_add_edge(lbBlock *from, lbBlock *to) { LLVMValueRef instr = LLVMGetLastInstruction(from->block); @@ -2515,10 +2549,13 @@ general_end:; } } - src_size = align_formula(src_size, src_align); - dst_size = align_formula(dst_size, dst_align); + // NOTE(laytan): even though this logic seems sound, the Address Sanitizer does not + // want you to load/store the space of a value that is there for alignment. +#if 0 + i64 aligned_src_size = align_formula(src_size, src_align); + i64 aligned_dst_size = align_formula(dst_size, dst_align); - if (LLVMIsALoadInst(val) && (src_size >= dst_size && src_align >= dst_align)) { + if (LLVMIsALoadInst(val) && (aligned_src_size >= aligned_dst_size && src_align >= dst_align)) { LLVMValueRef val_ptr = LLVMGetOperand(val, 0); val_ptr = LLVMBuildPointerCast(p->builder, val_ptr, LLVMPointerType(dst_type, 0), ""); LLVMValueRef loaded_val = OdinLLVMBuildLoad(p, dst_type, val_ptr); @@ -2526,8 +2563,57 @@ general_end:; // LLVMSetAlignment(loaded_val, gb_min(src_align, dst_align)); return loaded_val; + } +#endif + + if (src_size > dst_size) { + GB_ASSERT(p->decl_block != p->curr_block); + // NOTE(laytan): src is bigger than dst, need to memcpy the part of src we want. + + LLVMValueRef val_ptr; + if (LLVMIsALoadInst(val)) { + val_ptr = LLVMGetOperand(val, 0); + } else if (LLVMIsAAllocaInst(val)) { + val_ptr = LLVMBuildPointerCast(p->builder, val, LLVMPointerType(src_type, 0), ""); + } else { + // NOTE(laytan): we need a pointer to memcpy from. + LLVMValueRef val_copy = llvm_alloca(p, src_type, src_align); + val_ptr = LLVMBuildPointerCast(p->builder, val_copy, LLVMPointerType(src_type, 0), ""); + LLVMBuildStore(p->builder, val, val_ptr); + } + + i64 max_align = gb_max(lb_alignof(src_type), lb_alignof(dst_type)); + max_align = gb_max(max_align, 16); + + LLVMValueRef ptr = llvm_alloca(p, dst_type, max_align); + LLVMValueRef nptr = LLVMBuildPointerCast(p->builder, ptr, LLVMPointerType(dst_type, 0), ""); + + LLVMTypeRef types[3] = { + lb_type(p->module, t_rawptr), + lb_type(p->module, t_rawptr), + lb_type(p->module, t_int) + }; + + LLVMValueRef args[4] = { + nptr, + val_ptr, + LLVMConstInt(LLVMIntTypeInContext(p->module->ctx, 8*cast(unsigned)build_context.int_size), dst_size, 0), + LLVMConstInt(LLVMInt1TypeInContext(p->module->ctx), 0, 0), + }; + + lb_call_intrinsic( + p, + "llvm.memcpy.inline", + args, + gb_count_of(args), + types, + gb_count_of(types) + ); + + return OdinLLVMBuildLoad(p, dst_type, ptr); } else { GB_ASSERT(p->decl_block != p->curr_block); + GB_ASSERT(dst_size >= src_size); i64 max_align = gb_max(lb_alignof(src_type), lb_alignof(dst_type)); max_align = gb_max(max_align, 16); @@ -2543,9 +2629,14 @@ general_end:; -gb_internal LLVMValueRef lb_find_or_add_entity_string_ptr(lbModule *m, String const &str) { - StringHashKey key = string_hash_string(str); - LLVMValueRef *found = string_map_get(&m->const_strings, key); +gb_internal LLVMValueRef lb_find_or_add_entity_string_ptr(lbModule *m, String const &str, bool custom_link_section) { + StringHashKey key = {}; + LLVMValueRef *found = nullptr; + + if (!custom_link_section) { + key = string_hash_string(str); + found = string_map_get(&m->const_strings, key); + } if (found != nullptr) { return *found; } else { @@ -2568,15 +2659,17 @@ gb_internal LLVMValueRef lb_find_or_add_entity_string_ptr(lbModule *m, String co LLVMSetAlignment(global_data, 1); LLVMValueRef ptr = LLVMConstInBoundsGEP2(type, global_data, indices, 2); - string_map_set(&m->const_strings, key, ptr); + if (!custom_link_section) { + string_map_set(&m->const_strings, key, ptr); + } return ptr; } } -gb_internal lbValue lb_find_or_add_entity_string(lbModule *m, String const &str) { +gb_internal lbValue lb_find_or_add_entity_string(lbModule *m, String const &str, bool custom_link_section) { LLVMValueRef ptr = nullptr; if (str.len != 0) { - ptr = lb_find_or_add_entity_string_ptr(m, str); + ptr = lb_find_or_add_entity_string_ptr(m, str, custom_link_section); } else { ptr = LLVMConstNull(lb_type(m, t_u8_ptr)); } @@ -2721,6 +2814,14 @@ gb_internal lbValue lb_find_procedure_value_from_entity(lbModule *m, Entity *e) ignore_body = other_module != m; lbProcedure *missing_proc = lb_create_procedure(m, e, ignore_body); + if (missing_proc == nullptr) { + // This is an unspecialized polymorphic procedure, which should not be codegen'd + lbValue dummy = {}; + dummy.value = nullptr; + dummy.type = nullptr; + return dummy; + } + if (ignore_body) { mutex_lock(&gen->anonymous_proc_lits_mutex); defer (mutex_unlock(&gen->anonymous_proc_lits_mutex)); @@ -2913,25 +3014,7 @@ gb_internal lbValue lb_find_value_from_entity(lbModule *m, Entity *e) { lb_set_entity_from_other_modules_linkage_correctly(other_module, e, name); - if (e->Variable.thread_local_model != "") { - LLVMSetThreadLocal(g.value, true); - - String m = e->Variable.thread_local_model; - LLVMThreadLocalMode mode = LLVMGeneralDynamicTLSModel; - if (m == "default") { - mode = LLVMGeneralDynamicTLSModel; - } else if (m == "localdynamic") { - mode = LLVMLocalDynamicTLSModel; - } else if (m == "initialexec") { - mode = LLVMInitialExecTLSModel; - } else if (m == "localexec") { - mode = LLVMLocalExecTLSModel; - } else { - GB_PANIC("Unhandled thread local mode %.*s", LIT(m)); - } - LLVMSetThreadLocalMode(g.value, mode); - } - + lb_apply_thread_local_model(g.value, e->Variable.thread_local_model); return g; } @@ -3063,6 +3146,13 @@ gb_internal lbAddr lb_add_local(lbProcedure *p, Type *type, Entity *e, bool zero if (e != nullptr) { lb_add_entity(p->module, e, val); lb_add_debug_local_variable(p, ptr, type, e->token); + + // NOTE(lucas): In LLVM 20 and below we do not have the option to have asan cleanup poisoned stack + // locals ourselves. So we need to manually track and unpoison these locals on proc return. + // LLVM 21 adds the 'use-after-scope' asan option which does this for us. + if (build_context.sanitizer_flags & SanitizerFlag_Address && !p->entity->Procedure.no_sanitize_address) { + array_add(&p->asan_stack_locals, val); + } } if (zero_init) { diff --git a/src/llvm_backend_opt.cpp b/src/llvm_backend_opt.cpp index 7fe1359b4..8d5cfcb70 100644 --- a/src/llvm_backend_opt.cpp +++ b/src/llvm_backend_opt.cpp @@ -516,7 +516,7 @@ gb_internal void llvm_delete_function(LLVMValueRef func) { LLVMDeleteFunction(func); } -gb_internal void lb_append_to_compiler_used(lbModule *m, LLVMValueRef func) { +gb_internal void lb_append_to_compiler_used(lbModule *m, LLVMValueRef value) { LLVMValueRef global = LLVMGetNamedGlobal(m->mod, "llvm.compiler.used"); LLVMValueRef *constants; @@ -544,7 +544,7 @@ gb_internal void lb_append_to_compiler_used(lbModule *m, LLVMValueRef func) { LLVMTypeRef Int8PtrTy = LLVMPointerType(LLVMInt8TypeInContext(m->ctx), 0); LLVMTypeRef ATy = llvm_array_type(Int8PtrTy, operands); - constants[operands - 1] = LLVMConstBitCast(func, Int8PtrTy); + constants[operands - 1] = LLVMConstBitCast(value, Int8PtrTy); LLVMValueRef initializer = LLVMConstArray(Int8PtrTy, constants, operands); global = LLVMAddGlobal(m->mod, ATy, "llvm.compiler.used"); diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index 304db75bc..f51ed2b4d 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -67,6 +67,14 @@ gb_internal void lb_mem_copy_non_overlapping(lbProcedure *p, lbValue dst, lbValu gb_internal lbProcedure *lb_create_procedure(lbModule *m, Entity *entity, bool ignore_body) { GB_ASSERT(entity != nullptr); GB_ASSERT(entity->kind == Entity_Procedure); + // Skip codegen for unspecialized polymorphic procedures + if (is_type_polymorphic(entity->type) && !entity->Procedure.is_foreign) { + Type *bt = base_type(entity->type); + if (bt->kind == Type_Proc && bt->Proc.is_polymorphic && !bt->Proc.is_poly_specialized) { + // Do not generate code for unspecialized polymorphic procedures + return nullptr; + } + } if (!entity->Procedure.is_foreign) { if ((entity->flags & EntityFlag_ProcBodyChecked) == 0) { GB_PANIC("%.*s :: %s (was parapoly: %d %d)", LIT(entity->token.string), type_to_string(entity->type), is_type_polymorphic(entity->type, true), is_type_polymorphic(entity->type, false)); @@ -115,12 +123,13 @@ gb_internal lbProcedure *lb_create_procedure(lbModule *m, Entity *entity, bool i p->is_entry_point = false; gbAllocator a = heap_allocator(); - p->children.allocator = a; - p->defer_stmts.allocator = a; - p->blocks.allocator = a; - p->branch_blocks.allocator = a; - p->context_stack.allocator = a; - p->scope_stack.allocator = a; + p->children.allocator = a; + p->defer_stmts.allocator = a; + p->blocks.allocator = a; + p->branch_blocks.allocator = a; + p->context_stack.allocator = a; + p->scope_stack.allocator = a; + p->asan_stack_locals.allocator = a; // map_init(&p->selector_values, 0); // map_init(&p->selector_addr, 0); // map_init(&p->tuple_fix_map, 0); @@ -333,7 +342,7 @@ gb_internal lbProcedure *lb_create_procedure(lbModule *m, Entity *entity, bool i } if (p->body && entity->pkg && ((entity->pkg->kind == Package_Normal) || (entity->pkg->kind == Package_Init))) { - if (build_context.sanitizer_flags & SanitizerFlag_Address) { + if (build_context.sanitizer_flags & SanitizerFlag_Address && !entity->Procedure.no_sanitize_address) { lb_add_attribute_to_proc(m, p->value, "sanitize_address"); } if (build_context.sanitizer_flags & SanitizerFlag_Memory) { @@ -385,11 +394,12 @@ gb_internal lbProcedure *lb_create_dummy_procedure(lbModule *m, String link_name p->is_entry_point = false; gbAllocator a = permanent_allocator(); - p->children.allocator = a; - p->defer_stmts.allocator = a; - p->blocks.allocator = a; - p->branch_blocks.allocator = a; - p->context_stack.allocator = a; + p->children.allocator = a; + p->defer_stmts.allocator = a; + p->blocks.allocator = a; + p->branch_blocks.allocator = a; + p->context_stack.allocator = a; + p->asan_stack_locals.allocator = a; map_init(&p->tuple_fix_map, 0); @@ -781,8 +791,7 @@ gb_internal void lb_end_procedure_body(lbProcedure *p) { p->curr_block = nullptr; p->state_flags = 0; -} -gb_internal void lb_end_procedure(lbProcedure *p) { + LLVMDisposeBuilder(p->builder); } @@ -815,6 +824,10 @@ gb_internal void lb_build_nested_proc(lbProcedure *p, AstProcLit *pd, Entity *e) e->Procedure.link_name = name; lbProcedure *nested_proc = lb_create_procedure(p->module, e); + if (nested_proc == nullptr) { + // This is an unspecialized polymorphic procedure, skip codegen + return; + } e->code_gen_procedure = nested_proc; lbValue value = {}; @@ -1293,6 +1306,23 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn lbValue res = {}; res.type = tv.type; + switch (builtin_id) { + case BuiltinProc_simd_indices: { + Type *type = base_type(res.type); + GB_ASSERT(type->kind == Type_SimdVector); + Type *elem = type->SimdVector.elem; + + i64 count = type->SimdVector.count; + LLVMValueRef *scalars = gb_alloc_array(temporary_allocator(), LLVMValueRef, count); + for (i64 i = 0; i < count; i++) { + scalars[i] = lb_const_value(m, elem, exact_value_i64(i)).value; + } + + res.value = LLVMConstVector(scalars, cast(unsigned)count); + return res; + } + } + lbValue arg0 = {}; if (ce->args.count > 0) arg0 = lb_build_expr(p, ce->args[0]); lbValue arg1 = {}; if (ce->args.count > 1) arg1 = lb_build_expr(p, ce->args[1]); lbValue arg2 = {}; if (ce->args.count > 2) arg2 = lb_build_expr(p, ce->args[2]); @@ -1442,7 +1472,7 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn LLVMRealPredicate pred = cast(LLVMRealPredicate)0; switch (builtin_id) { case BuiltinProc_simd_lanes_eq: pred = LLVMRealOEQ; break; - case BuiltinProc_simd_lanes_ne: pred = LLVMRealONE; break; + case BuiltinProc_simd_lanes_ne: pred = LLVMRealUNE; break; case BuiltinProc_simd_lanes_lt: pred = LLVMRealOLT; break; case BuiltinProc_simd_lanes_le: pred = LLVMRealOLE; break; case BuiltinProc_simd_lanes_gt: pred = LLVMRealOGT; break; @@ -1478,6 +1508,38 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn res.value = LLVMBuildInsertElement(p->builder, arg0.value, arg2.value, arg1.value, ""); return res; + case BuiltinProc_simd_reduce_add_bisect: + case BuiltinProc_simd_reduce_mul_bisect: + { + GB_ASSERT(arg0.type->kind == Type_SimdVector); + i64 num_elems = arg0.type->SimdVector.count; + + LLVMValueRef *indices = gb_alloc_array(temporary_allocator(), LLVMValueRef, num_elems); + for (i64 i = 0; i < num_elems; i++) { + indices[i] = lb_const_int(m, t_uint, cast(u64)i).value; + } + + switch (builtin_id) { + case BuiltinProc_simd_reduce_add_bisect: op_code = is_float ? LLVMFAdd : LLVMAdd; break; + case BuiltinProc_simd_reduce_mul_bisect: op_code = is_float ? LLVMFMul : LLVMMul; break; + } + + LLVMValueRef remaining = arg0.value; + i64 num_remaining = num_elems; + + while (num_remaining > 1) { + num_remaining /= 2; + LLVMValueRef left_indices = LLVMConstVector(&indices[0], cast(unsigned)num_remaining); + LLVMValueRef left_value = LLVMBuildShuffleVector(p->builder, remaining, remaining, left_indices, ""); + LLVMValueRef right_indices = LLVMConstVector(&indices[num_remaining], cast(unsigned)num_remaining); + LLVMValueRef right_value = LLVMBuildShuffleVector(p->builder, remaining, remaining, right_indices, ""); + remaining = LLVMBuildBinOp(p->builder, op_code, left_value, right_value, ""); + } + + res.value = LLVMBuildExtractElement(p->builder, remaining, indices[0], ""); + return res; + } + case BuiltinProc_simd_reduce_add_ordered: case BuiltinProc_simd_reduce_mul_ordered: { @@ -1510,6 +1572,40 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn res.value = lb_call_intrinsic(p, name, args, cast(unsigned)args_count, types, gb_count_of(types)); return res; } + + case BuiltinProc_simd_reduce_add_pairs: + case BuiltinProc_simd_reduce_mul_pairs: + { + GB_ASSERT(arg0.type->kind == Type_SimdVector); + i64 num_elems = arg0.type->SimdVector.count; + + LLVMValueRef *indices = gb_alloc_array(temporary_allocator(), LLVMValueRef, num_elems); + for (i64 i = 0; i < num_elems/2; i++) { + indices[i] = lb_const_int(m, t_uint, cast(u64)(2*i)).value; + indices[i+num_elems/2] = lb_const_int(m, t_uint, cast(u64)(2*i+1)).value; + } + + switch (builtin_id) { + case BuiltinProc_simd_reduce_add_pairs: op_code = is_float ? LLVMFAdd : LLVMAdd; break; + case BuiltinProc_simd_reduce_mul_pairs: op_code = is_float ? LLVMFMul : LLVMMul; break; + } + + LLVMValueRef remaining = arg0.value; + i64 num_remaining = num_elems; + + while (num_remaining > 1) { + num_remaining /= 2; + LLVMValueRef left_indices = LLVMConstVector(&indices[0], cast(unsigned)num_remaining); + LLVMValueRef left_value = LLVMBuildShuffleVector(p->builder, remaining, remaining, left_indices, ""); + LLVMValueRef right_indices = LLVMConstVector(&indices[num_elems/2], cast(unsigned)num_remaining); + LLVMValueRef right_value = LLVMBuildShuffleVector(p->builder, remaining, remaining, right_indices, ""); + remaining = LLVMBuildBinOp(p->builder, op_code, left_value, right_value, ""); + } + + res.value = LLVMBuildExtractElement(p->builder, remaining, indices[0], ""); + return res; + } + case BuiltinProc_simd_reduce_min: case BuiltinProc_simd_reduce_max: case BuiltinProc_simd_reduce_and: @@ -2150,6 +2246,68 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu return lb_emit_load(p, tuple); } + case BuiltinProc_compress_values: { + isize value_count = 0; + for (Ast *arg : ce->args) { + Type *t = arg->tav.type; + if (is_type_tuple(t)) { + value_count += t->Tuple.variables.count; + } else { + value_count += 1; + } + } + + if (value_count == 1) { + lbValue x = lb_build_expr(p, ce->args[0]); + x = lb_emit_conv(p, x, tv.type); + return x; + } + + Type *dt = base_type(tv.type); + lbAddr addr = lb_add_local_generated(p, tv.type, true); + if (is_type_struct(dt) || is_type_tuple(dt)) { + i32 index = 0; + for (Ast *arg : ce->args) { + lbValue x = lb_build_expr(p, arg); + if (is_type_tuple(x.type)) { + for (isize i = 0; i < x.type->Tuple.variables.count; i++) { + lbValue y = lb_emit_tuple_ev(p, x, cast(i32)i); + lbValue ptr = lb_emit_struct_ep(p, addr.addr, index++); + y = lb_emit_conv(p, y, type_deref(ptr.type)); + lb_emit_store(p, ptr, y); + } + } else { + lbValue ptr = lb_emit_struct_ep(p, addr.addr, index++); + x = lb_emit_conv(p, x, type_deref(ptr.type)); + lb_emit_store(p, ptr, x); + } + } + GB_ASSERT(index == value_count); + } else if (is_type_array_like(dt)) { + i32 index = 0; + for (Ast *arg : ce->args) { + lbValue x = lb_build_expr(p, arg); + if (is_type_tuple(x.type)) { + for (isize i = 0; i < x.type->Tuple.variables.count; i++) { + lbValue y = lb_emit_tuple_ev(p, x, cast(i32)i); + lbValue ptr = lb_emit_array_epi(p, addr.addr, index++); + y = lb_emit_conv(p, y, type_deref(ptr.type)); + lb_emit_store(p, ptr, y); + } + } else { + lbValue ptr = lb_emit_array_epi(p, addr.addr, index++); + x = lb_emit_conv(p, x, type_deref(ptr.type)); + lb_emit_store(p, ptr, x); + } + } + GB_ASSERT(index == value_count); + } else { + GB_PANIC("TODO(bill): compress_values -> %s", type_to_string(tv.type)); + } + + return lb_addr_load(p, addr); + } + case BuiltinProc_min: { Type *t = type_of_expr(expr); if (ce->args.count == 2) { @@ -3226,11 +3384,28 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu { GB_ASSERT(arg_count <= 7); - char asm_string[] = "svc #0; cset x8, cc"; - gbString constraints = gb_string_make(heap_allocator(), "={x0},={x8}"); - for (unsigned i = 0; i < arg_count; i++) { - constraints = gb_string_appendc(constraints, ",{"); - static char const *regs[] = { + char const *asm_string; + char const **regs; + gbString constraints; + + if (build_context.metrics.os == TargetOs_netbsd) { + asm_string = "svc #0; cset x17, cc"; + constraints = gb_string_make(heap_allocator(), "={x0},={x17}"); + static char const *_regs[] = { + "x17", + "x0", + "x1", + "x2", + "x3", + "x4", + "x5", + }; + regs = _regs; + } else { + // FreeBSD (tested), OpenBSD (untested). + asm_string = "svc #0; cset x8, cc"; + constraints = gb_string_make(heap_allocator(), "={x0},={x8}"); + static char const *_regs[] = { "x8", "x0", "x1", @@ -3239,13 +3414,19 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu "x4", "x5", }; + regs = _regs; + + // FreeBSD clobbered x1 on a call to sysctl. + constraints = gb_string_appendc(constraints, ",~{x1}"); + } + + for (unsigned i = 0; i < arg_count; i++) { + constraints = gb_string_appendc(constraints, ",{"); constraints = gb_string_appendc(constraints, regs[i]); constraints = gb_string_appendc(constraints, "}"); } - // FreeBSD clobbered x1 on a call to sysctl. - constraints = gb_string_appendc(constraints, ",~{x1},~{cc},~{memory}"); - + constraints = gb_string_appendc(constraints, ",~{cc},~{memory}"); inline_asm = llvm_get_inline_asm(func_type, make_string_c(asm_string), make_string_c(constraints)); } break; @@ -3267,6 +3448,7 @@ gb_internal lbValue lb_build_builtin_proc(lbProcedure *p, Ast *expr, TypeAndValu case BuiltinProc_objc_find_class: return lb_handle_objc_find_class(p, expr); case BuiltinProc_objc_register_selector: return lb_handle_objc_register_selector(p, expr); case BuiltinProc_objc_register_class: return lb_handle_objc_register_class(p, expr); + case BuiltinProc_objc_ivar_get: return lb_handle_objc_ivar_get(p, expr); case BuiltinProc_constant_utf16_cstring: diff --git a/src/llvm_backend_stmt.cpp b/src/llvm_backend_stmt.cpp index 02ed8b092..efd2e4f87 100644 --- a/src/llvm_backend_stmt.cpp +++ b/src/llvm_backend_stmt.cpp @@ -136,7 +136,6 @@ gb_internal lbBranchBlocks lb_lookup_branch_blocks(lbProcedure *p, Ast *ident) { return empty; } - gb_internal lbTargetList *lb_push_target_list(lbProcedure *p, Ast *label, lbBlock *break_, lbBlock *continue_, lbBlock *fallthrough_) { lbTargetList *tl = gb_alloc_item(permanent_allocator(), lbTargetList); tl->prev = p->target_list; @@ -688,6 +687,18 @@ gb_internal void lb_build_range_interval(lbProcedure *p, AstBinaryExpr *node, lbBlock *body = lb_create_block(p, "for.interval.body"); lbBlock *done = lb_create_block(p, "for.interval.done"); + // TODO(tf2spi): This is inlined in more than several places. + // Putting this in a function might be preferred. + // LLVMSetCurrentDebugLocation2 has side effects, + // so I didn't want to hide that before it got reviewed. + if (rs->label != nullptr && p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "for.interval.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, rs->label)); + lb_add_debug_label(p, rs->label, label); + } lb_emit_jump(p, loop); lb_start_block(p, loop); @@ -893,6 +904,14 @@ gb_internal void lb_build_range_stmt_struct_soa(lbProcedure *p, AstRangeStmt *rs lbAddr index = lb_add_local_generated(p, t_int, false); + if (rs->label != nullptr && p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "for.soa.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, rs->label)); + lb_add_debug_label(p, rs->label, label); + } if (!is_reverse) { /* for x, i in array { @@ -970,7 +989,6 @@ gb_internal void lb_build_range_stmt_struct_soa(lbProcedure *p, AstRangeStmt *rs lb_store_range_stmt_val(p, val1, lb_addr_load(p, index)); } - lb_push_target_list(p, rs->label, done, loop, nullptr); lb_build_stmt(p, rs->body); @@ -1029,6 +1047,15 @@ gb_internal void lb_build_range_stmt(lbProcedure *p, AstRangeStmt *rs, Scope *sc lbBlock *done = nullptr; bool is_map = false; + if (rs->label != nullptr && p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "for.range.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, rs->label)); + lb_add_debug_label(p, rs->label, label); + } + if (tav.mode == Addressing_Type) { lb_build_range_enum(p, type_deref(tav.type), val0_type, &val, &key, &loop, &done); } else { @@ -1530,6 +1557,14 @@ gb_internal bool lb_switch_stmt_can_be_trivial_jump_table(AstSwitchStmt *ss, boo gb_internal void lb_build_switch_stmt(lbProcedure *p, AstSwitchStmt *ss, Scope *scope) { lb_open_scope(p, scope); + if (ss->label != nullptr && p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "switch.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, ss->label)); + lb_add_debug_label(p, ss->label, label); + } if (ss->init != nullptr) { lb_build_stmt(p, ss->init); } @@ -1736,6 +1771,7 @@ gb_internal lbAddr lb_store_range_stmt_val(lbProcedure *p, Ast *stmt_val, lbValu gb_internal void lb_type_case_body(lbProcedure *p, Ast *label, Ast *clause, lbBlock *body, lbBlock *done) { ast_node(cc, CaseClause, clause); + // NOTE(tf2spi): Debug info for label not generated here on purpose lb_push_target_list(p, label, done, nullptr, nullptr); lb_build_stmt_list(p, cc->stmts); lb_close_scope(p, lbDeferExit_Default, body, clause); @@ -1963,8 +1999,7 @@ gb_internal void lb_build_static_variables(lbProcedure *p, AstValueDecl *vd) { GB_ASSERT(ast_value->tav.mode == Addressing_Constant || ast_value->tav.mode == Addressing_Invalid); - bool allow_local = false; - value = lb_const_value(p->module, ast_value->tav.type, ast_value->tav.value, allow_local); + value = lb_const_value(p->module, ast_value->tav.type, ast_value->tav.value, LB_CONST_CONTEXT_DEFAULT_NO_LOCAL); } Ast *ident = vd->names[i]; @@ -1985,34 +2020,45 @@ gb_internal void lb_build_static_variables(lbProcedure *p, AstValueDecl *vd) { char *c_name = alloc_cstring(permanent_allocator(), mangled_name); LLVMValueRef global = LLVMAddGlobal(p->module->mod, lb_type(p->module, e->type), c_name); + LLVMSetAlignment(global, cast(u32)type_align_of(e->type)); LLVMSetInitializer(global, LLVMConstNull(lb_type(p->module, e->type))); - if (value.value != nullptr) { - LLVMSetInitializer(global, value.value); - } + if (e->Variable.is_rodata) { LLVMSetGlobalConstant(global, true); } - if (e->Variable.thread_local_model != "") { - LLVMSetThreadLocal(global, true); - String m = e->Variable.thread_local_model; - LLVMThreadLocalMode mode = LLVMGeneralDynamicTLSModel; - if (m == "default") { - mode = LLVMGeneralDynamicTLSModel; - } else if (m == "localdynamic") { - mode = LLVMLocalDynamicTLSModel; - } else if (m == "initialexec") { - mode = LLVMInitialExecTLSModel; - } else if (m == "localexec") { - mode = LLVMLocalExecTLSModel; - } else { - GB_PANIC("Unhandled thread local mode %.*s", LIT(m)); - } - LLVMSetThreadLocalMode(global, mode); - } else { + if (!lb_apply_thread_local_model(global, e->Variable.thread_local_model)) { LLVMSetLinkage(global, LLVMInternalLinkage); } + if (value.value != nullptr) { + if (is_type_any(e->type)) { + Type *var_type = default_type(value.type); + + gbString var_name = gb_string_make(temporary_allocator(), "__$static_any::"); + var_name = gb_string_append_length(var_name, mangled_name.text, mangled_name.len); + + lbAddr var_global = lb_add_global_generated_with_name(p->module, var_type, value, make_string_c(var_name), nullptr); + LLVMValueRef var_global_ref = var_global.addr.value; + + if (e->Variable.is_rodata) { + LLVMSetGlobalConstant(var_global_ref, true); + } + + if (!lb_apply_thread_local_model(var_global_ref, e->Variable.thread_local_model)) { + LLVMSetLinkage(var_global_ref, LLVMInternalLinkage); + } + + LLVMValueRef vals[2] = { + lb_emit_conv(p, var_global.addr, t_rawptr).value, + lb_typeid(p->module, var_type).value, + }; + LLVMValueRef init = llvm_const_named_struct(p->module, e->type, vals, gb_count_of(vals)); + LLVMSetInitializer(global, init); + } else { + LLVMSetInitializer(global, value.value); + } + } lbValue global_val = {global, alloc_type_pointer(e->type)}; lb_add_entity(p->module, e, global_val); @@ -2307,6 +2353,14 @@ gb_internal void lb_build_if_stmt(lbProcedure *p, Ast *node) { else_ = lb_create_block(p, "if.else"); } if (is->label != nullptr) { + if (p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "if.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, is->label)); + lb_add_debug_label(p, is->label, label); + } lbTargetList *tl = lb_push_target_list(p, is->label, done, nullptr, nullptr); tl->is_block = true; } @@ -2399,12 +2453,19 @@ gb_internal void lb_build_for_stmt(lbProcedure *p, Ast *node) { lb_push_target_list(p, fs->label, done, post, nullptr); + if (fs->label != nullptr && p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "for.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, fs->label)); + lb_add_debug_label(p, fs->label, label); + } if (fs->init != nullptr) { - #if 1 lbBlock *init = lb_create_block(p, "for.init"); lb_emit_jump(p, init); lb_start_block(p, init); - #endif + lb_build_stmt(p, fs->init); } @@ -2420,7 +2481,6 @@ gb_internal void lb_build_for_stmt(lbProcedure *p, Ast *node) { lb_start_block(p, body); } - lb_build_stmt(p, fs->body); lb_pop_target_list(p); @@ -2758,9 +2818,21 @@ gb_internal void lb_build_stmt(lbProcedure *p, Ast *node) { case_ast_node(bs, BlockStmt, node); + lbBlock *body = nullptr; lbBlock *done = nullptr; if (bs->label != nullptr) { + if (p->debug_info != nullptr) { + lbBlock *label = lb_create_block(p, "block.label"); + lb_emit_jump(p, label); + lb_start_block(p, label); + + LLVMSetCurrentDebugLocation2(p->builder, lb_debug_location_from_ast(p, bs->label)); + lb_add_debug_label(p, bs->label, label); + } + body = lb_create_block(p, "block.body"); done = lb_create_block(p, "block.done"); + lb_emit_jump(p, body); + lb_start_block(p, body); lbTargetList *tl = lb_push_target_list(p, bs->label, done, nullptr, nullptr); tl->is_block = true; } @@ -2981,6 +3053,18 @@ gb_internal void lb_emit_defer_stmts(lbProcedure *p, lbDeferExitKind kind, lbBlo } defer (p->branch_location_pos = prev_token_pos); + // TODO(lucas): In LLVM 21 use the 'use-after-scope' asan option which does this for us. + if (kind == lbDeferExit_Return) { + for_array(i, p->asan_stack_locals) { + lbValue local = p->asan_stack_locals[i]; + + auto args = array_make(temporary_allocator(), 2); + args[0] = lb_emit_conv(p, local, t_rawptr); + args[1] = lb_const_int(p->module, t_int, type_size_of(local.type->Pointer.elem)); + lb_emit_runtime_call(p, "__asan_unpoison_memory_region", args); + } + } + isize count = p->defer_stmts.count; isize i = count; while (i --> 0) { diff --git a/src/llvm_backend_type.cpp b/src/llvm_backend_type.cpp index ad4250f3c..4e514c3d1 100644 --- a/src/llvm_backend_type.cpp +++ b/src/llvm_backend_type.cpp @@ -1,4 +1,10 @@ +gb_internal void lb_set_odin_rtti_section(LLVMValueRef value) { + if (build_context.metrics.os != TargetOs_darwin) { + LLVMSetSection(value, ".odinti"); + } +} + gb_internal isize lb_type_info_index(CheckerInfo *info, TypeInfoPair pair, bool err_on_not_found=true) { isize index = type_info_index(info, pair, err_on_not_found); if (index >= 0) { @@ -221,6 +227,7 @@ gb_internal void lb_setup_type_info_data_giant_array(lbModule *m, i64 global_typ gb_snprintf(name, 63, "__$ti-%lld", cast(long long)index); LLVMValueRef g = LLVMAddGlobal(m->mod, type, name); lb_make_global_private_const(g); + lb_set_odin_rtti_section(g); return g; }; @@ -716,6 +723,8 @@ gb_internal void lb_setup_type_info_data_giant_array(lbModule *m, i64 global_typ LLVMSetInitializer(value_array.value, value_init); LLVMSetGlobalConstant(name_array.value, true); LLVMSetGlobalConstant(value_array.value, true); + lb_set_odin_rtti_section(name_array.value); + lb_set_odin_rtti_section(value_array.value); lbValue v_count = lb_const_int(m, t_int, fields.count); @@ -1056,6 +1065,7 @@ gb_internal void lb_setup_type_info_data_giant_array(lbModule *m, i64 global_typ LLVMValueRef giant_array = lb_global_type_info_data_ptr(m).value; LLVMSetInitializer(giant_array, giant_const); lb_make_global_private_const(giant_array); + lb_set_odin_rtti_section(giant_array); } diff --git a/src/llvm_backend_utility.cpp b/src/llvm_backend_utility.cpp index bfeebfcbe..521553147 100644 --- a/src/llvm_backend_utility.cpp +++ b/src/llvm_backend_utility.cpp @@ -2125,7 +2125,7 @@ gb_internal lbAddr lb_handle_objc_find_or_register_selector(lbProcedure *p, Stri return addr; } -gb_internal lbAddr lb_handle_objc_find_or_register_class(lbProcedure *p, String const &name) { +gb_internal lbAddr lb_handle_objc_find_or_register_class(lbProcedure *p, String const &name, Type *class_impl_type) { lbModule *m = p->module; lbAddr *found = string_map_get(&m->objc_classes, name); if (found) { @@ -2148,13 +2148,75 @@ gb_internal lbAddr lb_handle_objc_find_or_register_class(lbProcedure *p, String } else { LLVMSetLinkage(g.value, LLVMExternalLinkage); } - mpsc_enqueue(&m->gen->objc_classes, lbObjCGlobal{m, global_name, name, t_objc_Class}); + mpsc_enqueue(&m->gen->objc_classes, lbObjCGlobal{m, global_name, name, t_objc_Class, class_impl_type}); lbAddr addr = lb_addr(g); string_map_set(&m->objc_classes, name, addr); return addr; } +gb_internal lbAddr lb_handle_objc_find_or_register_ivar(lbModule *m, Type *self_type) { + + String name = self_type->Named.type_name->TypeName.objc_class_name; + GB_ASSERT(name != ""); + + lbAddr *found = string_map_get(&m->objc_ivars, name); + if (found) { + return *found; + } + + lbModule *default_module = &m->gen->default_module; + + gbString global_name = gb_string_make(permanent_allocator(), "__$objc_ivar::"); + global_name = gb_string_append_length(global_name, name.text, name.len); + + // Create a global variable to store offset of the ivar in an instance of an object + LLVMTypeRef t = lb_type(m, t_int); + + lbValue g = {}; + g.value = LLVMAddGlobal(m->mod, t, global_name); + g.type = t_int_ptr; + + if (default_module == m) { + LLVMSetInitializer(g.value, LLVMConstInt(t, 0, true)); + lb_add_member(m, make_string_c(global_name), g); + } else { + LLVMSetLinkage(g.value, LLVMExternalLinkage); + } + + mpsc_enqueue(&m->gen->objc_ivars, lbObjCGlobal{m, global_name, name, t_int, self_type}); + + lbAddr addr = lb_addr(g); + string_map_set(&m->objc_ivars, name, addr); + return addr; +} + +gb_internal lbValue lb_handle_objc_ivar_for_objc_object_pointer(lbProcedure *p, lbValue self) { + GB_ASSERT(self.type->kind == Type_Pointer && self.type->Pointer.elem->kind == Type_Named); + + Type *self_type = self.type->Pointer.elem; + + lbValue self_uptr = lb_emit_conv(p, self, t_uintptr); + + lbValue ivar_offset = lb_addr_load(p, lb_handle_objc_find_or_register_ivar(p->module, self_type)); + lbValue ivar_offset_uptr = lb_emit_conv(p, ivar_offset, t_uintptr); + + + lbValue ivar_uptr = lb_emit_arith(p, Token_Add, self_uptr, ivar_offset_uptr, t_uintptr); + + Type *ivar_type = self_type->Named.type_name->TypeName.objc_ivar; + return lb_emit_conv(p, ivar_uptr, alloc_type_pointer(ivar_type)); +} + +gb_internal lbValue lb_handle_objc_ivar_get(lbProcedure *p, Ast *expr) { + ast_node(ce, CallExpr, expr); + + GB_ASSERT(ce->args[0]->tav.type->kind == Type_Pointer); + lbValue self = lb_build_expr(p, ce->args[0]); + + return lb_handle_objc_ivar_for_objc_object_pointer(p, self); +} + gb_internal lbValue lb_handle_objc_find_selector(lbProcedure *p, Ast *expr) { ast_node(ce, CallExpr, expr); @@ -2188,7 +2250,7 @@ gb_internal lbValue lb_handle_objc_find_class(lbProcedure *p, Ast *expr) { auto tav = ce->args[0]->tav; GB_ASSERT(tav.value.kind == ExactValue_String); String name = tav.value.value_string; - return lb_addr_load(p, lb_handle_objc_find_or_register_class(p, name)); + return lb_addr_load(p, lb_handle_objc_find_or_register_class(p, name, nullptr)); } gb_internal lbValue lb_handle_objc_register_class(lbProcedure *p, Ast *expr) { @@ -2198,7 +2260,7 @@ gb_internal lbValue lb_handle_objc_register_class(lbProcedure *p, Ast *expr) { auto tav = ce->args[0]->tav; GB_ASSERT(tav.value.kind == ExactValue_String); String name = tav.value.value_string; - lbAddr dst = lb_handle_objc_find_or_register_class(p, name); + lbAddr dst = lb_handle_objc_find_or_register_class(p, name, nullptr); auto args = array_make(permanent_allocator(), 3); args[0] = lb_const_nil(m, t_objc_Class); @@ -2220,7 +2282,9 @@ gb_internal lbValue lb_handle_objc_id(lbProcedure *p, Ast *expr) { GB_ASSERT(e->kind == Entity_TypeName); String name = e->TypeName.objc_class_name; - return lb_addr_load(p, lb_handle_objc_find_or_register_class(p, name)); + Type *class_impl_type = e->TypeName.objc_is_implementation ? type : nullptr; + + return lb_addr_load(p, lb_handle_objc_find_or_register_class(p, name, class_impl_type)); } return lb_build_expr(p, expr); @@ -2266,9 +2330,6 @@ gb_internal lbValue lb_handle_objc_send(lbProcedure *p, Ast *expr) { return lb_emit_call(p, the_proc, args); } - - - gb_internal LLVMAtomicOrdering llvm_atomic_ordering_from_odin(ExactValue const &value) { GB_ASSERT(value.kind == ExactValue_Integer); i64 v = exact_value_to_i64(value); diff --git a/src/main.cpp b/src/main.cpp index c19bbde22..90f2aad7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -277,10 +277,11 @@ gb_internal void usage(String argv0, String argv1 = {}) { print_usage_line(1, "build Compiles directory of .odin files, as an executable."); print_usage_line(1, " One must contain the program's entry point, all must be in the same package."); print_usage_line(1, "run Same as 'build', but also then runs the newly compiled executable."); - print_usage_line(1, "check Parses, and type checks a directory of .odin files."); + print_usage_line(1, "bundle Bundles a directory in a specific layout for that platform."); + print_usage_line(1, "check Parses and type checks a directory of .odin files."); print_usage_line(1, "strip-semicolon Parses, type checks, and removes unneeded semicolons from the entire program."); print_usage_line(1, "test Builds and runs procedures with the attribute @(test) in the initial package."); - print_usage_line(1, "doc Generates documentation on a directory of .odin files."); + print_usage_line(1, "doc Generates documentation from a directory of .odin files."); print_usage_line(1, "version Prints version."); print_usage_line(1, "report Prints information useful to reporting a bug."); print_usage_line(1, "root Prints the root path where Odin looks for the builtin collections."); @@ -318,6 +319,7 @@ enum BuildFlagKind { BuildFlag_NoBoundsCheck, BuildFlag_NoTypeAssert, BuildFlag_NoDynamicLiterals, + BuildFlag_DynamicLiterals, BuildFlag_NoCRT, BuildFlag_NoRPath, BuildFlag_NoEntryPoint, @@ -411,7 +413,7 @@ enum BuildFlagKind { BuildFlag_AndroidKeystore, BuildFlag_AndroidKeystoreAlias, - BuildFlag_AndroidManifest, + BuildFlag_AndroidKeystorePassword, BuildFlag_COUNT, }; @@ -537,6 +539,7 @@ gb_internal bool parse_build_flags(Array args) { add_flag(&build_flags, BuildFlag_NoTypeAssert, str_lit("no-type-assert"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_NoThreadLocal, str_lit("no-thread-local"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_NoDynamicLiterals, str_lit("no-dynamic-literals"), BuildFlagParam_None, Command__does_check); + add_flag(&build_flags, BuildFlag_DynamicLiterals, str_lit("dynamic-literals"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_NoCRT, str_lit("no-crt"), BuildFlagParam_None, Command__does_build); add_flag(&build_flags, BuildFlag_NoRPath, str_lit("no-rpath"), BuildFlagParam_None, Command__does_build); add_flag(&build_flags, BuildFlag_NoEntryPoint, str_lit("no-entry-point"), BuildFlagParam_None, Command__does_check &~ Command_test); @@ -631,7 +634,7 @@ gb_internal bool parse_build_flags(Array args) { add_flag(&build_flags, BuildFlag_AndroidKeystore, str_lit("android-keystore"), BuildFlagParam_String, Command_bundle_android); add_flag(&build_flags, BuildFlag_AndroidKeystoreAlias, str_lit("android-keystore-alias"), BuildFlagParam_String, Command_bundle_android); - add_flag(&build_flags, BuildFlag_AndroidManifest, str_lit("android-manifest"), BuildFlagParam_String, Command_bundle_android); + add_flag(&build_flags, BuildFlag_AndroidKeystorePassword, str_lit("android-keystore-password"), BuildFlagParam_String, Command_bundle_android); Array flag_args = {}; @@ -1206,6 +1209,9 @@ gb_internal bool parse_build_flags(Array args) { case BuildFlag_NoDynamicLiterals: gb_printf_err("Warning: Use of -no-dynamic-literals is now redundant\n"); break; + case BuildFlag_DynamicLiterals: + build_context.dynamic_literals = true; + break; case BuildFlag_NoCRT: build_context.no_crt = true; break; @@ -1664,9 +1670,9 @@ gb_internal bool parse_build_flags(Array args) { build_context.android_keystore_alias = value.value_string; break; - case BuildFlag_AndroidManifest: + case BuildFlag_AndroidKeystorePassword: GB_ASSERT(value.kind == ExactValue_String); - build_context.android_manifest = value.value_string; + build_context.android_keystore_password = value.value_string; break; } } @@ -2200,7 +2206,7 @@ gb_internal void remove_temp_files(lbGenerator *gen) { return; } - TIME_SECTION("remove keep temp files"); + TIME_SECTION("remove temp files"); for (String const &path : gen->output_temp_paths) { gb_file_remove(cast(char const *)path.text); @@ -2220,20 +2226,30 @@ gb_internal void remove_temp_files(lbGenerator *gen) { } -gb_internal void print_show_help(String const arg0, String command, String optional_flag = {}) { +gb_internal int print_show_help(String const arg0, String command, String optional_flag = {}) { + bool help_resolved = false; + bool printed_usage_header = false; + bool printed_flags_header = false; + if (command == "help" && optional_flag.len != 0 && optional_flag[0] != '-') { command = optional_flag; optional_flag = {}; } - print_usage_line(0, "%.*s is a tool for managing Odin source code.", LIT(arg0)); - print_usage_line(0, "Usage:"); - print_usage_line(1, "%.*s %.*s [arguments]", LIT(arg0), LIT(command)); - print_usage_line(0, ""); - defer (print_usage_line(0, "")); - + auto const print_usage_header_once = [&help_resolved, &printed_usage_header, arg0, command]() { + if (printed_usage_header) { + return; + } + print_usage_line(0, "%.*s is a tool for managing Odin source code.", LIT(arg0)); + print_usage_line(0, "Usage:"); + print_usage_line(1, "%.*s %.*s [arguments]", LIT(arg0), LIT(command)); + print_usage_line(0, ""); + help_resolved = true; + printed_usage_header = true; + }; if (command == "build") { + print_usage_header_once(); print_usage_line(1, "build Compiles directory of .odin files as an executable."); print_usage_line(2, "One must contain the program's entry point, all must be in the same package."); print_usage_line(2, "Use `-file` to build a single file instead."); @@ -2242,6 +2258,7 @@ gb_internal void print_show_help(String const arg0, String command, String optio print_usage_line(3, "odin build Builds package in ."); print_usage_line(3, "odin build filename.odin -file Builds single-file package, must contain entry point."); } else if (command == "run") { + print_usage_header_once(); print_usage_line(1, "run Same as 'build', but also then runs the newly compiled executable."); print_usage_line(2, "Append an empty flag and then the args, '-- ', to specify args for the output."); print_usage_line(2, "Examples:"); @@ -2249,28 +2266,40 @@ gb_internal void print_show_help(String const arg0, String command, String optio print_usage_line(3, "odin run Builds and runs package in ."); print_usage_line(3, "odin run filename.odin -file Builds and runs single-file package, must contain entry point."); } else if (command == "check") { + print_usage_header_once(); print_usage_line(1, "check Parses and type checks directory of .odin files."); print_usage_line(2, "Examples:"); print_usage_line(3, "odin check . Type checks package in current directory."); print_usage_line(3, "odin check Type checks package in ."); print_usage_line(3, "odin check filename.odin -file Type checks single-file package, must contain entry point."); } else if (command == "test") { + print_usage_header_once(); print_usage_line(1, "test Builds and runs procedures with the attribute @(test) in the initial package."); } else if (command == "doc") { + print_usage_header_once(); print_usage_line(1, "doc Generates documentation from a directory of .odin files."); print_usage_line(2, "Examples:"); print_usage_line(3, "odin doc . Generates documentation on package in current directory."); print_usage_line(3, "odin doc Generates documentation on package in ."); print_usage_line(3, "odin doc filename.odin -file Generates documentation on single-file package."); } else if (command == "version") { + print_usage_header_once(); print_usage_line(1, "version Prints version."); } else if (command == "strip-semicolon") { + print_usage_header_once(); print_usage_line(1, "strip-semicolon"); print_usage_line(2, "Parses and type checks .odin file(s) and then removes unneeded semicolons from the entire project."); } else if (command == "bundle") { + print_usage_header_once(); print_usage_line(1, "bundle Bundles a directory in a specific layout for that platform"); print_usage_line(2, "Supported platforms:"); print_usage_line(3, "android"); + } else if (command == "report") { + print_usage_header_once(); + print_usage_line(1, "report Prints information useful to reporting a bug."); + } else if (command == "root") { + print_usage_header_once(); + print_usage_line(1, "root Prints the root path where Odin looks for the builtin collections."); } bool doc = command == "doc"; @@ -2292,13 +2321,10 @@ gb_internal void print_show_help(String const arg0, String command, String optio check = true; } - print_usage_line(0, ""); - print_usage_line(1, "Flags"); - print_usage_line(0, ""); - auto const print_flag = [&optional_flag](char const *flag) -> bool { + auto const print_flag = [&optional_flag, &help_resolved, &printed_flags_header, print_usage_header_once](char const *flag) -> bool { if (optional_flag.len != 0) { String f = make_string_c(flag); isize i = string_index_byte(f, ':'); @@ -2309,6 +2335,14 @@ gb_internal void print_show_help(String const arg0, String command, String optio return false; } } + print_usage_header_once(); + if (!printed_flags_header) { + print_usage_line(0, ""); + print_usage_line(1, "Flags"); + print_usage_line(0, ""); + printed_flags_header = true; + } + help_resolved = true; print_usage_line(0, ""); print_usage_line(1, flag); return true; @@ -2330,20 +2364,20 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-build-mode:")) { print_usage_line(2, "Sets the build mode."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-build-mode:exe Builds as an executable."); - print_usage_line(3, "-build-mode:test Builds as an executable that executes tests."); - print_usage_line(3, "-build-mode:dll Builds as a dynamically linked library."); - print_usage_line(3, "-build-mode:shared Builds as a dynamically linked library."); - print_usage_line(3, "-build-mode:dynamic Builds as a dynamically linked library."); - print_usage_line(3, "-build-mode:lib Builds as a statically linked library."); - print_usage_line(3, "-build-mode:static Builds as a statically linked library."); - print_usage_line(3, "-build-mode:obj Builds as an object file."); - print_usage_line(3, "-build-mode:object Builds as an object file."); - print_usage_line(3, "-build-mode:assembly Builds as an assembly file."); - print_usage_line(3, "-build-mode:assembler Builds as an assembly file."); - print_usage_line(3, "-build-mode:asm Builds as an assembly file."); - print_usage_line(3, "-build-mode:llvm-ir Builds as an LLVM IR file."); - print_usage_line(3, "-build-mode:llvm Builds as an LLVM IR file."); + print_usage_line(3, "-build-mode:exe Builds as an executable."); + print_usage_line(3, "-build-mode:test Builds as an executable that executes tests."); + print_usage_line(3, "-build-mode:dll Builds as a dynamically linked library."); + print_usage_line(3, "-build-mode:shared Builds as a dynamically linked library."); + print_usage_line(3, "-build-mode:dynamic Builds as a dynamically linked library."); + print_usage_line(3, "-build-mode:lib Builds as a statically linked library."); + print_usage_line(3, "-build-mode:static Builds as a statically linked library."); + print_usage_line(3, "-build-mode:obj Builds as an object file."); + print_usage_line(3, "-build-mode:object Builds as an object file."); + print_usage_line(3, "-build-mode:assembly Builds as an assembly file."); + print_usage_line(3, "-build-mode:assembler Builds as an assembly file."); + print_usage_line(3, "-build-mode:asm Builds as an assembly file."); + print_usage_line(3, "-build-mode:llvm-ir Builds as an LLVM IR file."); + print_usage_line(3, "-build-mode:llvm Builds as an LLVM IR file."); } } @@ -2352,16 +2386,16 @@ gb_internal void print_show_help(String const arg0, String command, String optio print_usage_line(2, "Defines a library collection used for imports."); print_usage_line(2, "Example: -collection:shared=dir/to/shared"); print_usage_line(2, "Usage in Code:"); - print_usage_line(3, "import \"shared:foo\""); + print_usage_line(3, "import \"shared:foo\""); } if (print_flag("-custom-attribute:")) { print_usage_line(2, "Add a custom attribute which will be ignored if it is unknown."); print_usage_line(2, "This can be used with metaprogramming tools."); print_usage_line(2, "Examples:"); - print_usage_line(3, "-custom-attribute:my_tag"); - print_usage_line(3, "-custom-attribute:my_tag,the_other_thing"); - print_usage_line(3, "-custom-attribute:my_tag -custom-attribute:the_other_thing"); + print_usage_line(3, "-custom-attribute:my_tag"); + print_usage_line(3, "-custom-attribute:my_tag,the_other_thing"); + print_usage_line(3, "-custom-attribute:my_tag -custom-attribute:the_other_thing"); } } @@ -2384,7 +2418,7 @@ gb_internal void print_show_help(String const arg0, String command, String optio print_usage_line(2, "Defines a scalar boolean, integer or string as global constant."); print_usage_line(2, "Example: -define:SPAM=123"); print_usage_line(2, "Usage in code:"); - print_usage_line(3, "#config(SPAM, default_value)"); + print_usage_line(3, "#config(SPAM, default_value)"); } } @@ -2419,9 +2453,9 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (check) { if (print_flag("-error-pos-style:")) { print_usage_line(2, "Available options:"); - print_usage_line(3, "-error-pos-style:unix file/path:45:3:"); - print_usage_line(3, "-error-pos-style:odin file/path(45:3)"); - print_usage_line(3, "-error-pos-style:default (Defaults to 'odin'.)"); + print_usage_line(3, "-error-pos-style:unix file/path:45:3:"); + print_usage_line(3, "-error-pos-style:odin file/path(45:3)"); + print_usage_line(3, "-error-pos-style:default (Defaults to 'odin'.)"); } if (print_flag("-export-defineables:")) { @@ -2432,8 +2466,8 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-export-dependencies:")) { print_usage_line(2, "Exports dependencies to one of a few formats. Requires `-export-dependencies-file`."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-export-dependencies:make Exports in Makefile format"); - print_usage_line(3, "-export-dependencies:json Exports in JSON format"); + print_usage_line(3, "-export-dependencies:make Exports in Makefile format"); + print_usage_line(3, "-export-dependencies:json Exports in JSON format"); } if (print_flag("-export-dependencies-file:")) { @@ -2444,8 +2478,8 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-export-timings:")) { print_usage_line(2, "Exports timings to one of a few formats. Requires `-show-timings` or `-show-more-timings`."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-export-timings:json Exports compile time stats to JSON."); - print_usage_line(3, "-export-timings:csv Exports compile time stats to CSV."); + print_usage_line(3, "-export-timings:json Exports compile time stats to JSON."); + print_usage_line(3, "-export-timings:csv Exports compile time stats to CSV."); } if (print_flag("-export-timings-file:")) { @@ -2535,9 +2569,9 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-microarch:")) { print_usage_line(2, "Specifies the specific micro-architecture for the build in a string."); print_usage_line(2, "Examples:"); - print_usage_line(3, "-microarch:sandybridge"); - print_usage_line(3, "-microarch:native"); - print_usage_line(3, "-microarch:\"?\" for a list"); + print_usage_line(3, "-microarch:sandybridge"); + print_usage_line(3, "-microarch:native"); + print_usage_line(3, "-microarch:\"?\" for a list"); } } @@ -2552,7 +2586,7 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-minimum-os-version:")) { print_usage_line(2, "Sets the minimum OS version targeted by the application."); print_usage_line(2, "Default: -minimum-os-version:11.0.0"); - print_usage_line(2, "Only used when target is Darwin, if given, linking mismatched versions will emit a warning."); + print_usage_line(2, "Only used when target is Darwin or subtarget is Android, if given, linking mismatched versions will emit a warning."); } } @@ -2594,10 +2628,10 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-o:")) { print_usage_line(2, "Sets the optimization mode for compilation."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-o:none"); - print_usage_line(3, "-o:minimal"); - print_usage_line(3, "-o:size"); - print_usage_line(3, "-o:speed"); + print_usage_line(3, "-o:none"); + print_usage_line(3, "-o:minimal"); + print_usage_line(3, "-o:size"); + print_usage_line(3, "-o:speed"); if (LB_USE_NEW_PASS_SYSTEM) { print_usage_line(3, "-o:aggressive (use this with caution)"); } @@ -2648,10 +2682,10 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-reloc-mode:")) { print_usage_line(2, "Specifies the reloc mode."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-reloc-mode:default"); - print_usage_line(3, "-reloc-mode:static"); - print_usage_line(3, "-reloc-mode:pic"); - print_usage_line(3, "-reloc-mode:dynamic-no-pic"); + print_usage_line(3, "-reloc-mode:default"); + print_usage_line(3, "-reloc-mode:static"); + print_usage_line(3, "-reloc-mode:pic"); + print_usage_line(3, "-reloc-mode:dynamic-no-pic"); } #if defined(GB_SYSTEM_WINDOWS) @@ -2666,9 +2700,9 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-sanitize:")) { print_usage_line(2, "Enables sanitization analysis."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-sanitize:address"); - print_usage_line(3, "-sanitize:memory"); - print_usage_line(3, "-sanitize:thread"); + print_usage_line(3, "-sanitize:address"); + print_usage_line(3, "-sanitize:memory"); + print_usage_line(3, "-sanitize:thread"); print_usage_line(2, "NOTE: This flag can be used multiple times."); } } @@ -2729,17 +2763,32 @@ gb_internal void print_show_help(String const arg0, String command, String optio print_usage_line(2, "[Windows only]"); print_usage_line(2, "Defines the subsystem for the application."); print_usage_line(2, "Available options:"); - print_usage_line(3, "-subsystem:console"); - print_usage_line(3, "-subsystem:windows"); + print_usage_line(3, "-subsystem:console"); + print_usage_line(3, "-subsystem:windows"); } #endif + } + if (build) { + if (print_flag("-subtarget:")) { + print_usage_line(2, "[Darwin and Linux only]"); + print_usage_line(2, "Available subtargets:"); + String prefix = str_lit("-subtarget:"); + for (u32 i = 1; i < Subtarget_COUNT; i++) { + String name = subtarget_strings[i]; + String help_string = concatenate_strings(temporary_allocator(), prefix, name); + print_usage_line(3, (const char *)help_string.text); + } + } + } + + if (run_or_build) { if (print_flag("-target-features:")) { print_usage_line(2, "Specifies CPU features to enable on top of the enabled features implied by -microarch."); print_usage_line(2, "Examples:"); - print_usage_line(3, "-target-features:atomics"); - print_usage_line(3, "-target-features:\"sse2,aes\""); - print_usage_line(3, "-target-features:\"?\" for a list"); + print_usage_line(3, "-target-features:atomics"); + print_usage_line(3, "-target-features:\"sse2,aes\""); + print_usage_line(3, "-target-features:\"?\" for a list"); } } @@ -2776,11 +2825,11 @@ gb_internal void print_show_help(String const arg0, String command, String optio if (print_flag("-vet")) { print_usage_line(2, "Does extra checks on the code."); print_usage_line(2, "Extra checks include:"); - print_usage_line(3, "-vet-unused"); - print_usage_line(3, "-vet-unused-variables"); - print_usage_line(3, "-vet-unused-imports"); - print_usage_line(3, "-vet-shadowing"); - print_usage_line(3, "-vet-using-stmt"); + print_usage_line(3, "-vet-unused"); + print_usage_line(3, "-vet-unused-variables"); + print_usage_line(3, "-vet-unused-imports"); + print_usage_line(3, "-vet-shadowing"); + print_usage_line(3, "-vet-using-stmt"); } if (print_flag("-vet-cast")) { @@ -2847,6 +2896,40 @@ gb_internal void print_show_help(String const arg0, String command, String optio print_usage_line(2, "Treats warning messages as error messages."); } } + + if (bundle) { + print_usage_line(0, ""); + print_usage_line(1, "Android-specific flags"); + print_usage_line(0, ""); + if (print_flag("-android-keystore:")) { + print_usage_line(2, "Specifies the keystore file to use to sign the apk."); + } + + if (print_flag("-android-keystore-alias:")) { + print_usage_line(2, "Specifies the key alias to use when signing the apk"); + print_usage_line(2, "Can be omitted if the keystore only contains one key"); + } + + if (print_flag("-android-keystore-password:")) { + print_usage_line(2, "Sets the password to use to unlock the keystore"); + print_usage_line(2, "If this is omitted, the terminal will prompt you to provide it."); + } + } + + if (!help_resolved) { + usage(arg0); + print_usage_line(0, ""); + if (command == "help") { + print_usage_line(0, "'%.*s' is not a recognized flag.", LIT(optional_flag)); + } else { + print_usage_line(0, "'%.*s' is not a recognized command.", LIT(command)); + } + return 1; + } + + print_usage_line(0, ""); + + return 0; } gb_internal void print_show_unused(Checker *c) { @@ -3219,6 +3302,16 @@ int main(int arg_count, char const **arg_ptr) { String run_args_string = {}; isize last_non_run_arg = args.count; + for_array(i, args) { + if (args[i] == "--") { + break; + } + if (args[i] == "-help" || args[i] == "--help") { + build_context.show_help = true; + return print_show_help(args[0], command); + } + } + bool run_output = false; if (command == "run" || command == "test") { if (args.count < 3) { @@ -3312,6 +3405,10 @@ int main(int arg_count, char const **arg_ptr) { return 1; #endif } else if (command == "version") { + if (args.count != 2) { + usage(args[0]); + return 1; + } build_context.command_kind = Command_version; gb_printf("%.*s version %.*s", LIT(args[0]), LIT(ODIN_VERSION)); @@ -3326,6 +3423,10 @@ int main(int arg_count, char const **arg_ptr) { gb_printf("\n"); return 0; } else if (command == "report") { + if (args.count != 2) { + usage(args[0]); + return 1; + } build_context.command_kind = Command_bug_report; print_bug_report_help(); return 0; @@ -3334,8 +3435,7 @@ int main(int arg_count, char const **arg_ptr) { usage(args[0]); return 1; } else { - print_show_help(args[0], args[1], args[2]); - return 0; + return print_show_help(args[0], args[1], args[2]); } } else if (command == "bundle") { if (args.count < 4) { @@ -3351,6 +3451,10 @@ int main(int arg_count, char const **arg_ptr) { } init_filename = args[3]; } else if (command == "root") { + if (args.count != 2) { + usage(args[0]); + return 1; + } gb_printf("%.*s", LIT(odin_root_dir())); return 0; } else if (command == "clear-cache") { @@ -3366,11 +3470,6 @@ int main(int arg_count, char const **arg_ptr) { init_filename = copy_string(permanent_allocator(), init_filename); - if (init_filename == "-help" || - init_filename == "--help") { - build_context.show_help = true; - } - if (init_filename.len > 0 && !build_context.show_help) { // The command must be build, run, test, check, or another that takes a directory or filename. if (!path_is_directory(init_filename)) { @@ -3421,8 +3520,7 @@ int main(int arg_count, char const **arg_ptr) { } if (build_context.show_help) { - print_show_help(args[0], command); - return 0; + return print_show_help(args[0], command); } if (command == "bundle") { diff --git a/src/name_canonicalization.cpp b/src/name_canonicalization.cpp index a80dc1996..0372f5039 100644 --- a/src/name_canonicalization.cpp +++ b/src/name_canonicalization.cpp @@ -1,3 +1,5 @@ +gb_internal bool is_in_doc_writer(void); + gb_internal GB_COMPARE_PROC(type_info_pair_cmp) { TypeInfoPair *x = cast(TypeInfoPair *)a; TypeInfoPair *y = cast(TypeInfoPair *)b; @@ -284,6 +286,23 @@ gb_internal void write_canonical_params(TypeWriter *w, Type *params) { } else { write_type_to_canonical_string(w, v->type); } + if (is_in_doc_writer()) { + // NOTE(bill): This just exists to make sure the entities default values exist when + // writing to the odin doc format + Ast *expr = v->Variable.init_expr; + if (expr == nullptr) { + expr = v->Variable.param_value.original_ast_expr; + } + if (expr != nullptr) { + type_writer_appendc(w, "="); + gbString s = write_expr_to_string( // Minor leak + gb_string_make(temporary_allocator(), ""), + expr, + build_context.cmd_doc_flags & CmdDocFlag_Short + ); + type_writer_append(w, s, gb_string_length(s)); + } + } break; case Entity_TypeName: type_writer_appendc(w, CANONICAL_PARAM_TYPEID); @@ -520,7 +539,6 @@ write_base_name: return; } -gb_internal bool is_in_doc_writer(void); // NOTE(bill): This exists so that we deterministically hash a type by serializing it to a canonical string gb_internal void write_type_to_canonical_string(TypeWriter *w, Type *type) { @@ -631,6 +649,10 @@ gb_internal void write_type_to_canonical_string(TypeWriter *w, Type *type) { case Type_Union: type_writer_appendc(w, "union"); + if (is_in_doc_writer() && type->Union.polymorphic_params) { + write_canonical_params(w, type->Union.polymorphic_params); + } + switch (type->Union.kind) { case UnionType_no_nil: type_writer_appendc(w, "#no_nil"); break; case UnionType_shared_nil: type_writer_appendc(w, "#shared_nil"); break; @@ -658,6 +680,11 @@ gb_internal void write_type_to_canonical_string(TypeWriter *w, Type *type) { } type_writer_appendc(w, "struct"); + + if (is_in_doc_writer() && type->Struct.polymorphic_params) { + write_canonical_params(w, type->Struct.polymorphic_params); + } + if (type->Struct.is_packed) type_writer_appendc(w, "#packed"); if (type->Struct.is_raw_union) type_writer_appendc(w, "#raw_union"); if (type->Struct.is_no_copy) type_writer_appendc(w, "#no_copy"); @@ -724,9 +751,17 @@ gb_internal void write_type_to_canonical_string(TypeWriter *w, Type *type) { if (is_in_doc_writer()) { type_writer_appendc(w, "$"); type_writer_append(w, type->Generic.name.text, type->Generic.name.len); - type_writer_append_fmt(w, "%lld", cast(long long)type->Generic.id); + type_writer_append_fmt(w, "-%lld", cast(long long)type->Generic.id); + if (type->Generic.specialized) { + type_writer_appendc(w, "/"); + write_type_to_canonical_string(w, type->Generic.specialized); + } + } else if (type->Generic.specialized) { + // If we have a specialized type, use that instead of panicking + write_type_to_canonical_string(w, type->Generic.specialized); } else { - GB_PANIC("Type_Generic should never be hit"); + // For unspecialized generics, use a generic placeholder string + type_writer_appendc(w, "rawptr"); } return; diff --git a/src/parser.cpp b/src/parser.cpp index df0ad87f8..7bb480cbd 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -6195,7 +6195,7 @@ gb_internal String build_tag_get_token(String s, String *out) { isize width = utf8_decode(&s[n], s.len-n, &rune); if (n == 0 && rune == '!') { - } else if (!rune_is_letter(rune) && !rune_is_digit(rune)) { + } else if (!rune_is_letter(rune) && !rune_is_digit(rune) && rune != ':') { isize k = gb_max(gb_max(n, width), 1); *out = substring(s, k, s.len); return substring(s, 0, k); @@ -6247,7 +6247,9 @@ gb_internal bool parse_build_tag(Token token_for_pos, String s) { continue; } - TargetOsKind os = get_target_os_from_string(p); + Subtarget subtarget = Subtarget_Default; + + TargetOsKind os = get_target_os_from_string(p, &subtarget); TargetArchKind arch = get_target_arch_from_string(p); num_tokens += 1; @@ -6261,11 +6263,13 @@ gb_internal bool parse_build_tag(Token token_for_pos, String s) { if (os != TargetOs_Invalid) { this_kind_os_seen = true; + bool same_subtarget = (subtarget == Subtarget_Default) || (subtarget == selected_subtarget); + GB_ASSERT(arch == TargetArch_Invalid); if (is_notted) { - this_kind_correct = this_kind_correct && (os != build_context.metrics.os); + this_kind_correct = this_kind_correct && (os != build_context.metrics.os || !same_subtarget); } else { - this_kind_correct = this_kind_correct && (os == build_context.metrics.os); + this_kind_correct = this_kind_correct && (os == build_context.metrics.os && same_subtarget); } } else if (arch != TargetArch_Invalid) { this_kind_arch_seen = true; diff --git a/src/path.cpp b/src/path.cpp index 12f8d3d4e..d5e982088 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -81,7 +81,7 @@ String get_working_directory(gbAllocator allocator) { auto buf = array_make(temporary_allocator()); size_t size = PATH_MAX; - char const *cwd; + char const *cwd = nullptr; for (; cwd == nullptr; size *= 2) { array_resize(&buf, size); diff --git a/src/ptr_map.cpp b/src/ptr_map.cpp index 1c157c386..61f703cf1 100644 --- a/src/ptr_map.cpp +++ b/src/ptr_map.cpp @@ -15,7 +15,7 @@ static void *const MAP_TOMBSTONE = (void *)~(uintptr)0; template struct PtrMapEntry { static_assert(sizeof(K) == sizeof(void *), "Key size must be pointer size"); - + K key; V value; }; @@ -374,7 +374,7 @@ struct PtrMapIterator { } bool operator==(PtrMapIterator const &other) const noexcept { - return this->map == other->map && this->index == other->index; + return this->map == other.map && this->index == other.index; } operator PtrMapEntry *() const { @@ -858,4 +858,4 @@ gb_internal OrderedInsertPtrMapEntry *end(OrderedInsertPtrMap &m) { template gb_internal OrderedInsertPtrMapEntry const *end(OrderedInsertPtrMap const &m) { return m.entries + m.count; -} \ No newline at end of file +} diff --git a/src/string.cpp b/src/string.cpp index 88b679540..ae8d066b1 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -336,6 +336,83 @@ gb_internal Array split_lines_from_array(Array const &array, gbAlloc return lines; } +enum : u32 { PRIME_RABIN_KARP = 16777619u }; + +gb_internal u32 hash_str_rabin_karp(String const &s, u32 *pow_) { + u32 hash = 0; + u32 pow = 1; + for (isize i = 0; i < s.len; i++) { + hash = hash*PRIME_RABIN_KARP + cast(u32)s.text[i]; + } + u32 sq = PRIME_RABIN_KARP; + for (isize i = s.len; i > 0; i >>= 1) { + if ((i & 1) != 0) { + pow *= sq; + } + sq *= sq; + } + if (pow_) *pow_ = pow; + return hash; + +} + + +gb_internal isize string_index(String const &s, String const &substr) { + isize n = substr.len; + if (n == 0) { + return 0; + } else if (n == 1) { + return string_index_byte(s, substr[0]); + } else if (n == s.len) { + if (s == substr) { + return 0; + } + return -1; + } else if (n > s.len) { + return -1; + } + u32 pow = 1; + u32 hash = hash_str_rabin_karp(s, &pow); + u32 h = 0; + for (isize i = 0; i < n; i++) { + h = h*PRIME_RABIN_KARP + cast(u32)s.text[i]; + } + if (h == hash && substring(s, 0, n) == substr) { + return 0; + } + for (isize i = n; i < s.len; /**/) { + h *= PRIME_RABIN_KARP; + h += cast(u32)s.text[i]; + h -= pow * u32(s.text[i-n]); + i += 1; + if (h == hash && substring(s, i-n, i) == substr) { + return i - n; + } + } + return -1; +} + + +struct StringPartition { + String head; + String match; + String tail; +}; + +gb_internal StringPartition string_partition(String const &str, String const &sep) { + StringPartition res = {}; + isize i = string_index(str, sep); + if (i < 0) { + res.head = str; + return res; + } + + res.head = substring(str, 0, i); + res.match = substring(str, i, i+sep.len); + res.tail = substring(str, i+sep.len, str.len); + return res; +} + gb_internal bool string_contains_char(String const &s, u8 c) { isize i; for (i = 0; i < s.len; i++) { diff --git a/src/types.cpp b/src/types.cpp index 43fe625f2..ce921796d 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -111,7 +111,7 @@ enum BasicFlag { BasicFlag_Ordered = BasicFlag_Integer | BasicFlag_Float | BasicFlag_String | BasicFlag_Pointer | BasicFlag_Rune, BasicFlag_OrderedNumeric = BasicFlag_Integer | BasicFlag_Float | BasicFlag_Rune, BasicFlag_ConstantType = BasicFlag_Boolean | BasicFlag_Numeric | BasicFlag_String | BasicFlag_Pointer | BasicFlag_Rune, - BasicFlag_SimpleCompare = BasicFlag_Boolean | BasicFlag_Numeric | BasicFlag_Pointer | BasicFlag_Rune, + BasicFlag_SimpleCompare = BasicFlag_Boolean | BasicFlag_Integer | BasicFlag_Pointer | BasicFlag_Rune, }; struct BasicType { @@ -729,10 +729,12 @@ gb_global Type *t_map_set_proc = nullptr; gb_global Type *t_objc_object = nullptr; gb_global Type *t_objc_selector = nullptr; gb_global Type *t_objc_class = nullptr; +gb_global Type *t_objc_ivar = nullptr; gb_global Type *t_objc_id = nullptr; gb_global Type *t_objc_SEL = nullptr; gb_global Type *t_objc_Class = nullptr; +gb_global Type *t_objc_Ivar = nullptr; enum OdinAtomicMemoryOrder : i32 { OdinAtomicMemoryOrder_relaxed = 0, // unordered @@ -872,6 +874,29 @@ gb_internal Type *base_type(Type *t) { return t; } +gb_internal Type *base_named_type(Type *t) { + if (t->kind != Type_Named) { + return t_invalid; + } + + Type *prev_named = t; + t = t->Named.base; + for (;;) { + if (t == nullptr) { + break; + } + if (t->kind != Type_Named) { + break; + } + if (t == t->Named.base) { + return t_invalid; + } + prev_named = t; + t = t->Named.base; + } + return prev_named; +} + gb_internal Type *base_enum_type(Type *t) { Type *bt = base_type(t); if (bt != nullptr && @@ -2932,6 +2957,10 @@ gb_internal Type *default_type(Type *type) { case Basic_UntypedString: return t_string; case Basic_UntypedRune: return t_rune; } + } else if (type->kind == Type_Generic) { + if (type->Generic.specialized) { + return default_type(type->Generic.specialized); + } } return type; } @@ -3327,6 +3356,15 @@ gb_internal Selection lookup_field_with_selection(Type *type_, String field_name } } } + + Type *objc_ivar_type = e->TypeName.objc_ivar; + if (objc_ivar_type != nullptr) { + sel = lookup_field_with_selection(objc_ivar_type, field_name, false, sel, allow_blank_ident); + if (sel.entity != nullptr) { + sel.pseudo_field = true; + return sel; + } + } } if (is_type_polymorphic(type)) { @@ -4108,10 +4146,10 @@ gb_internal i64 type_size_of_internal(Type *t, TypePath *path) { } i64 max = 0; - i64 field_size = 0; for_array(i, t->Union.variants) { Type *variant_type = t->Union.variants[i]; + i64 size = type_size_of_internal(variant_type, path); if (max < size) { max = size; @@ -4130,7 +4168,7 @@ gb_internal i64 type_size_of_internal(Type *t, TypePath *path) { size = align_formula(max, tag_size); // NOTE(bill): Calculate the padding between the common fields and the tag t->Union.tag_size = cast(i16)tag_size; - t->Union.variant_block_size = size - field_size; + t->Union.variant_block_size = size; size += tag_size; } @@ -4542,7 +4580,7 @@ gb_internal Type *alloc_type_proc_from_types(Type **param_types, unsigned param_ // return type; // } -gb_internal gbString write_type_to_string(gbString str, Type *type, bool shorthand=false) { +gb_internal gbString write_type_to_string(gbString str, Type *type, bool shorthand=false, bool allow_polymorphic=false) { if (type == nullptr) { return gb_string_appendc(str, ""); } @@ -4567,24 +4605,24 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha str = gb_string_append_length(str, name.text, name.len); if (type->Generic.specialized != nullptr) { str = gb_string_append_rune(str, '/'); - str = write_type_to_string(str, type->Generic.specialized); + str = write_type_to_string(str, type->Generic.specialized, shorthand, allow_polymorphic); } } break; case Type_Pointer: str = gb_string_append_rune(str, '^'); - str = write_type_to_string(str, type->Pointer.elem); + str = write_type_to_string(str, type->Pointer.elem, shorthand, allow_polymorphic); break; case Type_SoaPointer: str = gb_string_appendc(str, "#soa ^"); - str = write_type_to_string(str, type->SoaPointer.elem); + str = write_type_to_string(str, type->SoaPointer.elem, shorthand, allow_polymorphic); break; case Type_MultiPointer: str = gb_string_appendc(str, "[^]"); - str = write_type_to_string(str, type->Pointer.elem); + str = write_type_to_string(str, type->Pointer.elem, shorthand, allow_polymorphic); break; case Type_EnumeratedArray: @@ -4592,31 +4630,31 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha str = gb_string_appendc(str, "#sparse"); } str = gb_string_append_rune(str, '['); - str = write_type_to_string(str, type->EnumeratedArray.index); + str = write_type_to_string(str, type->EnumeratedArray.index, shorthand, allow_polymorphic); str = gb_string_append_rune(str, ']'); - str = write_type_to_string(str, type->EnumeratedArray.elem); + str = write_type_to_string(str, type->EnumeratedArray.elem, shorthand, allow_polymorphic); break; case Type_Array: str = gb_string_appendc(str, gb_bprintf("[%lld]", cast(long long)type->Array.count)); - str = write_type_to_string(str, type->Array.elem); + str = write_type_to_string(str, type->Array.elem, shorthand, allow_polymorphic); break; case Type_Slice: str = gb_string_appendc(str, "[]"); - str = write_type_to_string(str, type->Array.elem); + str = write_type_to_string(str, type->Array.elem, shorthand, allow_polymorphic); break; case Type_DynamicArray: str = gb_string_appendc(str, "[dynamic]"); - str = write_type_to_string(str, type->DynamicArray.elem); + str = write_type_to_string(str, type->DynamicArray.elem, shorthand, allow_polymorphic); break; case Type_Enum: str = gb_string_appendc(str, "enum"); if (type->Enum.base_type != nullptr) { str = gb_string_appendc(str, " "); - str = write_type_to_string(str, type->Enum.base_type); + str = write_type_to_string(str, type->Enum.base_type, shorthand, allow_polymorphic); } str = gb_string_appendc(str, " {"); for_array(i, type->Enum.fields) { @@ -4633,6 +4671,13 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha case Type_Union: str = gb_string_appendc(str, "union"); + + if (allow_polymorphic && type->Struct.polymorphic_params) { + str = gb_string_appendc(str, "("); + str = write_type_to_string(str, type->Struct.polymorphic_params, shorthand, allow_polymorphic); + str = gb_string_appendc(str, ")"); + } + switch (type->Union.kind) { case UnionType_no_nil: str = gb_string_appendc(str, " #no_nil"); break; case UnionType_shared_nil: str = gb_string_appendc(str, " #shared_nil"); break; @@ -4642,7 +4687,7 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha for_array(i, type->Union.variants) { Type *t = type->Union.variants[i]; if (i > 0) str = gb_string_appendc(str, ", "); - str = write_type_to_string(str, t); + str = write_type_to_string(str, t, shorthand, allow_polymorphic); } str = gb_string_append_rune(str, '}'); break; @@ -4655,17 +4700,24 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha case StructSoa_Dynamic: str = gb_string_appendc(str, "#soa[dynamic]"); break; default: GB_PANIC("Unknown StructSoaKind"); break; } - str = write_type_to_string(str, type->Struct.soa_elem); + str = write_type_to_string(str, type->Struct.soa_elem, shorthand, allow_polymorphic); break; } str = gb_string_appendc(str, "struct"); + + if (allow_polymorphic && type->Struct.polymorphic_params) { + str = gb_string_appendc(str, "("); + str = write_type_to_string(str, type->Struct.polymorphic_params, shorthand, allow_polymorphic); + str = gb_string_appendc(str, ")"); + } + if (type->Struct.is_packed) str = gb_string_appendc(str, " #packed"); if (type->Struct.is_raw_union) str = gb_string_appendc(str, " #raw_union"); if (type->Struct.is_no_copy) str = gb_string_appendc(str, " #no_copy"); if (type->Struct.custom_align != 0) str = gb_string_append_fmt(str, " #align %d", cast(int)type->Struct.custom_align); - str = gb_string_appendc(str, " {"); + str = gb_string_appendc(str, " {"); if (shorthand && type->Struct.fields.count > 16) { str = gb_string_append_fmt(str, "%lld fields...", cast(long long)type->Struct.fields.count); @@ -4678,7 +4730,7 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha } str = gb_string_append_length(str, f->token.string.text, f->token.string.len); str = gb_string_appendc(str, ": "); - str = write_type_to_string(str, f->type); + str = write_type_to_string(str, f->type, shorthand, allow_polymorphic); } } str = gb_string_append_rune(str, '}'); @@ -4686,9 +4738,9 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha case Type_Map: { str = gb_string_appendc(str, "map["); - str = write_type_to_string(str, type->Map.key); + str = write_type_to_string(str, type->Map.key, shorthand, allow_polymorphic); str = gb_string_append_rune(str, ']'); - str = write_type_to_string(str, type->Map.value); + str = write_type_to_string(str, type->Map.value, shorthand, allow_polymorphic); } break; case Type_Named: @@ -4718,9 +4770,11 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha str = gb_string_append_length(str, name.text, name.len); if (!is_type_untyped(var->type)) { str = gb_string_appendc(str, ": "); - str = write_type_to_string(str, var->type); - str = gb_string_appendc(str, " = "); - str = write_exact_value_to_string(str, var->Constant.value); + str = write_type_to_string(str, var->type, shorthand, allow_polymorphic); + if (var->Constant.value.kind) { + str = gb_string_appendc(str, " = "); + str = write_exact_value_to_string(str, var->Constant.value); + } } else { str = gb_string_appendc(str, " := "); str = write_exact_value_to_string(str, var->Constant.value); @@ -4736,20 +4790,31 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha Type *slice = base_type(var->type); str = gb_string_appendc(str, ".."); GB_ASSERT(var->type->kind == Type_Slice); - str = write_type_to_string(str, slice->Slice.elem); + str = write_type_to_string(str, slice->Slice.elem, shorthand, allow_polymorphic); } else { - str = write_type_to_string(str, var->type); + str = write_type_to_string(str, var->type, shorthand, allow_polymorphic); } } else { GB_ASSERT(var->kind == Entity_TypeName); if (var->type->kind == Type_Generic) { - str = gb_string_appendc(str, "typeid/"); - str = write_type_to_string(str, var->type); + if (var->token.string.len != 0) { + String name = var->token.string; + str = gb_string_appendc(str, "$"); + str = gb_string_append_length(str, name.text, name.len); + str = gb_string_appendc(str, ": typeid"); + if (var->type->Generic.specialized) { + str = gb_string_appendc(str, "/"); + str = write_type_to_string(str, var->type->Generic.specialized, shorthand, allow_polymorphic); + } + } else { + str = gb_string_appendc(str, "typeid/"); + str = write_type_to_string(str, var->type, shorthand, allow_polymorphic); + } } else { str = gb_string_appendc(str, "$"); str = gb_string_append_length(str, name.text, name.len); str = gb_string_appendc(str, "="); - str = write_type_to_string(str, var->type); + str = write_type_to_string(str, var->type, shorthand, allow_polymorphic); } } } @@ -4795,7 +4860,7 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha } str = gb_string_appendc(str, "("); if (type->Proc.params) { - str = write_type_to_string(str, type->Proc.params); + str = write_type_to_string(str, type->Proc.params, shorthand, allow_polymorphic); } str = gb_string_appendc(str, ")"); if (type->Proc.results) { @@ -4803,7 +4868,7 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha if (type->Proc.results->Tuple.variables.count > 1) { str = gb_string_appendc(str, "("); } - str = write_type_to_string(str, type->Proc.results); + str = write_type_to_string(str, type->Proc.results, shorthand, allow_polymorphic); if (type->Proc.results->Tuple.variables.count > 1) { str = gb_string_appendc(str, ")"); } @@ -4815,7 +4880,7 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha if (type->BitSet.elem == nullptr) { str = gb_string_appendc(str, ""); } else if (is_type_enum(type->BitSet.elem)) { - str = write_type_to_string(str, type->BitSet.elem); + str = write_type_to_string(str, type->BitSet.elem, shorthand, allow_polymorphic); } else { str = gb_string_append_fmt(str, "%lld", type->BitSet.lower); str = gb_string_append_fmt(str, "..="); @@ -4823,14 +4888,14 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha } if (type->BitSet.underlying != nullptr) { str = gb_string_appendc(str, "; "); - str = write_type_to_string(str, type->BitSet.underlying); + str = write_type_to_string(str, type->BitSet.underlying, shorthand, allow_polymorphic); } str = gb_string_appendc(str, "]"); break; case Type_SimdVector: str = gb_string_append_fmt(str, "#simd[%d]", cast(int)type->SimdVector.count); - str = write_type_to_string(str, type->SimdVector.elem); + str = write_type_to_string(str, type->SimdVector.elem, shorthand, allow_polymorphic); break; case Type_Matrix: @@ -4838,12 +4903,12 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha str = gb_string_appendc(str, "#row_major "); } str = gb_string_appendc(str, gb_bprintf("matrix[%d, %d]", cast(int)type->Matrix.row_count, cast(int)type->Matrix.column_count)); - str = write_type_to_string(str, type->Matrix.elem); + str = write_type_to_string(str, type->Matrix.elem, shorthand, allow_polymorphic); break; case Type_BitField: str = gb_string_appendc(str, "bit_field "); - str = write_type_to_string(str, type->BitField.backing_type); + str = write_type_to_string(str, type->BitField.backing_type, shorthand, allow_polymorphic); str = gb_string_appendc(str, " {"); for (isize i = 0; i < type->BitField.fields.count; i++) { Entity *f = type->BitField.fields[i]; @@ -4852,7 +4917,7 @@ gb_internal gbString write_type_to_string(gbString str, Type *type, bool shortha } str = gb_string_append_length(str, f->token.string.text, f->token.string.len); str = gb_string_appendc(str, ": "); - str = write_type_to_string(str, f->type); + str = write_type_to_string(str, f->type, shorthand, allow_polymorphic); str = gb_string_append_fmt(str, " | %u", type->BitField.bit_sizes[i]); } str = gb_string_appendc(str, " }"); @@ -4870,6 +4935,11 @@ gb_internal gbString type_to_string(Type *type, bool shorthand) { return write_type_to_string(gb_string_make(heap_allocator(), ""), type, shorthand); } +gb_internal gbString type_to_string_polymorphic(Type *type) { + return write_type_to_string(gb_string_make(heap_allocator(), ""), type, false, true); +} + + gb_internal gbString type_to_string_shorthand(Type *type) { return type_to_string(type, true); } diff --git a/tests/core/assets/XML/entities.html b/tests/core/assets/XML/entities.html index 05a6b107e..a60f45070 100644 --- a/tests/core/assets/XML/entities.html +++ b/tests/core/assets/XML/entities.html @@ -25,5 +25,10 @@
| | | fj ` \ ® ϱ ∳ ⁏
+
HHellope!
+
HHellope!
+
HHellope!
+
HHellope!
+
HHellope!
\ No newline at end of file diff --git a/tests/core/encoding/xml/test_core_xml.odin b/tests/core/encoding/xml/test_core_xml.odin index c0e4329bd..409a8c9c0 100644 --- a/tests/core/encoding/xml/test_core_xml.odin +++ b/tests/core/encoding/xml/test_core_xml.odin @@ -114,7 +114,7 @@ xml_test_entities :: proc(t: ^testing.T) { }, expected_doctype = "html", }, - crc32 = 0x05373317, + crc32 = 0x48f41216, }) } @@ -128,7 +128,7 @@ xml_test_entities_unbox :: proc(t: ^testing.T) { }, expected_doctype = "html", }, - crc32 = 0x350ca83e, + crc32 = 0xd0567818, }) } @@ -142,7 +142,7 @@ xml_test_entities_unbox_decode :: proc(t: ^testing.T) { }, expected_doctype = "html", }, - crc32 = 0x7f58db7d, + crc32 = 0x68d2571e, }) } @@ -191,7 +191,7 @@ xml_test_unicode :: proc(t: ^testing.T) { } @(private) -run_test :: proc(t: ^testing.T, test: TEST) { +run_test :: proc(t: ^testing.T, test: TEST, loc := #caller_location) { path := strings.concatenate({TEST_SUITE_PATH, test.filename}) defer delete(path) @@ -205,10 +205,10 @@ run_test :: proc(t: ^testing.T, test: TEST) { crc32 := hash.crc32(tree_bytes) failed := err != test.err - testing.expectf(t, err == test.err, "%v: Expected return value %v, got %v", test.filename, test.err, err) + testing.expectf(t, err == test.err, "%v: Expected return value %v, got %v", test.filename, test.err, err, loc=loc) failed |= crc32 != test.crc32 - testing.expectf(t, crc32 == test.crc32, "%v: Expected CRC 0x%08x, got 0x%08x, with options %v", test.filename, test.crc32, crc32, test.options) + testing.expectf(t, crc32 == test.crc32, "%v: Expected CRC 0x%08x, got 0x%08x, with options %v", test.filename, test.crc32, crc32, test.options, loc=loc) if failed { // Don't fully print big trees. diff --git a/tests/core/math/test_core_math.odin b/tests/core/math/test_core_math.odin index 8d51c9da0..5797cb4ea 100644 --- a/tests/core/math/test_core_math.odin +++ b/tests/core/math/test_core_math.odin @@ -1012,7 +1012,7 @@ alike :: proc(t: ^testing.T, a, b: f64, loc := #caller_location) -> bool { case math.is_nan(a) && math.is_nan(b): ok = true case a == b: - ok = math.signbit(a) == math.signbit(b) + ok = math.sign_bit(a) == math.sign_bit(b) } testing.expectf(t, ok, "%.15g is not alike to %.15g", a, b, loc=loc) return ok @@ -1264,3 +1264,192 @@ test_count_digits :: proc(t: ^testing.T) { _run_test(t, 15) _run_test(t, 16) } + +@test +test_nextafter :: proc(t: ^testing.T) { + Datum :: struct($F: typeid) { + x, y, r: F, + } + + @static f16_data := [?]Datum(f16) { + {0h3c00, 0h7c42, 0h7e00}, // +1 -> +NaN = +canonical NaN + {0h3c00, 0hfc42, 0h7e00}, // +1 -> -NaN = +canonical NaN + {0hbc00, 0h7c42, 0h7e00}, // -1 -> +NaN = +canonical NaN + {0hbc00, 0hfc42, 0h7e00}, // -1 -> -NaN = +canonical NaN + + {0h7c42, 0h3c00, 0h7e00}, // +NaN -> +1 = +canonical NaN + {0hfc42, 0h3c00, 0h7e00}, // -NaN -> +1 = +canonical NaN + {0h7c42, 0hbc00, 0h7e00}, // +NaN -> -1 = +canonical NaN + {0hfc42, 0hbc00, 0h7e00}, // -NaN -> -1 = +canonical NaN + + {0h0000, 0h8000, 0h0000}, // +0 -> -0 = +0 + {0h0000, +1, 0h0001}, // +0 -> +1 = +smallest subnormal + {0h0000, -1, 0h8001}, // +0 -> -1 = -smallest subnormal + + {0h0000, 0h0000, 0h0000}, // +0 -> +0 = +0 + {0h0000, 0h8000, 0h0000}, // +0 -> -0 = +0 + {0h0000, +1, 0h0001}, // +0 -> +1 = +smallest subnormal + {0h0000, -1, 0h8001}, // +0 -> -1 = -smallest subnormal + + {0h8000, 0h0000, 0h8000}, // -0 -> +0 = -0 + {0h8000, 0h8000, 0h8000}, // -0 -> -0 = -0 + {0h8000, +1, 0h0001}, // -0 -> +1 = +smallest subnormal + {0h8000, -1, 0h8001}, // -0 -> -1 = -smallest subnormal + + {0h0001, -1, 0h0000}, // +smallest subnormal -> -1 = +0 + {0h8001, +1, 0h8000}, // -smallest subnormal -> +1 = -0 + + {0h03ff, +1, 0h0400}, // +largest subnormal -> +1 = +smallest normal + {0h0400, -1, 0h03ff}, // +smallest normal -> -1 = +largest subnormal + {0h83ff, -1, 0h8400}, // -largest subnormal -> -1 = -smallest normal + {0h8400, +1, 0h83ff}, // -smallest normal -> +1 = -largest subnormal + + {0h3c00, 0, 0h3bff}, // +1 -> 0 = +1-ulp + {0h3c00, +1, 0h3c00}, // +1 -> +1 = +1 + {0h3c00, +2, 0h3c01}, // +1 -> +2 = +1+ulp + + {0hbc00, 0, 0hbbff}, // -1 -> 0 = -1+ulp + {0hbc00, -1, 0hbc00}, // -1 -> -1 = -1 + {0hbc00, -2, 0hbc01}, // -1 -> +2 = -1-ulp + + {0h3c00, 0hfc00, 0h3bff}, // +1 -> -inf = +1-ulp + {0hbc00, 0h7c00, 0hbbff}, // -1 -> +inf = -1+ulp + + {0h7bff, 0h7c00, 0h7c00}, // +max -> +inf = +inf + {0h7c00, 0h7c00, 0h7c00}, // +inf -> +inf = +inf + {0h7c00, 0, 0h7bff}, // +inf -> 0 = +max + + {0hfbff, 0hfc00, 0hfc00}, // -max -> -inf = -inf + {0hfc00, 0hfc00, 0hfc00}, // -inf -> -inf = -inf + {0hfc00, 0, 0hfbff}, // -inf -> 0 = -max + } + + for datum in f16_data { + r := math.nextafter_f16(datum.x, datum.y) + testing.expectf(t, + transmute(u16)r == transmute(u16)datum.r, + "%h, %h -> %h != %h", datum.x, datum.y, r, datum.r) + } + + @(static, rodata) + f32_data := [?]Datum(f32) { + { +1, 0h7f842000, 0h7fc00000}, // +1 -> +NaN = +canonical NaN + { +1, 0hff842000, 0h7fc00000}, // +1 -> -NaN = +canonical NaN + { -1, 0h7f842000, 0h7fc00000}, // -1 -> +NaN = +canonical NaN + { -1, 0hff842000, 0h7fc00000}, // -1 -> -NaN = +canonical NaN + + {0h7f842000, +1, 0h7fc00000}, // +NaN -> +1 = +canonical NaN + {0hff842000, +1, 0h7fc00000}, // -NaN -> +1 = +canonical NaN + {0h7f842000, -1, 0h7fc00000}, // +NaN -> -1 = +canonical NaN + {0hff842000, -1, 0h7fc00000}, // -NaN -> -1 = +canonical NaN + + {0h00000000, 0h80000000, 0h00000000}, // +0 -> -0 = +0 + {0h00000000, +1, 0h00000001}, // +0 -> +1 = +smallest subnormal + {0h00000000, -1, 0h80000001}, // +0 -> -1 = -smallest subnormal + + {0h00000000, 0h00000000, 0h00000000}, // +0 -> +0 = +0 + {0h00000000, 0h80000000, 0h00000000}, // +0 -> -0 = +0 + {0h00000000, +1, 0h00000001}, // +0 -> +1 = +smallest subnormal + {0h00000000, -1, 0h80000001}, // +0 -> -1 = -smallest subnormal + + {0h80000000, 0h00000000, 0h80000000}, // -0 -> +0 = -0 + {0h80000000, 0h80000000, 0h80000000}, // -0 -> -0 = -0 + {0h80000000, +1, 0h00000001}, // -0 -> +1 = +smallest subnormal + {0h80000000, -1, 0h80000001}, // -0 -> -1 = -smallest subnormal + + {0h00000001, -1, 0h00000000}, // +smallest subnormal -> -1 = +0 + {0h80000001, +1, 0h80000000}, // -smallest subnormal -> +1 = -0 + + {0h03ffffff, +1, 0h04000000}, // +largest subnormal -> +1 = +smallest normal + {0h04000000, -1, 0h03ffffff}, // +smallest normal -> -1 = +largest subnormal + {0h83ffffff, -1, 0h84000000}, // -largest subnormal -> -1 = -smallest normal + {0h84000000, +1, 0h83ffffff}, // -smallest normal -> +1 = -largest subnormal + + {0h3f800000, 0, 0h3f7fffff}, // +1 -> 0 = +1-ulp + {0h3f800000, +1, 0h3f800000}, // +1 -> +1 = +1 + {0h3f800000, +2, 0h3f800001}, // +1 -> +2 = +1+ulp + + {0hbf800000, 0, 0hbf7fffff}, // -1 -> 0 = -1+ulp + {0hbf800000, -1, 0hbf800000}, // -1 -> -1 = -1 + {0hbf800000, -2, 0hbf800001}, // -1 -> +2 = -1-ulp + + {0h3c000000, 0hfc000000, 0h3bffffff}, // +1 -> -inf = +1-ulp + {0hbc000000, 0h7c000000, 0hbbffffff}, // -1 -> +inf = -1+ulp + + {0h7bffffff, 0h7c000000, 0h7c000000}, // +max -> +inf = +inf + {0h7c000000, 0h7c000000, 0h7c000000}, // +inf -> +inf = +inf + {0h7c000000, 0, 0h7bffffff}, // +inf -> 0 = +max + + {0hfbffffff, 0hfc000000, 0hfc000000}, // -max -> -inf = -inf + {0hfc000000, 0hfc000000, 0hfc000000}, // -inf -> -inf = -inf + {0hfc000000, 0, 0hfbffffff}, // -inf -> 0 = -max + } + + for datum in f32_data { + r := math.nextafter_f32(datum.x, datum.y) + testing.expectf(t, + transmute(u32)r == transmute(u32)datum.r, + "%h, %h -> %h != %h", datum.x, datum.y, r, datum.r) + } + + @(static, rodata) + f64_data := [?]Datum(f64) { + {0h3c00000000000000, 0h7ff4200000000000, 0h7ff8000000000001}, // +1 -> +NaN = +canonical NaN + {0h3c00000000000000, 0hfff4200000000000, 0h7ff8000000000001}, // +1 -> -NaN = +canonical NaN + {0hbc00000000000000, 0h7ff4200000000000, 0h7ff8000000000001}, // -1 -> +NaN = +canonical NaN + {0hbc00000000000000, 0hfff4200000000000, 0h7ff8000000000001}, // -1 -> -NaN = +canonical NaN + + {0h7ff4200000000000, 0h3c00000000000000, 0h7ff8000000000001}, // +NaN -> +1 = +canonical NaN + {0hfff4200000000000, 0h3c00000000000000, 0h7ff8000000000001}, // -NaN -> +1 = +canonical NaN + {0h7ff4200000000000, 0hbc00000000000000, 0h7ff8000000000001}, // +NaN -> -1 = +canonical NaN + {0hfff4200000000000, 0hbc00000000000000, 0h7ff8000000000001}, // -NaN -> -1 = +canonical NaN + + {0h0000000000000000, 0h8000000000000000, 0h0000000000000000}, // +0 -> -0 = +0 + {0h0000000000000000, +1, 0h0000000000000001}, // +0 -> +1 = +smallest subnormal + {0h0000000000000000, -1, 0h8000000000000001}, // +0 -> -1 = -smallest subnormal + + {0h0000000000000000, 0h0000000000000000, 0h0000000000000000}, // +0 -> +0 = +0 + {0h0000000000000000, 0h8000000000000000, 0h0000000000000000}, // +0 -> -0 = +0 + {0h0000000000000000, +1, 0h0000000000000001}, // +0 -> +1 = +smallest subnormal + {0h0000000000000000, -1, 0h8000000000000001}, // +0 -> -1 = -smallest subnormal + + {0h8000000000000000, 0h0000000000000000, 0h8000000000000000}, // -0 -> +0 = -0 + {0h8000000000000000, 0h8000000000000000, 0h8000000000000000}, // -0 -> -0 = -0 + {0h8000000000000000, +1, 0h0000000000000001}, // -0 -> +1 = +smallest subnormal + {0h8000000000000000, -1, 0h8000000000000001}, // -0 -> -1 = -smallest subnormal + + {0h0000000000000001, -1, 0h0000000000000000}, // +smallest subnormal -> -1 = +0 + {0h8000000000000001, +1, 0h8000000000000000}, // -smallest subnormal -> +1 = -0 + + {0h03ffffffffffffff, +1, 0h0400000000000000}, // +largest subnormal -> +1 = +smallest normal + {0h0400000000000000, -1, 0h03ffffffffffffff}, // +smallest normal -> -1 = +largest subnormal + {0h83ffffffffffffff, -1, 0h8400000000000000}, // -largest subnormal -> -1 = -smallest normal + {0h8400000000000000, +1, 0h83ffffffffffffff}, // -smallest normal -> +1 = -largest subnormal + + {0h3ff0000000000000, 0, 0h3fefffffffffffff}, // +1 -> 0 = +1-ulp + {0h3ff0000000000000, +1, 0h3ff0000000000000}, // +1 -> +1 = +1 + {0h3ff0000000000000, +2, 0h3ff0000000000001}, // +1 -> +2 = +1+ulp + + {0hbff0000000000000, 0, 0hbfefffffffffffff}, // -1 -> 0 = -1+ulp + {0hbff0000000000000, -1, 0hbff0000000000000}, // -1 -> -1 = -1 + {0hbff0000000000000, -2, 0hbff0000000000001}, // -1 -> +2 = -1-ulp + + {0h3ff0000000000000, 0hfff0000000000000, 0h3fefffffffffffff}, // +1 -> -inf = +1-ulp + {0hbff0000000000000, 0h7ff0000000000000, 0hbfefffffffffffff}, // -1 -> +inf = -1+ulp + + {0h7fefffffffffffff, 0h7ff0000000000000, 0h7ff0000000000000}, // +max -> +inf = +inf + {0h7ff0000000000000, 0h7ff0000000000000, 0h7ff0000000000000}, // +inf -> +inf = +inf + {0h7ff0000000000000, 0, 0h7fefffffffffffff}, // +inf -> 0 = +max + + {0hffefffffffffffff, 0hfff0000000000000, 0hfff0000000000000}, // -max -> -inf = -inf + {0hfff0000000000000, 0hfff0000000000000, 0hfff0000000000000}, // -inf -> -inf = -inf + {0hfff0000000000000, 0, 0hffefffffffffffff}, // -inf -> 0 = -max + } + + for datum in f64_data { + r := math.nextafter_f64(datum.x, datum.y) + testing.expectf(t, + transmute(u64)r == transmute(u64)datum.r, + "%h, %h -> %h != %h", datum.x, datum.y, r, datum.r) + } +} diff --git a/tests/core/mem/test_core_mem.odin b/tests/core/mem/test_core_mem.odin index 4f2095fca..bd072b4e9 100644 --- a/tests/core/mem/test_core_mem.odin +++ b/tests/core/mem/test_core_mem.odin @@ -1,8 +1,10 @@ package test_core_mem +import "core:mem" import "core:mem/tlsf" import "core:mem/virtual" import "core:testing" +import "core:slice" @test test_tlsf_bitscan :: proc(t: ^testing.T) { @@ -54,3 +56,140 @@ test_align_bumping_block_limit :: proc(t: ^testing.T) { testing.expect_value(t, err, nil) testing.expect(t, len(data) == 896) } + +@(test) +tlsf_test_overlap_and_zero :: proc(t: ^testing.T) { + default_allocator := context.allocator + alloc: tlsf.Allocator + defer tlsf.destroy(&alloc) + + NUM_ALLOCATIONS :: 1_000 + BACKING_SIZE :: NUM_ALLOCATIONS * (1_000 + size_of(uintptr)) + + if err := tlsf.init_from_allocator(&alloc, default_allocator, BACKING_SIZE); err != .None { + testing.fail_now(t, "TLSF init error") + } + context.allocator = tlsf.allocator(&alloc) + + allocations := make([dynamic][]byte, 0, NUM_ALLOCATIONS, default_allocator) + defer delete(allocations) + + err: mem.Allocator_Error + s: []byte + + for size := 1; err == .None && size <= NUM_ALLOCATIONS; size += 1 { + s, err = make([]byte, size) + append(&allocations, s) + } + + slice.sort_by(allocations[:], proc(a, b: []byte) -> bool { + return uintptr(raw_data(a)) < uintptr(raw_data((b))) + }) + + for i in 0.. bool { + return uintptr(raw_data(a)) < uintptr(raw_data((b))) + }) + + for i in 0..= 10) + + free_all(tlsf.allocator(&alloc)) + + for { + s := make([]byte, ALLOCATION_SIZE) or_break + append(&allocations[1], s) + } + testing.expect(t, len(allocations[1]) >= 10) + + for i in 0..= b_end && b_end >= a_start { + testing.fail_now(t, "Allocations overlapped") + } +} diff --git a/tests/core/net/test_core_net_freebsd.odin b/tests/core/net/test_core_net_freebsd.odin index 39e364e80..590db7de0 100644 --- a/tests/core/net/test_core_net_freebsd.odin +++ b/tests/core/net/test_core_net_freebsd.odin @@ -17,9 +17,6 @@ import "core:net" import "core:time" import "core:testing" -ENDPOINT_DUPLICATE_BINDING := net.Endpoint{net.IP4_Address{127, 0, 0, 1}, 11000} -ENDPOINT_EPIPE_TEST := net.Endpoint{net.IP4_Address{127, 0, 0, 1}, 11001} - @test test_duplicate_binding :: proc(t: ^testing.T) { // FreeBSD has the capacity to permit multiple processes and sockets to @@ -35,11 +32,16 @@ test_duplicate_binding :: proc(t: ^testing.T) { if !testing.expect_value(t, err_set1, nil) { return } - err_bind1 := net.bind(tcp_socket1, ENDPOINT_DUPLICATE_BINDING) + err_bind1 := net.bind(tcp_socket1, {net.IP4_Loopback, 0}) if !testing.expect_value(t, err_bind1, nil) { return } + ep, err_bound := net.bound_endpoint(tcp_socket1) + if !testing.expect_value(t, err_bound, nil) { + return + } + raw_socket2, err_create2 := net.create_socket(.IP4, .TCP) if !testing.expect_value(t, err_create2, nil) { return @@ -50,7 +52,7 @@ test_duplicate_binding :: proc(t: ^testing.T) { if !testing.expect_value(t, err_set2, nil) { return } - err_bind2 := net.bind(tcp_socket2, ENDPOINT_DUPLICATE_BINDING) + err_bind2 := net.bind(tcp_socket2, ep) if !testing.expect_value(t, err_bind2, nil) { return } @@ -60,13 +62,18 @@ test_duplicate_binding :: proc(t: ^testing.T) { test_sigpipe_bypass :: proc(t: ^testing.T) { // If the internals aren't working as expected, this test will fail by raising SIGPIPE. - server_socket, listen_err := net.listen_tcp(ENDPOINT_EPIPE_TEST) + server_socket, listen_err := net.listen_tcp({net.IP4_Loopback, 0}) if !testing.expect_value(t, listen_err, nil) { return } defer net.close(server_socket) - client_socket, dial_err := net.dial_tcp(ENDPOINT_EPIPE_TEST) + ep, bound_err := net.bound_endpoint(server_socket) + if !testing.expect_value(t, bound_err, nil) { + return + } + + client_socket, dial_err := net.dial_tcp(ep) if !testing.expect_value(t, dial_err, nil) { return } @@ -80,7 +87,7 @@ test_sigpipe_bypass :: proc(t: ^testing.T) { data := "Hellope!" bytes_written, err_send := net.send(client_socket, transmute([]u8)data) - if !testing.expect_value(t, err_send, net.TCP_Send_Error.Cannot_Send_More_Data) { + if !testing.expect_value(t, err_send, net.TCP_Send_Error.Connection_Closed) { return } if !testing.expect_value(t, bytes_written, 0) { diff --git a/tests/core/os/os2/path.odin b/tests/core/os/os2/path.odin index 2cf1f1f1c..7b1cb0146 100644 --- a/tests/core/os/os2/path.odin +++ b/tests/core/os/os2/path.odin @@ -36,47 +36,58 @@ posix_to_dos_path :: proc(path: string) -> string { @(test) test_clean_path :: proc(t: ^testing.T) { Test_Case :: struct{ - path: string, + path: string, expected: string, } - test_cases := [?]Test_Case { - {`../../foo/../../`, `../../..`}, - {`../../foo/..`, `../..`}, - {`../../foo`, `../../foo`}, - {`../..`, `../..`}, - {`.././foo`, `../foo`}, - {`..`, `..`}, - {`.`, `.`}, - {`.foo`, `.foo`}, - {`/../../foo/../../`, `/`}, - {`/../`, `/`}, - {`/..`, `/`}, - {`/`, `/`}, - {`//home/foo/bar/../../`, `/home`}, - {`/a/../..`, `/`}, - {`/a/../`, `/`}, - {`/a/あ`, `/a/あ`}, - {`/a/あ/..`, `/a`}, - {`/あ/a/..`, `/あ`}, - {`/あ/a/../あ`, `/あ/あ`}, - {`/home/../`, `/`}, - {`/home/..`, `/`}, - {`/home/foo/../../usr`, `/usr`}, - {`/home/foo/../..`, `/`}, - {`/home/foo/../`, `/home`}, - {``, `.`}, - {`a/..`, `.`}, - {`a`, `a`}, - {`abc//.//../foo`, `foo`}, - {`foo`, `foo`}, - {`home/foo/bar/../../`, `home`}, - } - when ODIN_OS == .Windows { - for &tc in test_cases { - tc.path = posix_to_dos_path(tc.path) - tc.expected = posix_to_dos_path(tc.expected) + test_cases := [?]Test_Case { + {`W:/odin\examples\demo/demo.odin`, `W:\odin\examples\demo\demo.odin`}, + {`\\server\share\path\file.ext`, `\\server\share\path\file.ext`}, + {`//server\share/path\file.ext`, `\\server\share\path\file.ext`}, + {`/\192.168.0.10\share/path\file.ext`, `\\192.168.0.10\share\path\file.ext`}, + {`\\?\C:/Users/Foo/path\file.ext`, `\\?\C:\Users\Foo\path\file.ext`}, + {`\\?\\localhost\share\file.ext`, `\\?\\localhost\share\file.ext`}, + {`//?\/192.168.0.10\share\file.ext`, `\\?\\192.168.0.10\share\file.ext`}, + {`\\.\PhysicalDrive3`, `\\.\PhysicalDrive3`}, + {`/\./PhysicalDrive3`, `\\.\PhysicalDrive3`}, + {`C:\a\..\..`, `C:\`}, + {`C:\a\..`, `C:\`}, + {`C:\あ/a/..`, `C:\あ`}, + {`C:\あ/a/../あ`, `C:\あ\あ`}, + } + } else { + test_cases := [?]Test_Case { + {`../../foo/../../`, `../../..`}, + {`../../foo/..`, `../..`}, + {`../../foo`, `../../foo`}, + {`../..`, `../..`}, + {`.././foo`, `../foo`}, + {`..`, `..`}, + {`.`, `.`}, + {`.foo`, `.foo`}, + {`/../../foo/../../`, `/`}, + {`/../`, `/`}, + {`/..`, `/`}, + {`/`, `/`}, + {`//home/foo/bar/../../`, `/home`}, + {`/a/../..`, `/`}, + {`/a/../`, `/`}, + {`/a/あ`, `/a/あ`}, + {`/a/あ/..`, `/a`}, + {`/あ/a/..`, `/あ`}, + {`/あ/a/../あ`, `/あ/あ`}, + {`/home/../`, `/`}, + {`/home/..`, `/`}, + {`/home/foo/../../usr`, `/usr`}, + {`/home/foo/../..`, `/`}, + {`/home/foo/../`, `/home`}, + {``, `.`}, + {`a/..`, `.`}, + {`a`, `a`}, + {`abc//.//../foo`, `foo`}, + {`foo`, `foo`}, + {`home/foo/bar/../../`, `home`}, } } diff --git a/tests/core/os/os2/process.odin b/tests/core/os/os2/process.odin index d7700d201..c530b4c79 100644 --- a/tests/core/os/os2/process.odin +++ b/tests/core/os/os2/process.odin @@ -1,3 +1,4 @@ +#+build !windows package tests_core_os_os2 import os "core:os/os2" diff --git a/tests/core/reflect/test_core_reflect.odin b/tests/core/reflect/test_core_reflect.odin index 7d2394688..bc2d01a4a 100644 --- a/tests/core/reflect/test_core_reflect.odin +++ b/tests/core/reflect/test_core_reflect.odin @@ -1,6 +1,7 @@ // Tests "core:reflect/reflect". package test_core_reflect +import "base:intrinsics" import "core:reflect" import "core:testing" @@ -260,4 +261,74 @@ test_as_f64 :: proc(t: ^testing.T) { testing.expectf(t, r == d.e, "f64 %v -> %v != %v", d.v, r, d.e) } } +} + +@test +test_simd_vectors :: proc(t: ^testing.T) { + { + V :: #simd[2]u64 + v: V + E := typeid_of(u64) + + testing.expect(t, typeid_of(intrinsics.type_elem_type(V)) == E) + testing.expect(t, reflect.typeid_elem(V) == E) + testing.expect(t, reflect.length(v) == len(V)) + testing.expect(t, reflect.capacity(v) == cap(V)) + testing.expect(t, reflect.length(v) == 2) + } + { + V :: #simd[4]f32 + v: V + E := typeid_of(f32) + + testing.expect(t, typeid_of(intrinsics.type_elem_type(V)) == E) + testing.expect(t, reflect.typeid_elem(V) == E) + testing.expect(t, reflect.length(v) == len(V)) + testing.expect(t, reflect.capacity(v) == cap(V)) + testing.expect(t, reflect.length(v) == 4) + } + { + V :: #simd[8]i16 + v: V + E := typeid_of(i16) + + testing.expect(t, typeid_of(intrinsics.type_elem_type(V)) == E) + testing.expect(t, reflect.typeid_elem(V) == E) + testing.expect(t, reflect.length(v) == len(V)) + testing.expect(t, reflect.capacity(v) == cap(V)) + testing.expect(t, reflect.length(v) == 8) + } + { + V :: #simd[16]u32 + v: V + E := typeid_of(u32) + + testing.expect(t, typeid_of(intrinsics.type_elem_type(V)) == E) + testing.expect(t, reflect.typeid_elem(V) == E) + testing.expect(t, reflect.length(v) == len(V)) + testing.expect(t, reflect.capacity(v) == cap(V)) + testing.expect(t, reflect.length(v) == 16) + } + { + V :: #simd[32]u16 + v: V + E := typeid_of(u16) + + testing.expect(t, typeid_of(intrinsics.type_elem_type(V)) == E) + testing.expect(t, reflect.typeid_elem(V) == E) + testing.expect(t, reflect.length(v) == len(V)) + testing.expect(t, reflect.capacity(v) == cap(V)) + testing.expect(t, reflect.length(v) == 32) + } + { + V :: #simd[64]i8 + v: V + E := typeid_of(i8) + + testing.expect(t, typeid_of(intrinsics.type_elem_type(V)) == E) + testing.expect(t, reflect.typeid_elem(V) == E) + testing.expect(t, reflect.length(v) == len(V)) + testing.expect(t, reflect.capacity(v) == cap(V)) + testing.expect(t, reflect.length(v) == 64) + } } \ No newline at end of file diff --git a/tests/core/strconv/test_core_strconv.odin b/tests/core/strconv/test_core_strconv.odin index 6b70654cc..8266ece23 100644 --- a/tests/core/strconv/test_core_strconv.odin +++ b/tests/core/strconv/test_core_strconv.odin @@ -30,6 +30,43 @@ test_float :: proc(t: ^testing.T) { testing.expect_value(t, n, 0) testing.expect_value(t, ok, false) + f, ok = strconv.parse_f64("0", &n) + testing.expect_value(t, f, 0) + testing.expect_value(t, n, 1) + testing.expect_value(t, ok, true) + + f, ok = strconv.parse_f64("0h", &n) + testing.expect_value(t, f, 0) + testing.expect_value(t, n, 1) + testing.expect_value(t, ok, false) + + f, ok = strconv.parse_f64("0h1", &n) + testing.expect_value(t, f, 0) + testing.expect_value(t, n, 3) + testing.expect_value(t, ok, false) + + f, ok = strconv.parse_f64("0h0000_0001", &n) + testing.expect_value(t, f, 0h0000_0001) + testing.expect_value(t, n, 11) + testing.expect_value(t, ok, true) + + f, ok = strconv.parse_f64("0h4c60", &n) + testing.expect_value(t, f, 0h4c60) + testing.expect_value(t, f, 17.5) + testing.expect_value(t, n, 6) + testing.expect_value(t, ok, true) + + f, ok = strconv.parse_f64("0h418c0000", &n) + testing.expect_value(t, f, 0h418c0000) + testing.expect_value(t, f, 17.5) + testing.expect_value(t, n, 10) + testing.expect_value(t, ok, true) + + f, ok = strconv.parse_f64("0h4031_8000_0000_0000", &n) + testing.expect_value(t, f, 0h4031800000000000) + testing.expect_value(t, f, f64(17.5)) + testing.expect_value(t, n, 21) + testing.expect_value(t, ok, true) } @(test) diff --git a/tests/core/text/regex/test_core_text_regex.odin b/tests/core/text/regex/test_core_text_regex.odin index 3e7145406..913e716e5 100644 --- a/tests/core/text/regex/test_core_text_regex.odin +++ b/tests/core/text/regex/test_core_text_regex.odin @@ -72,6 +72,18 @@ expect_error :: proc(t: ^testing.T, pattern: string, expected_error: typeid, fla testing.expect_value(t, variant_ti, expected_ti, loc = loc) } +check_capture :: proc(t: ^testing.T, got, expected: regex.Capture, loc := #caller_location) { + testing.expect_value(t, len(got.pos), len(got.groups), loc = loc) + testing.expect_value(t, len(got.pos), len(expected.pos), loc = loc) + testing.expect_value(t, len(got.groups), len(expected.groups), loc = loc) + + if len(got.pos) == len(expected.pos) { + for i in 0..= len(test.expected) { + break + } + check_capture(t, capture, test.expected[idx]) + } + testing.expect_value(t, it.idx, len(test.expected)) + } +} \ No newline at end of file diff --git a/tests/internal/test_global_any.odin b/tests/internal/test_global_any.odin new file mode 100644 index 000000000..73b70e0a4 --- /dev/null +++ b/tests/internal/test_global_any.odin @@ -0,0 +1,40 @@ +package test_internal + +@(private="file") +global_any_from_proc: any = from_proc() + +from_proc :: proc() -> f32 { + return 1.1 +} + +@(private="file") +global_any: any = 1 + +import "core:testing" + +@(test) +test_global_any :: proc(t: ^testing.T) { + as_f32, is_f32 := global_any_from_proc.(f32) + testing.expect(t, is_f32 == true) + testing.expect(t, as_f32 == 1.1) + + as_int, is_int := global_any.(int) + testing.expect(t, is_int == true) + testing.expect(t, as_int == 1) +} + +@(test) +test_static_any :: proc(t: ^testing.T) { + @(static) + var: any = 3 + + as_int, is_int := var.(int) + testing.expect(t, is_int == true) + testing.expect(t, as_int == 3) + + var = f32(1.1) + + as_f32, is_f32 := var.(f32) + testing.expect(t, is_f32 == true) + testing.expect(t, as_f32 == 1.1) +} diff --git a/tests/internal/test_intrinsics_integer_to.odin b/tests/internal/test_intrinsics_integer_to.odin new file mode 100644 index 000000000..108318c9a --- /dev/null +++ b/tests/internal/test_intrinsics_integer_to.odin @@ -0,0 +1,34 @@ +package test_internal + +import "base:intrinsics" +import "core:testing" + +/* +example_usage :: proc(#any_int x: int) -> intrinsics.type_integer_to_unsigned(type_of(x)) { + T :: intrinsics.type_integer_to_unsigned(type_of(x)) + return 1< NaN, false) + testing.expect_value(t, NaN >= NaN, false) +} + +@(test) +compare_constant_nans_f64 :: proc(t: ^testing.T) { + NaN :: f64(0h7fff_0000_0000_0000) + NaN2 :: f64(0h7fff_0000_0000_0001) + Inf :: f64(0h7FF0_0000_0000_0000) + Neg_Inf :: f64(0hFFF0_0000_0000_0000) + + testing.expect_value(t, NaN == NaN, false) + testing.expect_value(t, NaN == NaN2, false) + testing.expect_value(t, NaN != 0, true) + testing.expect_value(t, NaN != 5, true) + testing.expect_value(t, NaN != -5, true) + testing.expect_value(t, NaN != NaN, true) + testing.expect_value(t, NaN != NaN2, true) + testing.expect_value(t, NaN != Inf, true) + testing.expect_value(t, NaN != Neg_Inf, true) + testing.expect_value(t, NaN < NaN, false) + testing.expect_value(t, NaN <= NaN, false) + testing.expect_value(t, NaN > NaN, false) + testing.expect_value(t, NaN >= NaN, false) +} + +@(test) +compare_variable_nans_f32 :: proc(t: ^testing.T) { + NaN := f32(0h7fc0_0000) + NaN2 := f32(0h7fc0_0001) + Inf := f32(0h7F80_0000) + Neg_Inf := f32(0hFF80_0000) + + testing.expect_value(t, NaN == NaN, false) + testing.expect_value(t, NaN == NaN2, false) + testing.expect_value(t, NaN != 0, true) + testing.expect_value(t, NaN != 5, true) + testing.expect_value(t, NaN != -5, true) + testing.expect_value(t, NaN != NaN, true) + testing.expect_value(t, NaN != NaN2, true) + testing.expect_value(t, NaN != Inf, true) + testing.expect_value(t, NaN != Neg_Inf, true) + testing.expect_value(t, NaN < NaN, false) + testing.expect_value(t, NaN <= NaN, false) + testing.expect_value(t, NaN > NaN, false) + testing.expect_value(t, NaN >= NaN, false) +} + +@(test) +compare_variable_nans_f64 :: proc(t: ^testing.T) { + NaN := f64(0h7fff_0000_0000_0000) + NaN2 := f64(0h7fff_0000_0000_0001) + Inf := f64(0h7FF0_0000_0000_0000) + Neg_Inf := f64(0hFFF0_0000_0000_0000) + + testing.expect_value(t, NaN == NaN, false) + testing.expect_value(t, NaN == NaN2, false) + testing.expect_value(t, NaN != 0, true) + testing.expect_value(t, NaN != 5, true) + testing.expect_value(t, NaN != -5, true) + testing.expect_value(t, NaN != NaN, true) + testing.expect_value(t, NaN != NaN2, true) + testing.expect_value(t, NaN != Inf, true) + testing.expect_value(t, NaN != Neg_Inf, true) + testing.expect_value(t, NaN < NaN, false) + testing.expect_value(t, NaN <= NaN, false) + testing.expect_value(t, NaN > NaN, false) + testing.expect_value(t, NaN >= NaN, false) +} diff --git a/tests/issues/run.bat b/tests/issues/run.bat index 7ed43205d..267a2e030 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -17,6 +17,8 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style ..\..\..\odin test ..\test_issue_2666.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_4210.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_4584.odin %COMMON% || exit /b +..\..\..\odin build ..\test_issue_5043.odin %COMMON% || exit /b +..\..\..\odin build ..\test_issue_5097.odin %COMMON% || exit /b @echo off diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 54543980e..5102ee307 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -24,6 +24,8 @@ else echo "SUCCESSFUL 0/1" exit 1 fi +$ODIN build ../test_issue_5043.odin $COMMON +$ODIN build ../test_issue_5097.odin $COMMON set +x diff --git a/tests/issues/test_issue_5043.odin b/tests/issues/test_issue_5043.odin new file mode 100644 index 000000000..ce36bf674 --- /dev/null +++ b/tests/issues/test_issue_5043.odin @@ -0,0 +1,23 @@ +// Tests issue #5043 https://github.com/odin-lang/Odin/issues/5043 +package test_issues + +Table :: map [string] Type +List :: [dynamic] Type + +Type :: union { + ^Table, + ^List, + i64, +} + + +main :: proc() { + v: Type = 5 + + switch t in v { + case ^Table: // or case ^map [string] Type: + case ^List: + case i64: + + } +} diff --git a/tests/issues/test_issue_5097.odin b/tests/issues/test_issue_5097.odin new file mode 100644 index 000000000..57ef60b8d --- /dev/null +++ b/tests/issues/test_issue_5097.odin @@ -0,0 +1,15 @@ +// Tests issue #5097 https://github.com/odin-lang/Odin/issues/5097 +package test_issues + +Node_Ptr :: ^Node // the typedef... + +Node :: struct { + prev, next: Node_Ptr, // replacing the type with ^Node also fixes it +} + +// ...if placed here, it works just fine + +main :: proc() { + node: Node_Ptr + _ = node +} diff --git a/vendor/box2d/box2d.odin b/vendor/box2d/box2d.odin index 8abf6ce03..640f430b8 100644 --- a/vendor/box2d/box2d.odin +++ b/vendor/box2d/box2d.odin @@ -53,26 +53,7 @@ Version :: struct { revision: i32, // Bug fixes } -when ODIN_OS == .Windows { - // Timer for profiling. This has platform specific code and may - // not work on every platform. - Timer :: struct { - start: i64, - } -} else when ODIN_OS == .Linux || ODIN_OS == .Darwin { - // Timer for profiling. This has platform specific code and may - // not work on every platform. - Timer :: struct { - start_sec: u64, - start_usec: u64, - } -} else { - // Timer for profiling. This has platform specific code and may - // not work on every platform. - Timer :: struct { - dummy: i32, - } -} +HASH_INIT :: 5381 @(link_prefix="b2", default_calling_convention="c", require_results) foreign lib { @@ -85,19 +66,33 @@ foreign lib { // @param assertFcn a non-null assert callback SetAssertFcn :: proc(assertfcn: AssertFcn) --- - - CreateTimer :: proc() -> Timer --- - GetTicks :: proc(timer: ^Timer) -> i64 --- - GetMilliseconds :: proc(#by_ptr timer: Timer) -> f32 --- - GetMillisecondsAndReset :: proc(timer: ^Timer) -> f32 --- - SleepMilliseconds :: proc(milliseconds: c.int) --- + // Get the absolute number of system ticks. The value is platform specific. + GetTicks :: proc() -> u64 --- + // Get the milliseconds passed from an initial tick value. + GetMilliseconds :: proc(ticks: u64) -> f32 --- + // Get the milliseconds passed from an initial tick value. Resets the passed in + // value to the current tick value. + GetMillisecondsAndReset :: proc(ticks: ^u64) -> f32 --- + // Yield to be used in a busy loop. Yield :: proc() --- + // Simple djb2 hash function for determinism testing. + Hash :: proc(hash: u32, data: [^]byte, count: c.int) -> u32 --- // Box2D bases all length units on meters, but you may need different units for your game. // You can set this value to use different units. This should be done at application startup - // and only modified once. Default value is 1. - // @warning This must be modified before any calls to Box2D + // and only modified once. Default value is 1. + // For example, if your game uses pixels for units you can use pixels for all length values + // sent to Box2D. There should be no extra cost. However, Box2D has some internal tolerances + // and thresholds that have been tuned for meters. By calling this function, Box2D is able + // to adjust those tolerances and thresholds to improve accuracy. + // A good rule of thumb is to pass the height of your player character to this function. So + // if your player character is 32 pixels high, then pass 32 to this function. Then you may + // confidently use pixels for all the length values sent to Box2D. All length values returned + // from Box2D will also be pixels because Box2D does not do any scaling internally. + // However, you are now on the hook for coming up with good values for gravity, density, and + // forces. + // @warning This must be modified before any calls to Box2D SetLengthUnitsPerMeter :: proc(lengthUnits: f32) --- // Get the current length units per meter. @@ -122,6 +117,10 @@ foreign lib { // @ingroup shape DefaultQueryFilter :: proc() -> QueryFilter --- + // Use this to initialize your surface material + // @ingroup shape + DefaultSurfaceMaterial :: proc() -> SurfaceMaterial --- + // Use this to initialize your shape definition // @ingroup shape DefaultShapeDef :: proc() -> ShapeDef --- @@ -142,6 +141,10 @@ foreign lib { // @ingroup mouse_joint DefaultMouseJointDef :: proc() -> MouseJointDef --- + // Use this to initialize your joint definition + // @ingroup filter_joint + DefaultFilterJointDef :: proc() -> FilterJointDef --- + // Use this to initialize your joint definition // @ingroupd prismatic_joint DefaultPrismaticJointDef :: proc() -> PrismaticJointDef --- @@ -157,6 +160,14 @@ foreign lib { // Use this to initialize your joint definition // @ingroup wheel_joint DefaultWheelJointDef :: proc() -> WheelJointDef --- + + // Use this to initialize your explosion definition + // @ingroup world + DefaultExplosionDef :: proc() -> ExplosionDef --- + + // Use this to initialize your drawing interface. This allows you to implement a sub-set + // of the drawing functions. + DefaultDebugDraw :: proc() -> DebugDraw --- } @@ -164,85 +175,92 @@ foreign lib { @(link_prefix="b2", default_calling_convention="c", require_results) foreign lib { // Validate ray cast input data (NaN, etc) - IsValidRay :: proc(#by_ptr input: RayCastInput) -> bool --- + IsValidRay :: proc(#by_ptr input: RayCastInput) -> bool --- // Make a convex polygon from a convex hull. This will assert if the hull is not valid. // @warning Do not manually fill in the hull data, it must come directly from b2ComputeHull - MakePolygon :: proc(#by_ptr hull: Hull, radius: f32) -> Polygon --- + MakePolygon :: proc(#by_ptr hull: Hull, radius: f32) -> Polygon --- // Make an offset convex polygon from a convex hull. This will assert if the hull is not valid. // @warning Do not manually fill in the hull data, it must come directly from b2ComputeHull - MakeOffsetPolygon :: proc(#by_ptr hull: Hull, radius: f32, transform: Transform) -> Polygon --- + MakeOffsetPolygon :: proc(#by_ptr hull: Hull, position: Vec2, rotation: Rot) -> Polygon --- + + // Make an offset convex polygon from a convex hull. This will assert if the hull is not valid. + // @warning Do not manually fill in the hull data, it must come directly from b2ComputeHull + MakeOffsetRoundedPolygon :: proc(#by_ptr hull: Hull, position: Vec2, rotation: Rot, radius: f32) -> Polygon --- // Make a square polygon, bypassing the need for a convex hull. - MakeSquare :: proc(h: f32) -> Polygon --- + MakeSquare :: proc(halfWidth: f32) -> Polygon --- // Make a box (rectangle) polygon, bypassing the need for a convex hull. - MakeBox :: proc(hx, hy: f32) -> Polygon --- + MakeBox :: proc(halfWidth, halfHeight: f32) -> Polygon --- // Make a rounded box, bypassing the need for a convex hull. - MakeRoundedBox :: proc(hx, hy: f32, radius: f32) -> Polygon --- + MakeRoundedBox :: proc(halfWidth, halfHeight: f32, radius: f32) -> Polygon --- // Make an offset box, bypassing the need for a convex hull. - MakeOffsetBox :: proc(hx, hy: f32, center: Vec2, angle: f32) -> Polygon --- + MakeOffsetBox :: proc(halfWidth, halfHeight: f32, center: Vec2, rotation: Rot) -> Polygon --- + + // Make an offset rounded box, bypassing the need for a convex hull. + MakeOffsetRoundedBox :: proc(halfWidth, halfHeight: f32, center: Vec2, rotation: Rot, radius: f32) -> Polygon --- // Transform a polygon. This is useful for transferring a shape from one body to another. - TransformPolygon :: proc(transform: Transform, #by_ptr polygon: Polygon) -> Polygon --- + TransformPolygon :: proc(transform: Transform, #by_ptr polygon: Polygon) -> Polygon --- // Compute mass properties of a circle - ComputeCircleMass :: proc(#by_ptr shape: Circle, density: f32) -> MassData --- + ComputeCircleMass :: proc(#by_ptr shape: Circle, density: f32) -> MassData --- // Compute mass properties of a capsule - ComputeCapsuleMass :: proc(#by_ptr shape: Capsule, density: f32) -> MassData --- + ComputeCapsuleMass :: proc(#by_ptr shape: Capsule, density: f32) -> MassData --- // Compute mass properties of a polygon - ComputePolygonMass :: proc(#by_ptr shape: Polygon, density: f32) -> MassData --- + ComputePolygonMass :: proc(#by_ptr shape: Polygon, density: f32) -> MassData --- // Compute the bounding box of a transformed circle - ComputeCircleAABB :: proc(#by_ptr shape: Circle, transform: Transform) -> AABB --- + ComputeCircleAABB :: proc(#by_ptr shape: Circle, transform: Transform) -> AABB --- // Compute the bounding box of a transformed capsule - ComputeCapsuleAABB :: proc(#by_ptr shape: Capsule, transform: Transform) -> AABB --- + ComputeCapsuleAABB :: proc(#by_ptr shape: Capsule, transform: Transform) -> AABB --- // Compute the bounding box of a transformed polygon - ComputePolygonAABB :: proc(#by_ptr shape: Polygon, transform: Transform) -> AABB --- + ComputePolygonAABB :: proc(#by_ptr shape: Polygon, transform: Transform) -> AABB --- // Compute the bounding box of a transformed line segment - ComputeSegmentAABB :: proc(#by_ptr shape: Segment, transform: Transform) -> AABB --- + ComputeSegmentAABB :: proc(#by_ptr shape: Segment, transform: Transform) -> AABB --- // Test a point for overlap with a circle in local space - PointInCircle :: proc(point: Vec2, #by_ptr shape: Circle) -> bool --- + PointInCircle :: proc(point: Vec2, #by_ptr shape: Circle) -> bool --- // Test a point for overlap with a capsule in local space - PointInCapsule :: proc(point: Vec2, #by_ptr shape: Capsule) -> bool --- + PointInCapsule :: proc(point: Vec2, #by_ptr shape: Capsule) -> bool --- // Test a point for overlap with a convex polygon in local space - PointInPolygon :: proc(point: Vec2, #by_ptr shape: Polygon) -> bool --- + PointInPolygon :: proc(point: Vec2, #by_ptr shape: Polygon) -> bool --- // Ray cast versus circle in shape local space. Initial overlap is treated as a miss. - RayCastCircle :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Circle) -> CastOutput --- + RayCastCircle :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Circle) -> CastOutput --- // Ray cast versus capsule in shape local space. Initial overlap is treated as a miss. - RayCastCapsule :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Capsule) -> CastOutput --- + RayCastCapsule :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Capsule) -> CastOutput --- // Ray cast versus segment in shape local space. Optionally treat the segment as one-sided with hits from // the left side being treated as a miss. - RayCastSegment :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Segment, oneSided: bool) -> CastOutput --- + RayCastSegment :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Segment, oneSided: bool) -> CastOutput --- // Ray cast versus polygon in shape local space. Initial overlap is treated as a miss. - RayCastPolygon :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Polygon) -> CastOutput --- + RayCastPolygon :: proc(#by_ptr input: RayCastInput, #by_ptr shape: Polygon) -> CastOutput --- // Shape cast versus a circle. Initial overlap is treated as a miss. - ShapeCastCircle :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Circle) -> CastOutput --- + ShapeCastCircle :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Circle) -> CastOutput --- // Shape cast versus a capsule. Initial overlap is treated as a miss. - ShapeCastCapsule :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Capsule) -> CastOutput --- + ShapeCastCapsule :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Capsule) -> CastOutput --- // Shape cast versus a line segment. Initial overlap is treated as a miss. - ShapeCastSegment :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Segment) -> CastOutput --- + ShapeCastSegment :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Segment) -> CastOutput --- // Shape cast versus a convex polygon. Initial overlap is treated as a miss. - ShapeCastPolygon :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Polygon) -> CastOutput --- + ShapeCastPolygon :: proc(#by_ptr input: ShapeCastInput, #by_ptr shape: Polygon) -> CastOutput --- } @@ -251,7 +269,7 @@ foreign lib { // - all points very close together // - all points on a line // - less than 3 points -// - more than maxPolygonVertices points +// - more than MAX_POLYGON_VERTICES points // This welds close points and removes collinear points. // @warning Do not modify a hull once it has been computed @(require_results) @@ -279,30 +297,40 @@ foreign lib { } // Compute the closest points between two shapes represented as point clouds. -// DistanceCache cache is input/output. On the first call set DistanceCache.count to zero. +// SimplexCache cache is input/output. On the first call set SimplexCache.count to zero. // The underlying GJK algorithm may be debugged by passing in debug simplexes and capacity. You may pass in NULL and 0 for these. @(require_results) -ShapeDistance :: proc "c" (cache: ^DistanceCache, #by_ptr input: DistanceInput, simplexes: []Simplex) -> DistanceOutput { +ShapeDistance :: proc "c" (#by_ptr input: DistanceInput, cache: ^SimplexCache, simplexes: []Simplex) -> DistanceOutput { foreign lib { - b2ShapeDistance :: proc "c" (cache: ^DistanceCache, #by_ptr input: DistanceInput, simplexes: [^]Simplex, simplexCapacity: c.int) -> DistanceOutput --- + b2ShapeDistance :: proc "c" (#by_ptr input: DistanceInput, cache: ^SimplexCache, simplexes: [^]Simplex, simplexCapacity: c.int) -> DistanceOutput --- } - return b2ShapeDistance(cache, input, raw_data(simplexes), i32(len(simplexes))) + return b2ShapeDistance(input, cache, raw_data(simplexes), i32(len(simplexes))) } -// Make a proxy for use in GJK and related functions. +// Make a proxy for use in overlap, shape cast, and related functions. This is a deep copy of the points. @(require_results) -MakeProxy :: proc "c" (vertices: []Vec2, radius: f32) -> DistanceProxy { +MakeProxy :: proc "c" (points: []Vec2, radius: f32) -> ShapeProxy { foreign lib { - b2MakeProxy :: proc "c" (vertices: [^]Vec2, count: i32, radius: f32) -> DistanceProxy --- + b2MakeProxy :: proc "c" (points: [^]Vec2, count: i32, radius: f32) -> ShapeProxy --- } - return b2MakeProxy(raw_data(vertices), i32(len(vertices)), radius) + return b2MakeProxy(raw_data(points), i32(len(points)), radius) +} + +// Make a proxy with a transform. This is a deep copy of the points. +@(require_results) +MakeOffsetProxy :: proc "c" (points: []Vec2, radius: f32, position: Vec2, rotation: Rot) -> ShapeProxy { + foreign lib { + b2MakeOffsetProxy :: proc "c" (points: [^]Vec2, count: i32, radius: f32, position: Vec2, rotation: Rot) -> ShapeProxy --- + } + return b2MakeOffsetProxy(raw_data(points), i32(len(points)), radius, position, rotation) } @(link_prefix="b2", default_calling_convention="c", require_results) foreign lib { // Perform a linear shape cast of shape B moving and shape A fixed. Determines the hit point, normal, and translation fraction. + // You may optionally supply an array to hold debug data. ShapeCast :: proc(#by_ptr input: ShapeCastPairInput) -> CastOutput --- // Evaluate the transform sweep at a specific time. @@ -344,14 +372,14 @@ foreign lib { // Compute the contact manifold between an segment and a polygon CollideSegmentAndPolygon :: proc(#by_ptr segmentA: Segment, xfA: Transform, #by_ptr polygonB: Polygon, xfB: Transform) -> Manifold --- - // Compute the contact manifold between a smooth segment and a circle - CollideSmoothSegmentAndCircle :: proc(#by_ptr smoothSegmentA: SmoothSegment, xfA: Transform, #by_ptr circleB: Circle, xfB: Transform) -> Manifold --- + // Compute the contact manifold between a chain segment and a circle + CollideChainSegmentAndCircle :: proc(#by_ptr segmentA: ChainSegment, xfA: Transform, #by_ptr circleB: Circle, xfB: Transform) -> Manifold --- // Compute the contact manifold between an segment and a capsule - CollideSmoothSegmentAndCapsule :: proc(#by_ptr smoothSegmentA: SmoothSegment, xfA: Transform, #by_ptr capsuleB: Capsule, xfB: Transform, cache: ^DistanceCache) -> Manifold --- + CollideChainSegmentAndCapsule :: proc(#by_ptr segmentA: ChainSegment, xfA: Transform, #by_ptr capsuleB: Capsule, xfB: Transform, cache: ^SimplexCache) -> Manifold --- - // Compute the contact manifold between a smooth segment and a rounded polygon - CollideSmoothSegmentAndPolygon :: proc(#by_ptr smoothSegmentA: SmoothSegment, xfA: Transform, #by_ptr polygonB: Polygon, xfB: Transform, cache: ^DistanceCache) -> Manifold --- + // Compute the contact manifold between a chain segment and a rounded polygon + CollideChainSegmentAndPolygon :: proc(#by_ptr segmentA: ChainSegment, xfA: Transform, #by_ptr polygonB: Polygon, xfB: Transform, cache: ^SimplexCache) -> Manifold --- } @@ -365,7 +393,7 @@ foreign lib { DynamicTree_Destroy :: proc(tree: ^DynamicTree) --- // Create a proxy. Provide an AABB and a userData value. - DynamicTree_CreateProxy :: proc(tree: ^DynamicTree, aabb: AABB, categoryBits: u32, userData: i32) -> i32 --- + DynamicTree_CreateProxy :: proc(tree: ^DynamicTree, aabb: AABB, categoryBits: u64, userData: u64) -> i32 --- // Destroy a proxy. This asserts if the id is invalid. DynamicTree_DestroyProxy :: proc(tree: ^DynamicTree, proxyId: i32) --- @@ -376,50 +404,52 @@ foreign lib { // Enlarge a proxy and enlarge ancestors as necessary. DynamicTree_EnlargeProxy :: proc(tree: ^DynamicTree, proxyId: i32, aabb: AABB) --- - // Query an AABB for overlapping proxies. The callback class - // is called for each proxy that overlaps the supplied AABB. - DynamicTree_Query :: proc(#by_ptr tree: DynamicTree, aabb: AABB, maskBits: u32, callback: TreeQueryCallbackFcn, ctx: rawptr) --- + // Modify the category bits on a proxy. This is an expensive operation. + DynamicTree_SetCategoryBits :: proc(tree: ^DynamicTree, proxyId: i32, categoryBits: u64) --- - // Ray-cast against the proxies in the tree. This relies on the callback - // to perform a exact ray-cast in the case were the proxy contains a shape. + // Get the category bits on a proxy. + DynamicTree_GetCategoryBits :: proc(tree: ^DynamicTree, proxyId: i32) --- + + // Query an AABB for overlapping proxies. The callback class is called for each proxy that overlaps the supplied AABB. + // @return performance data + DynamicTree_Query :: proc(#by_ptr tree: DynamicTree, aabb: AABB, maskBits: u64, callback: TreeQueryCallbackFcn, ctx: rawptr) -> TreeStats --- + + // Ray cast against the proxies in the tree. This relies on the callback + // to perform a exact ray cast in the case were the proxy contains a shape. // The callback also performs the any collision filtering. This has performance // roughly equal to k * log(n), where k is the number of collisions and n is the // number of proxies in the tree. - // Bit-wise filtering using mask bits can greatly improve performance in some scenarios. - // @param tree the dynamic tree to ray cast - // @param input the ray-cast input data. The ray extends from p1 to p1 + maxFraction * (p2 - p1) - // @param maskBits filter bits: `bool accept = (maskBits & node->categoryBits) != 0 ---` + // Bit-wise filtering using mask bits can greatly improve performance in some scenarios. + // However, this filtering may be approximate, so the user should still apply filtering to results. + // @param tree the dynamic tree to ray cast + // @param input the ray cast input data. The ray extends from p1 to p1 + maxFraction * (p2 - p1) + // @param maskBits mask bit hint: `bool accept = (maskBits & node->categoryBits) != 0;` // @param callback a callback class that is called for each proxy that is hit by the ray - // @param context user context that is passed to the callback - DynamicTree_RayCast :: proc(#by_ptr tree: DynamicTree, #by_ptr input: RayCastInput, maskBits: u32, callback: TreeRayCastCallbackFcn, ctx: rawptr) --- + // @param context user context that is passed to the callback + // @return performance data + DynamicTree_RayCast :: proc(#by_ptr tree: DynamicTree, #by_ptr input: RayCastInput, maskBits: u64, callback: TreeRayCastCallbackFcn, ctx: rawptr) -> TreeStats --- - // Ray-cast against the proxies in the tree. This relies on the callback - // to perform a exact ray-cast in the case were the proxy contains a shape. + // Ray cast against the proxies in the tree. This relies on the callback + // to perform a exact ray cast in the case were the proxy contains a shape. // The callback also performs the any collision filtering. This has performance // roughly equal to k * log(n), where k is the number of collisions and n is the // number of proxies in the tree. // @param tree the dynamic tree to ray cast - // @param input the ray-cast input data. The ray extends from p1 to p1 + maxFraction * (p2 - p1). + // @param input the ray cast input data. The ray extends from p1 to p1 + maxFraction * (p2 - p1). // @param maskBits filter bits: `bool accept = (maskBits & node->categoryBits) != 0 ---` // @param callback a callback class that is called for each proxy that is hit by the shape // @param context user context that is passed to the callback - DynamicTree_ShapeCast :: proc(#by_ptr tree: DynamicTree, #by_ptr input: ShapeCastInput, maskBits: u32, callback: TreeShapeCastCallbackFcn, ctx: rawptr) --- + // @return performance data + DynamicTree_ShapeCast :: proc(#by_ptr tree: DynamicTree, #by_ptr input: ShapeCastInput, maskBits: u32, callback: TreeShapeCastCallbackFcn, ctx: rawptr) -> TreeStats --- - // Validate this tree. For testing. - DynamicTree_Validate :: proc(#by_ptr tree: DynamicTree) --- - - // Compute the height of the binary tree in O(N) time. Should not be - // called often. + // Get the height of the binary tree. DynamicTree_GetHeight :: proc(#by_ptr tree: DynamicTree) -> c.int --- - // Get the maximum balance of the tree. The balance is the difference in height of the two children of a node. - DynamicTree_GetMaxBalance :: proc(#by_ptr tree: DynamicTree) -> c.int --- - // Get the ratio of the sum of the node areas to the root area. DynamicTree_GetAreaRatio :: proc(#by_ptr tree: DynamicTree) -> f32 --- - // Build an optimal tree. Very expensive. For testing. - DynamicTree_RebuildBottomUp :: proc(tree: ^DynamicTree) --- + // Get the bounding box that contains the entire tree + DynamicTree_GetRootBounds :: proc(#by_ptr tree: DynamicTree) -> AABB --- // Get the number of proxies created DynamicTree_GetProxyCount :: proc(#by_ptr tree: DynamicTree) -> c.int --- @@ -427,29 +457,47 @@ foreign lib { // Rebuild the tree while retaining subtrees that haven't changed. Returns the number of boxes sorted. DynamicTree_Rebuild :: proc(tree: ^DynamicTree, fullBuild: bool) -> c.int --- - // Shift the world origin. Useful for large worlds. - // The shift formula is: position -= newOrigin - // @param tree the tree to shift - // @param newOrigin the new origin with respect to the old origin - DynamicTree_ShiftOrigin :: proc(tree: ^DynamicTree, newOrigin: Vec2) --- - // Get the number of bytes used by this tree DynamicTree_GetByteCount :: proc(#by_ptr tree: DynamicTree) -> c.int --- + + // Get proxy user data + DynamicTree_GetUserData :: proc(#by_ptr tree: DynamicTree, proxyId: c.int) -> u64 --- + + // Get the AABB of a proxy + DynamicTree_GetAABB :: proc(#by_ptr tree: DynamicTree, proxyId: c.int) -> AABB --- + + // Validate this tree. For testing. + DynamicTree_Validate :: proc(#by_ptr tree: DynamicTree) --- + + // Validate this tree has no enlarged AABBs. For testing. + DynamicTree_ValidateNoEnlarged :: proc(#by_ptr tree: DynamicTree) --- } -// Get proxy user data -// @return the proxy user data or 0 if the id is invalid +/** + * @defgroup character Character mover + * Character movement solver + * @{ + */ + @(require_results) -DynamicTree_GetUserData :: #force_inline proc "contextless" (tree: DynamicTree, proxyId: i32) -> i32 { - return tree.nodes[proxyId].userData +SolvePlanes :: proc(position: Vec2, planes: []CollisionPlane) -> PlaneSolverResult { + foreign lib { + b2SolvePlanes :: proc "c" (position: Vec2, planes: [^]CollisionPlane, count: i32) -> PlaneSolverResult --- + } + + return b2SolvePlanes(position, raw_data(planes), i32(len(planes))) } -// Get the AABB of a proxy @(require_results) -DynamicTree_GetAABB :: #force_inline proc "contextless" (tree: DynamicTree, proxyId: i32) -> AABB { - return tree.nodes[proxyId].aabb +ClipVector :: proc(vector: Vec2, planes: []CollisionPlane) -> Vec2 { + foreign lib { + b2ClipVector :: proc "c" (vector: Vec2, planes: [^]CollisionPlane, count: i32) -> Vec2 --- + } + + return b2ClipVector(vector, raw_data(planes), i32(len(planes))) } +/**@}*/ @(link_prefix="b2", default_calling_convention="c", require_results) @@ -477,8 +525,8 @@ foreign lib { // Simulate a world for one time step. This performs collision detection, integration, and constraint solution. // @param worldId The world to simulate - // @param timeStep The amount of time to simulate, this should be a fixed number. Typically 1/60. - // @param subStepCount The number of sub-steps, increasing the sub-step count can increase accuracy. Typically 4. + // @param timeStep The amount of time to simulate, this should be a fixed number. Usually 1/60. + // @param subStepCount The number of sub-steps, increasing the sub-step count can increase accuracy. Usually 4. World_Step :: proc(worldId: WorldId, timeStep: f32 , subStepCount: c.int) --- // Call this to draw shapes and other debug draw data @@ -494,63 +542,73 @@ foreign lib { World_GetContactEvents :: proc(worldId: WorldId) -> ContactEvents --- // Overlap test for all shapes that *potentially* overlap the provided AABB - World_OverlapAABB :: proc(worldId: WorldId, aabb: AABB, filter: QueryFilter, fcn: OverlapResultFcn, ctx: rawptr) --- + World_OverlapAABB :: proc(worldId: WorldId, aabb: AABB, filter: QueryFilter, fcn: OverlapResultFcn, ctx: rawptr) -> TreeStats --- - // Overlap test for for all shapes that overlap the provided circle - World_OverlapCircle :: proc(worldId: WorldId, #by_ptr circle: Circle, transform: Transform, filter: QueryFilter, fcn: OverlapResultFcn, ctx: rawptr) --- - - // Overlap test for all shapes that overlap the provided capsule - World_OverlapCapsule :: proc(worldId: WorldId, #by_ptr capsule: Capsule, transform: Transform, filter: QueryFilter, fcn: OverlapResultFcn, ctx: rawptr) --- - - // Overlap test for all shapes that overlap the provided polygon - World_OverlapPolygon :: proc(worldId: WorldId, #by_ptr polygon: Polygon, transform: Transform, filter: QueryFilter, fcn: OverlapResultFcn, ctx: rawptr) --- + // Overlap test for all shapes that overlap the provided shape proxy. + World_OverlapShape :: proc(worldId: WorldId, #by_ptr proxy: ShapeProxy, filter: QueryFilter, fcn: OverlapResultFcn, ctx: rawptr) -> TreeStats --- // Cast a ray into the world to collect shapes in the path of the ray. // Your callback function controls whether you get the closest point, any point, or n-points. // The ray-cast ignores shapes that contain the starting point. + // @note The callback function may receive shapes in any order // @param worldId The world to cast the ray against // @param origin The start point of the ray // @param translation The translation of the ray from the start point to the end point // @param filter Contains bit flags to filter unwanted shapes from the results - // @param fcn A user implemented callback function - // @param context A user context that is passed along to the callback function - // @note The callback function may receive shapes in any order - World_CastRay :: proc(worldId: WorldId, origin: Vec2, translation: Vec2, filter: QueryFilter, fcn: CastResultFcn, ctx: rawptr) --- + // @param fcn A user implemented callback function + // @param context A user context that is passed along to the callback function + // @return traversal performance counters + World_CastRay :: proc(worldId: WorldId, origin: Vec2, translation: Vec2, filter: QueryFilter, fcn: CastResultFcn, ctx: rawptr) -> TreeStats --- // Cast a ray into the world to collect the closest hit. This is a convenience function. // This is less general than b2World_CastRay() and does not allow for custom filtering. World_CastRayClosest :: proc(worldId: WorldId, origin: Vec2, translation: Vec2, filter: QueryFilter) -> RayResult --- - // Cast a circle through the world. Similar to a cast ray except that a circle is cast instead of a point. - World_CastCircle :: proc(worldId: WorldId, #by_ptr circle: Circle, originTransform: Transform, translation: Vec2, filter: QueryFilter, fcn: CastResultFcn, ctx: rawptr) --- + // Cast a shape through the world. Similar to a cast ray except that a shape is cast instead of a point. + // @see World_CastRay + World_CastShape :: proc(worldId: WorldId, #by_ptr shape: ShapeProxy, translation: Vec2, filter: QueryFilter, fcn: CastResultFcn, ctx: rawptr) -> TreeStats --- - // Cast a capsule through the world. Similar to a cast ray except that a capsule is cast instead of a point. - World_CastCapsule :: proc(worldId: WorldId, #by_ptr capsule: Capsule, originTransform: Transform, translation: Vec2, filter: QueryFilter, fcn: CastResultFcn, ctx: rawptr) --- + // Cast a capsule mover through the world. This is a special shape cast that handles sliding along other shapes while reducing + // clipping. + World_CastMover :: proc(worldId: WorldId, #by_ptr mover: Capsule, translation: Vec2, filter: QueryFilter) -> f32 --- - // Cast a polygon through the world. Similar to a cast ray except that a polygon is cast instead of a point. - World_CastPolygon :: proc(worldId: WorldId, #by_ptr polygon: Polygon, originTransform: Transform, translation: Vec2, filter: QueryFilter, fcn: CastResultFcn, ctx: rawptr) --- + // Collide a capsule mover with the world, gathering collision planes that can be fed to b2SolvePlanes. Useful for + // kinematic character movement. + World_CollideMover :: proc(worldId: WorldId, #by_ptr mover: Capsule, filter: QueryFilter, fcn: PlaneResultFcn, ctx: rawptr) --- // Enable/disable sleep. If your application does not need sleeping, you can gain some performance // by disabling sleep completely at the world level. // @see WorldDef World_EnableSleeping :: proc(worldId: WorldId, flag: bool) --- + // Is body sleeping enabled? + World_IsSleepingEnabled :: proc(worldId: WorldId) -> bool --- + // Enable/disable continuous collision between dynamic and static bodies. Generally you should keep continuous // collision enabled to prevent fast moving objects from going through static objects. The performance gain from // disabling continuous collision is minor. // @see WorldDef World_EnableContinuous :: proc(worldId: WorldId, flag: bool) --- + // Is continuous collision enabled? + World_IsContinuousEnabled :: proc(worldId: WorldId) -> bool --- + // Adjust the restitution threshold. It is recommended not to make this value very small - // because it will prevent bodies from sleeping. Typically in meters per second. + // because it will prevent bodies from sleeping. Usually in meters per second. // @see WorldDef World_SetRestitutionThreshold :: proc(worldId: WorldId, value: f32) --- + // Get the restitution speed threshold. Usually in meters per second. + World_GetRestitutionThreshold :: proc(worldId: WorldId) -> f32 --- + // Adjust the hit event threshold. This controls the collision velocity needed to generate a b2ContactHitEvent. - // Typically in meters per second. + // Usually in meters per second. // @see WorldDef::hitEventThreshold World_SetHitEventThreshold :: proc(worldId: WorldId, value: f32) --- + // Get the hit event speed threshold. Usually in meters per second. + World_GetHitEventThreshold :: proc(worldId: WorldId) -> f32 --- + // Register the custom filter callback. This is optional. World_SetCustomFilterCallback :: proc(worldId: WorldId, fcn: CustomFilterFcn, ctx: rawptr) --- @@ -558,7 +616,7 @@ foreign lib { World_SetPreSolveCallback :: proc(worldId: WorldId, fcn: PreSolveFcn, ctx: rawptr) --- // Set the gravity vector for the entire world. Box2D has no concept of an up direction and this - // is left as a decision for the application. Typically in m/s^2. + // is left as a decision for the application. Usually in m/s^2. // @see WorldDef World_SetGravity :: proc(worldId: WorldId, gravity: Vec2) --- @@ -567,31 +625,66 @@ foreign lib { // Apply a radial explosion // @param worldId The world id - // @param position The center of the explosion - // @param radius The radius of the explosion - // @param impulse The impulse of the explosion, typically in kg * m / s or N * s. - World_Explode :: proc(worldId: WorldId, position: Vec2, radius: f32, impulse: f32) --- + // @param explosionDef The explosion definition + World_Explode :: proc(worldId: WorldId, #by_ptr explosionDef: ExplosionDef) --- // Adjust contact tuning parameters // @param worldId The world id // @param hertz The contact stiffness (cycles per second) // @param dampingRatio The contact bounciness with 1 being critical damping (non-dimensional) - // @param pushVelocity The maximum contact constraint push out velocity (meters per second) + // @param pushSpeed The maximum contact constraint push out speed (meters per second) // @note Advanced feature - World_SetContactTuning :: proc(worldId: WorldId, hertz: f32, dampingRatio: f32, pushVelocity: f32) --- + World_SetContactTuning :: proc(worldId: WorldId, hertz: f32, dampingRatio: f32, pushSpeed: f32) --- + + // Adjust joint tuning parameters + // @param worldId The world id + // @param hertz The contact stiffness (cycles per second) + // @param dampingRatio The contact bounciness with 1 being critical damping (non-dimensional) + // @note Advanced feature + World_SetJointTuning :: proc(worldId: WorldId, hertz: f32, dampingRatio: f32) --- + + // Set the maximum linear speed. Usually in m/s. + World_SetMaximumLinearSpeed :: proc(worldId: WorldId, maximumLinearSpeed: f32) --- + + // Get the maximum linear speed. Usually in m/s. + World_GetMaximumLinearSpeed :: proc(worldId: WorldId) -> f32 --- // Enable/disable constraint warm starting. Advanced feature for testing. Disabling - // sleeping greatly reduces stability and provides no performance gain. + // warm starting greatly reduces stability and provides no performance gain. World_EnableWarmStarting :: proc(worldId: WorldId, flag: bool) --- + // Is constraint warm starting enabled? + World_IsWarmStartingEnabled :: proc(worldId: WorldId) -> bool --- + + // Get the number of awake bodies. + World_GetAwakeBodyCount :: proc(worldId: WorldId) -> c.int --- + // Get the current world performance profile World_GetProfile :: proc(worldId: WorldId) -> Profile --- // Get world counters and sizes World_GetCounters :: proc(worldId: WorldId) -> Counters --- + // Set the user data pointer. + World_SetUserData :: proc(worldId: WorldId, userData: rawptr) --- + + // Get the user data pointer. + World_GetUserData :: proc(worldId: WorldId) -> rawptr --- + + // Set the friction callback. Passing nil resets to default. + World_SetFrictionCallback :: proc(worldId: WorldId, callback: FrictionCallback) --- + + // Set the restitution callback. Passing nil resets to default. + World_SetRestitutionCallback :: proc(worldId: WorldId, callback: RestitutionCallback) --- + // Dump memory stats to box2d_memory.txt World_DumpMemoryStats :: proc(worldId: WorldId) --- + + // This is for internal testing + World_RebuildStaticTree :: proc(worldId: WorldId) --- + + // This is for internal testing + World_EnableSpeculative :: proc(worldId: WorldId, flag: bool) --- } @@ -625,6 +718,12 @@ foreign lib { // properties regardless of the automatic mass setting. Body_SetType :: proc(bodyId: BodyId, type: BodyType) --- + // Set the body name. Up to 32 characters excluding 0 termination. + Body_SetName :: proc(bodyId: BodyId, name: cstring) --- + + // Get the body name. May be nil. + Body_GetName :: proc(bodyId: BodyId) -> cstring --- + // Set the user data for a body Body_SetUserData :: proc(bodyId: BodyId, userData: rawptr) --- @@ -657,23 +756,34 @@ foreign lib { // Get a world vector on a body given a local vector Body_GetWorldVector :: proc(bodyId: BodyId, localVector: Vec2) -> Vec2 --- - // Get the linear velocity of a body's center of mass. Typically in meters per second. + // Get the linear velocity of a body's center of mass. Usually in meters per second. Body_GetLinearVelocity :: proc(bodyId: BodyId) -> Vec2 --- // Get the angular velocity of a body in radians per second Body_GetAngularVelocity :: proc(bodyId: BodyId) -> f32 --- - // Set the linear velocity of a body. Typically in meters per second. + // Set the linear velocity of a body. Usually in meters per second. Body_SetLinearVelocity :: proc(bodyId: BodyId, linearVelocity: Vec2) --- // Set the angular velocity of a body in radians per second Body_SetAngularVelocity :: proc(bodyId: BodyId, angularVelocity: f32) --- + // Set the velocity to reach the given transform after a given time step. + // The result will be close but maybe not exact. This is meant for kinematic bodies. + // This will automatically wake the body if asleep. + Body_SetTargetTransform :: proc(bodyId: BodyId, target: Transform, timeStep: f32) --- + + // Get the linear velocity of a local point attached to a body. Usually in meters per second. + Body_GetLocalPointVelocity :: proc(bodyId: BodyId, localPoint: Vec2) -> Vec2 --- + + // Get the linear velocity of a world point attached to a body. Usually in meters per second. + GetWorldPointVelocity :: proc(bodyId: BodyId, worldPoint: Vec2) -> Vec2 --- + // Apply a force at a world point. If the force is not applied at the center of mass, // it will generate a torque and affect the angular velocity. This optionally wakes up the body. // The force is ignored if the body is not awake. // @param bodyId The body id - // @param force The world force vector, typically in newtons (N) + // @param force The world force vector, usually in newtons (N) // @param point The world position of the point of application // @param wake Option to wake up the body Body_ApplyForce :: proc(bodyId: BodyId, force: Vec2, point: Vec2, wake: bool) --- @@ -688,7 +798,7 @@ foreign lib { // Apply a torque. This affects the angular velocity without affecting the linear velocity. // This optionally wakes the body. The torque is ignored if the body is not awake. // @param bodyId The body id - // @param torque about the z-axis (out of the screen), typically in N*m. + // @param torque about the z-axis (out of the screen), usually in N*m. // @param wake also wake up the body Body_ApplyTorque :: proc(bodyId: BodyId, torque: f32, wake: bool) --- @@ -697,7 +807,7 @@ foreign lib { // is not at the center of mass. This optionally wakes the body. // The impulse is ignored if the body is not awake. // @param bodyId The body id - // @param impulse the world impulse vector, typically in N*s or kg*m/s. + // @param impulse the world impulse vector, usually in N*s or kg*m/s. // @param point the world position of the point of application. // @param wake also wake up the body // @warning This should be used for one-shot impulses. If you need a steady force, @@ -707,7 +817,7 @@ foreign lib { // Apply an impulse to the center of mass. This immediately modifies the velocity. // The impulse is ignored if the body is not awake. This optionally wakes the body. // @param bodyId The body id - // @param impulse the world impulse vector, typically in N*s or kg*m/s. + // @param impulse the world impulse vector, usually in N*s or kg*m/s. // @param wake also wake up the body // @warning This should be used for one-shot impulses. If you need a steady force, // use a force instead, which will work better with the sub-stepping solver. @@ -716,17 +826,17 @@ foreign lib { // Apply an angular impulse. The impulse is ignored if the body is not awake. // This optionally wakes the body. // @param bodyId The body id - // @param impulse the angular impulse, typically in units of kg*m*m/s + // @param impulse the angular impulse, usually in units of kg*m*m/s // @param wake also wake up the body // @warning This should be used for one-shot impulses. If you need a steady force, // use a force instead, which will work better with the sub-stepping solver. Body_ApplyAngularImpulse :: proc(bodyId: BodyId, impulse: f32, wake: bool) --- - // Get the mass of the body, typically in kilograms + // Get the mass of the body, usually in kilograms Body_GetMass :: proc(bodyId: BodyId) -> f32 --- - // Get the inertia tensor of the body, typically in kg*m^2 - Body_GetInertiaTensor :: proc(bodyId: BodyId) -> f32 --- + // Get the rotational inertia of the body, usually in kg*m^2 + Body_GetRotationalInertia :: proc(bodyId: BodyId) -> f32 --- // Get the center of mass position of the body in local space Body_GetLocalCenterOfMass :: proc(bodyId: BodyId) -> Vec2 --- @@ -749,13 +859,6 @@ foreign lib { // You should call this regardless of body type. Body_ApplyMassFromShapes :: proc(bodyId: BodyId) --- - // Set the automatic mass setting. Normally this is set in BodyDef before creation. - // @see BodyDef::automaticMass - Body_SetAutomaticMass :: proc(bodyId: BodyId, automaticMass: bool ) --- - - // Get the automatic mass setting - Body_GetAutomaticMass :: proc(bodyId: BodyId) -> bool --- - // Adjust the linear damping. Normally this is set in BodyDef before creation. Body_SetLinearDamping :: proc(bodyId: BodyId, linearDamping: f32) --- @@ -789,10 +892,10 @@ foreign lib { // Returns true if sleeping is enabled for this body Body_IsSleepEnabled :: proc(bodyId: BodyId) -> bool --- - // Set the sleep threshold, typically in meters per second - Body_SetSleepThreshold :: proc(bodyId: BodyId, sleepVelocity: f32) --- + // Set the sleep threshold, usually in meters per second + Body_SetSleepThreshold :: proc(bodyId: BodyId, sleepThreshold: f32) --- - // Get the sleep threshold, typically in meters per second. + // Get the sleep threshold, usually in meters per second. Body_GetSleepThreshold :: proc(bodyId: BodyId) -> f32 --- // Returns true if this body is enabled @@ -817,9 +920,17 @@ foreign lib { // Is this body a bullet? Body_IsBullet :: proc(bodyId: BodyId) -> bool --- + // Enable/disable contact events on all shapes. + // @see b2ShapeDef::enableContactEvents + // @warning changing this at runtime may cause mismatched begin/end touch events + Body_EnableContactEvents :: proc(bodyId: BodyId, flag: bool) --- + // Enable/disable hit events on all shapes // @see b2ShapeDef::enableHitEvents - Body_EnableHitEvents :: proc(bodyId: BodyId, enableHitEvents: bool) --- + Body_EnableHitEvents :: proc(bodyId: BodyId, flag: bool) --- + + // Get the world that owns this body + Body_GetWorld :: proc(bodyId: BodyId) -> WorldId --- // Get the number of shapes on this body Body_GetShapeCount :: proc(bodyId: BodyId) -> c.int --- @@ -898,8 +1009,10 @@ foreign lib { // @return the shape id for accessing the shape CreatePolygonShape :: proc(bodyId: BodyId, #by_ptr def: ShapeDef, #by_ptr polygon: Polygon) -> ShapeId --- - // Destroy a shape - DestroyShape :: proc(shapeId: ShapeId) --- + // Destroy a shape. You may defer the body mass update which can improve performance if several shapes on a + // body are destroyed at once. + // @see b2Body_ApplyMassFromShapes + DestroyShape :: proc(shapeId: ShapeId, updateBodyMass: bool) --- // Shape identifier validation. Provides validation for up to 64K allocations. Shape_IsValid :: proc(id: ShapeId) -> bool --- @@ -910,7 +1023,12 @@ foreign lib { // Get the id of the body that a shape is attached to Shape_GetBody :: proc(shapeId: ShapeId) -> BodyId --- - // Returns true If the shape is a sensor + // Get the world that owns this shape. + Shape_GetWorld :: proc(shapeId: ShapeId) -> WorldId --- + + // Returns true if the shape is a sensor. It is not possible to change a shape + // from sensor to solid dynamically because this breaks the contract for + // sensor events. Shape_IsSensor :: proc(shapeId: ShapeId) -> bool --- // Set the user data for a shape @@ -920,12 +1038,12 @@ foreign lib { // from an event or query. Shape_GetUserData :: proc(shapeId: ShapeId) -> rawptr --- - // Set the mass density of a shape, typically in kg/m^2. - // This will not update the mass properties on the parent body. + // Set the mass density of a shape, usually in kg/m^2. + // This will optionally update the mass properties on the parent body. // @see b2ShapeDef::density, b2Body_ApplyMassFromShapes - Shape_SetDensity :: proc(shapeId: ShapeId, density: f32) --- + Shape_SetDensity :: proc(shapeId: ShapeId, density: f32, updateBodyMass: bool) --- - // Get the density of a shape, typically in kg/m^2 + // Get the density of a shape, usually in kg/m^2 Shape_GetDensity :: proc(shapeId: ShapeId) -> f32 --- // Set the friction on a shape @@ -942,15 +1060,24 @@ foreign lib { // Get the shape restitution Shape_GetRestitution :: proc(shapeId: ShapeId) -> f32 --- + // Set the shape material identifier + // @see b2ShapeDef::material + Shape_SetMaterial :: proc(shapeId: ShapeId, material: c.int) --- + + // Get the shape material identifier + Shape_GetMaterial :: proc(shapeId: ShapeId) -> c.int --- + // Get the shape filter Shape_GetFilter :: proc(shapeId: ShapeId) -> Filter --- - // Set the current filter. This is almost as expensive as recreating the shape. - // @see b2ShapeDef::filter + // Set the current filter. This is almost as expensive as recreating the shape. This may cause + // contacts to be immediately destroyed. However contacts are not created until the next world step. + // Sensor overlap state is also not updated until the next world step. + // @see b2ShapeDef::filter Shape_SetFilter :: proc(shapeId: ShapeId, filter: Filter) --- - // Enable sensor events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. - // @see b2ShapeDef::isSensor + // Enable sensor events for this shape. + // @see b2ShapeDef::enableSensorEvents Shape_EnableSensorEvents :: proc(shapeId: ShapeId, flag: bool) --- // Returns true if sensor events are enabled @@ -958,6 +1085,7 @@ foreign lib { // Enable contact events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. // @see b2ShapeDef::enableContactEvents + // @warning changing this at run-time may lead to lost begin/end events Shape_EnableContactEvents :: proc(shapeId: ShapeId, flag: bool) --- // Returns true if contact events are enabled @@ -982,7 +1110,7 @@ foreign lib { Shape_TestPoint :: proc(shapeId: ShapeId, point: Vec2) -> bool --- // Ray cast a shape directly - Shape_RayCast :: proc(shapeId: ShapeId, origin: Vec2, translation: Vec2) -> CastOutput --- + Shape_RayCast :: proc(shapeId: ShapeId, #by_ptr input: RayCastInput) -> CastOutput --- // Get a copy of the shape's circle. Asserts the type is correct. Shape_GetCircle :: proc(shapeId: ShapeId) -> Circle --- @@ -990,9 +1118,9 @@ foreign lib { // Get a copy of the shape's line segment. Asserts the type is correct. Shape_GetSegment :: proc(shapeId: ShapeId) -> Segment --- - // Get a copy of the shape's smooth line segment. These come from chain shapes. + // Get a copy of the shape's chain segment. These come from chain shapes. // Asserts the type is correct. - Shape_GetSmoothSegment :: proc(shapeId: ShapeId) -> SmoothSegment --- + Shape_GetChainSegment :: proc(shapeId: ShapeId) -> ChainSegment --- // Get a copy of the shape's capsule. Asserts the type is correct. Shape_GetCapsule :: proc(shapeId: ShapeId) -> Capsule --- @@ -1018,16 +1146,34 @@ foreign lib { // @see b2Body_ApplyMassFromShapes Shape_SetPolygon :: proc(shapeId: ShapeId, #by_ptr polygon: Polygon) --- - // Get the parent chain id if the shape type is b2_smoothSegmentShape, otherwise + // Get the parent chain id if the shape type is a chain segment, otherwise // returns b2_nullChainId. Shape_GetParentChain :: proc(shapeId: ShapeId) -> ChainId --- // Get the maximum capacity required for retrieving all the touching contacts on a shape Shape_GetContactCapacity :: proc(shapeId: ShapeId) -> c.int --- + // Get the maximum capacity required for retrieving all the overlapped shapes on a sensor shape. + // This returns 0 if the provided shape is not a sensor. + // @param shapeId the id of a sensor shape + // @returns the required capacity to get all the overlaps in b2Shape_GetSensorOverlaps + Shape_GetSensorCapacity :: proc(shapeId: ShapeId) -> c.int --- + + // Get the overlapped shapes for a sensor shape. + // @param shapeId the id of a sensor shape + // @param overlaps a user allocated array that is filled with the overlapping shapes + // @param capacity the capacity of overlappedShapes + // @returns the number of elements filled in the provided array + // @warning do not ignore the return value, it specifies the valid number of elements + // @warning overlaps may contain destroyed shapes so use b2Shape_IsValid to confirm each overlap + Shape_GetSensorOverlaps :: proc(shapeId: ShapeId, overlaps: [^]ShapeId, capacity: c.int) -> c.int --- + // Get the current world AABB Shape_GetAABB :: proc(shapeId: ShapeId) -> AABB --- + // Get the mass data of a shape + Shape_GetMassData :: proc(shapeId: ShapeId) -> MassData --- + // Get the closest point on a shape to a target point. Target and result are in world space. Shape_GetClosestPoint :: proc(shapeId: ShapeId, target: Vec2) -> Vec2 --- } @@ -1049,21 +1195,44 @@ foreign lib { // Create a chain shape // @see b2ChainDef for details - CreateChain :: proc(bodyId: BodyId, #by_ptr def: ChainDef) -> ChainId --- + CreateChain :: proc(bodyId: BodyId, #by_ptr def: ChainDef) -> ChainId --- // Destroy a chain shape - DestroyChain :: proc(chainId: ChainId) --- + DestroyChain :: proc(chainId: ChainId) --- + + // Get the world that owns this chain shape + Chain_GetWorld :: proc(chainId: ChainId) -> WorldId --- + + // Get the number of segments on this chain + Chain_GetSegmentCount :: proc(chainId: ChainId) -> c.int --- + + // Fill a user array with chain segment shape ids up to the specified capacity. Returns + // the actual number of segments returned. + Chain_GetSegments :: proc(chainId: ChainId, segmentArray: [^]ShapeId, capacity: c.int) -> c.int --- // Set the chain friction // @see b2ChainDef::friction - Chain_SetFriction :: proc(chainId: ChainId, friction: f32) --- + Chain_SetFriction :: proc(chainId: ChainId, friction: f32) --- + + // Get the chain friction + Chain_GetFriction :: proc(chainId: ChainId) -> f32 --- // Set the chain restitution (bounciness) // @see b2ChainDef::restitution - Chain_SetRestitution :: proc(chainId: ChainId, restitution: f32) --- + Chain_SetRestitution :: proc(chainId: ChainId, restitution: f32) --- + + // Get the chain restitution + Chain_GetRestitution :: proc(chainId: ChainId) -> f32 --- + + // Set the chain material + // @see b2ChainDef::material + Chain_SetMaterial :: proc(chainId: ChainId, material: c.int) --- + + // Get the chain material + Chain_GetMaterial :: proc(chainId: ChainId) -> c.int --- // Chain identifier validation. Provides validation for up to 64K allocations. - Chain_IsValid :: proc(id: ChainId) -> bool --- + Chain_IsValid :: proc(id: ChainId) -> bool --- /** * @defgroup joint Joint @@ -1085,6 +1254,9 @@ foreign lib { // Get body B id on a joint Joint_GetBodyB :: proc(jointId: JointId) -> BodyId --- + // Get the world that owns this joint + Joint_GetWorld :: proc(jointId: JointId) -> WorldId --- + // Get the local anchor on bodyA Joint_GetLocalAnchorA :: proc(jointId: JointId) -> Vec2 --- @@ -1106,10 +1278,10 @@ foreign lib { // Wake the bodies connect to this joint Joint_WakeBodies :: proc(jointId: JointId) --- - // Get the current constraint force for this joint + // Get the current constraint force for this joint. Usually in Newtons. Joint_GetConstraintForce :: proc(jointId: JointId) -> Vec2 --- - // Get the current constraint torque for this joint + // Get the current constraint torque for this joint. Usually in Newton * meters. Joint_GetConstraintTorque :: proc(jointId: JointId) -> f32 --- /** @@ -1142,10 +1314,10 @@ foreign lib { DistanceJoint_SetSpringDampingRatio :: proc(jointId: JointId, dampingRatio: f32) --- // Get the spring Hertz - DistanceJoint_GetHertz :: proc(jointId: JointId) -> f32 --- + DistanceJoint_GetSpringHertz :: proc(jointId: JointId) -> f32 --- // Get the spring damping ratio - DistanceJoint_GetDampingRatio :: proc(jointId: JointId) -> f32 --- + DistanceJoint_GetSpringDampingRatio :: proc(jointId: JointId) -> f32 --- // Enable joint limit. The limit only works if the joint spring is enabled. Otherwise the joint is rigid // and the limit has no effect. @@ -1172,19 +1344,19 @@ foreign lib { // Is the distance joint motor enabled? DistanceJoint_IsMotorEnabled :: proc(jointId: JointId) -> bool --- - // Set the distance joint motor speed, typically in meters per second + // Set the distance joint motor speed, usually in meters per second DistanceJoint_SetMotorSpeed :: proc(jointId: JointId, motorSpeed: f32) --- - // Get the distance joint motor speed, typically in meters per second + // Get the distance joint motor speed, usually in meters per second DistanceJoint_GetMotorSpeed :: proc(jointId: JointId) -> f32 --- - // Set the distance joint maximum motor force, typically in newtons + // Set the distance joint maximum motor force, usually in newtons DistanceJoint_SetMaxMotorForce :: proc(jointId: JointId, force: f32) --- - // Get the distance joint maximum motor force, typically in newtons + // Get the distance joint maximum motor force, usually in newtons DistanceJoint_GetMaxMotorForce :: proc(jointId: JointId) -> f32 --- - // Get the distance joint current motor force, typically in newtons + // Get the distance joint current motor force, usually in newtons DistanceJoint_GetMotorForce :: proc(jointId: JointId) -> f32 --- /** @@ -1212,22 +1384,22 @@ foreign lib { // Get the motor joint angular offset target in radians MotorJoint_GetAngularOffset :: proc(jointId: JointId) -> f32 --- - // Set the motor joint maximum force, typically in newtons + // Set the motor joint maximum force, usually in newtons MotorJoint_SetMaxForce :: proc(jointId: JointId, maxForce: f32) --- - // Get the motor joint maximum force, typically in newtons + // Get the motor joint maximum force, usually in newtons MotorJoint_GetMaxForce :: proc(jointId: JointId) -> f32 --- - // Set the motor joint maximum torque, typically in newton-meters + // Set the motor joint maximum torque, usually in newton-meters MotorJoint_SetMaxTorque :: proc(jointId: JointId, maxTorque: f32) --- - // Get the motor joint maximum torque, typically in newton-meters + // Get the motor joint maximum torque, usually in newton-meters MotorJoint_GetMaxTorque :: proc(jointId: JointId) -> f32 --- - // Set the motor joint correction factor, typically in [0, 1] + // Set the motor joint correction factor, usually in [0, 1] MotorJoint_SetCorrectionFactor :: proc(jointId: JointId, correctionFactor: f32) --- - // Get the motor joint correction factor, typically in [0, 1] + // Get the motor joint correction factor, usually in [0, 1] MotorJoint_GetCorrectionFactor :: proc(jointId: JointId) -> f32 --- /**@}*/ @@ -1262,14 +1434,29 @@ foreign lib { // Get the mouse joint damping ratio, non-dimensional MouseJoint_GetSpringDampingRatio :: proc(jointId: JointId) -> f32 --- - // Set the mouse joint maximum force, typically in newtons + // Set the mouse joint maximum force, usually in newtons MouseJoint_SetMaxForce :: proc(jointId: JointId, maxForce: f32) --- - // Get the mouse joint maximum force, typically in newtons + // Get the mouse joint maximum force, usually in newtons MouseJoint_GetMaxForce :: proc(jointId: JointId) -> f32 --- /**@}*/ + /** + * @defgroup filter_joint Filter Joint + * @brief Functions for the filter joint. + * + * The filter joint is used to disable collision between two bodies. As a side effect of being a joint, it also + * keeps the two bodies in the same simulation island. + * @{ + */ + + // Create a filter joint. + // @see b2FilterJointDef for details + CreateFilterJoint :: proc(worldId: WorldId, #by_ptr def: FilterJointDef) -> JointId --- + + /**@}*/ + /** * @defgroup prismatic_joint Prismatic Joint * @brief A prismatic joint allows for translation along a single axis with no rotation. @@ -1323,21 +1510,27 @@ foreign lib { // Is the prismatic joint motor enabled? PrismaticJoint_IsMotorEnabled :: proc(jointId: JointId) -> bool --- - // Set the prismatic joint motor speed, typically in meters per second + // Set the prismatic joint motor speed, usually in meters per second PrismaticJoint_SetMotorSpeed :: proc(jointId: JointId, motorSpeed: f32) --- - // Get the prismatic joint motor speed, typically in meters per second + // Get the prismatic joint motor speed, usually in meters per second PrismaticJoint_GetMotorSpeed :: proc(jointId: JointId) -> f32 --- - // Set the prismatic joint maximum motor force, typically in newtons + // Set the prismatic joint maximum motor force, usually in newtons PrismaticJoint_SetMaxMotorForce :: proc(jointId: JointId, force: f32) --- - // Get the prismatic joint maximum motor force, typically in newtons + // Get the prismatic joint maximum motor force, usually in newtons PrismaticJoint_GetMaxMotorForce :: proc(jointId: JointId) -> f32 --- - // Get the prismatic joint current motor force, typically in newtons + // Get the prismatic joint current motor force, usually in newtons PrismaticJoint_GetMotorForce :: proc(jointId: JointId) -> f32 --- + // Get the current joint translation, usually in meters. + PrismaticJoint_GetTranslation :: proc(jointId: JointId) -> f32 --- + + // Get the current joint translation speed, usually in meters per second. + PrismaticJoint_GetSpeed :: proc(jointId: JointId) -> f32 --- + /** * @defgroup revolute_joint Revolute Joint * @brief A revolute joint allows for relative rotation in the 2D plane with no relative translation. @@ -1353,6 +1546,9 @@ foreign lib { // Enable/disable the revolute joint spring RevoluteJoint_EnableSpring :: proc(jointId: JointId, enableSpring: bool) --- + // Is the revolute spring enabled? + RevoluteJoint_IsSpringEnabled :: proc(jointId: JointId) -> bool --- + // Set the revolute joint spring stiffness in Hertz RevoluteJoint_SetSpringHertz :: proc(jointId: JointId, hertz: f32) --- @@ -1381,7 +1577,8 @@ foreign lib { // Get the revolute joint upper limit in radians RevoluteJoint_GetUpperLimit :: proc(jointId: JointId) -> f32 --- - // Set the revolute joint limits in radians + // Set the revolute joint limits in radians. It is expected that lower <= upper + // and that -0.95 * B2_PI <= lower && upper <= -0.95 * B2_PI. RevoluteJoint_SetLimits :: proc(jointId: JointId, lower, upper: f32) --- // Enable/disable a revolute joint motor @@ -1396,13 +1593,13 @@ foreign lib { // Get the revolute joint motor speed in radians per second RevoluteJoint_GetMotorSpeed :: proc(jointId: JointId) -> f32 --- - // Get the revolute joint current motor torque, typically in newton-meters + // Get the revolute joint current motor torque, usually in newton-meters RevoluteJoint_GetMotorTorque :: proc(jointId: JointId) -> f32 --- - // Set the revolute joint maximum motor torque, typically in newton-meters + // Set the revolute joint maximum motor torque, usually in newton-meters RevoluteJoint_SetMaxMotorTorque :: proc(jointId: JointId, torque: f32) --- - // Get the revolute joint maximum motor torque, typically in newton-meters + // Get the revolute joint maximum motor torque, usually in newton-meters RevoluteJoint_GetMaxMotorTorque :: proc(jointId: JointId) -> f32 --- /**@}*/ @@ -1421,6 +1618,12 @@ foreign lib { // @see b2WeldJointDef for details CreateWeldJoint :: proc(worldId: WorldId, #by_ptr def: WeldJointDef) -> JointId --- + // Get the weld joint reference angle in radians + WeldJoint_GetReferenceAngle :: proc(jointId: JointId) -> f32 --- + + // Set the weld joint reference angle in radians, must be in [-pi,pi]. + WeldJoint_SetReferenceAngle :: proc(jointId: JointId, angleInRadians: f32) --- + // Set the weld joint linear stiffness in Hertz. 0 is rigid. WeldJoint_SetLinearHertz :: proc(jointId: JointId, hertz: f32) --- @@ -1503,22 +1706,24 @@ foreign lib { // Get the wheel joint motor speed in radians per second WheelJoint_GetMotorSpeed :: proc(jointId: JointId) -> f32 --- - // Set the wheel joint maximum motor torque, typically in newton-meters + // Set the wheel joint maximum motor torque, usually in newton-meters WheelJoint_SetMaxMotorTorque :: proc(jointId: JointId, torque: f32) --- - // Get the wheel joint maximum motor torque, typically in newton-meters + // Get the wheel joint maximum motor torque, usually in newton-meters WheelJoint_GetMaxMotorTorque :: proc(jointId: JointId) -> f32 --- - // Get the wheel joint current motor torque, typically in newton-meters + // Get the wheel joint current motor torque, usually in newton-meters WheelJoint_GetMotorTorque :: proc(jointId: JointId) -> f32 --- } IsValid :: proc{ - Float_IsValid, - Vec2_IsValid, - Rot_IsValid, + IsValidFloat, + IsValidVec2, + IsValidRotation, + IsValidAABB, + IsValidPlane, World_IsValid, Body_IsValid, Shape_IsValid, diff --git a/vendor/box2d/build_box2d.sh b/vendor/box2d/build_box2d.sh index 74d75eb57..d6fb3888d 100755 --- a/vendor/box2d/build_box2d.sh +++ b/vendor/box2d/build_box2d.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -eu -VERSION="3.0.0" +VERSION="3.1.0" RELEASE="https://github.com/erincatto/box2d/archive/refs/tags/v$VERSION.tar.gz" -cd "$(odin root)"/vendor/box2d +cd "$(dirname "$0")" curl -O -L "$RELEASE" tar -xzvf "v$VERSION.tar.gz" @@ -58,7 +58,7 @@ Darwin) *) rm -rf build mkdir build - cmake $FLAGS -DCMAKE_OSX_ARCHITECTURES=arm64 -S . -B build + cmake $FLAGS -S . -B build cmake --build build cp build/src/libbox2d.a ../lib/box2d_other.a ;; @@ -73,7 +73,8 @@ make -f wasm.Makefile if [[ $? -ne 0 ]]; then printf "\e[30;43mwarning:\e[0m Native Box2D libraries were built successfully, the WASM build failed, likely because your default C compiler and/or linker doesn't support WASM, you can set the CC and LD environment variables to point to a compiler and linker that support it\n" fi +make -f wasm.Makefile clean set -e -rm -rf v3.0.0.tar.gz -rm -rf box2d-3.0.0 +rm -rf "v$VERSION.tar.gz" +rm -rf box2d-"$VERSION" diff --git a/vendor/box2d/collision.odin b/vendor/box2d/collision.odin index 88f3b3a77..b29e2f946 100644 --- a/vendor/box2d/collision.odin +++ b/vendor/box2d/collision.odin @@ -5,9 +5,9 @@ import "core:c" // The maximum number of vertices on a convex polygon. Changing this affects performance even if you // don't use more vertices. -maxPolygonVertices :: 8 +MAX_POLYGON_VERTICES :: 8 -// Low level ray-cast input data +// Low level ray cast input data RayCastInput :: struct { // Start point of the ray cast origin: Vec2, @@ -19,27 +19,37 @@ RayCastInput :: struct { maxFraction: f32, } +// A distance proxy is used by the GJK algorithm. It encapsulates any shape. +// You can provide between 1 and MAX_POLYGON_VERTICES and a radius. +ShapeProxy :: struct { + // The point cloud + points: [MAX_POLYGON_VERTICES]Vec2 `fmt:"v,count"`, + + // The number of points. Must be greater than 0. + count: c.int, + + // The external radius of the point cloud. May be zero. + radius: f32, +} + // Low level shape cast input in generic form. This allows casting an arbitrary point -// cloud wrap with a radius. For example, a circle is a single point with a non-zero radius. -// A capsule is two points with a non-zero radius. A box is four points with a zero radius. +// cloud wrap with a radius. For example, a circle is a single point with a non-zero radius. +// A capsule is two points with a non-zero radius. A box is four points with a zero radius. ShapeCastInput :: struct { - // A point cloud to cast - points: [maxPolygonVertices]Vec2 `fmt:"v,count"`, - - // The number of points - count: i32, - - // The radius around the point cloud - radius: f32, + // A generic shape + proxy: ShapeProxy, // The translation of the shape cast translation: Vec2, // The maximum fraction of the translation to consider, typically 1 maxFraction: f32, + + // Allow shape cast to encroach when initially touching. This only works if the radius is greater than zero. + canEncroach: bool, } -// Low level ray-cast or shape-cast output data +// Low level ray cast or shape-cast output data CastOutput :: struct { // The surface normal at the hit point normal: Vec2, @@ -51,7 +61,7 @@ CastOutput :: struct { fraction: f32, // The number of iterations used - iterations: i32, + iterations: c.int, // Did the cast hit? hit: bool, @@ -93,16 +103,16 @@ Capsule :: struct { // A solid convex polygon. It is assumed that the interior of the polygon is to // the left of each edge. -// Polygons have a maximum number of vertices equal to maxPolygonVertices. +// Polygons have a maximum number of vertices equal to MAX_POLYGON_VERTICES. // In most cases you should not need many vertices for a convex polygon. -// @warning DO NOT fill this out manually, instead use a helper function like -// b2MakePolygon or b2MakeBox. +// @warning DO NOT fill this out manually, instead use a helper function like +// b2MakePolygon or b2MakeBox. Polygon :: struct { // The polygon vertices - vertices: [maxPolygonVertices]Vec2 `fmt:"v,count"`, + vertices: [MAX_POLYGON_VERTICES]Vec2 `fmt:"v,count"`, // The outward normal vectors of the polygon sides - normals: [maxPolygonVertices]Vec2 `fmt:"v,count"`, + normals: [MAX_POLYGON_VERTICES]Vec2 `fmt:"v,count"`, // The centroid of the polygon centroid: Vec2, @@ -111,7 +121,7 @@ Polygon :: struct { radius: f32, // The number of polygon vertices - count: i32, + count: c.int, } // A line segment with two-sided collision. @@ -123,10 +133,10 @@ Segment :: struct { point2: Vec2, } -// A smooth line segment with one-sided collision. Only collides on the right side. +// A line segment with one-sided collision. Only collides on the right side. // Several of these are generated for a chain shape. // ghost1 -> point1 -> point2 -> ghost2 -SmoothSegment :: struct { +ChainSegment :: struct { // The tail ghost vertex ghost1: Vec2, @@ -137,7 +147,7 @@ SmoothSegment :: struct { ghost2: Vec2, // The owning chain shape index (internal usage only) - chainId: i32, + chainId: c.int, } @@ -145,10 +155,10 @@ SmoothSegment :: struct { // @warning Do not modify these values directly, instead use b2ComputeHull() Hull :: struct { // The final points of the hull - points: [maxPolygonVertices]Vec2 `fmt:"v,count"`, + points: [MAX_POLYGON_VERTICES]Vec2 `fmt:"v,count"`, // The number of points - count: i32, + count: c.int, } /** @@ -178,21 +188,11 @@ SegmentDistanceResult :: struct { distanceSquared: f32, } -// A distance proxy is used by the GJK algorithm. It encapsulates any shape. -DistanceProxy :: struct { - // The point cloud - points: [maxPolygonVertices]Vec2 `fmt:"v,count"`, - - // The number of points - count: i32, - - // The external radius of the point cloud - radius: f32, -} - -// Used to warm start b2Distance. Set count to zero on first call or -// use zero initialization. -DistanceCache :: struct { +// Used to warm start the GJK simplex. If you call this function multiple times with nearby +// transforms this might improve performance. Otherwise you can zero initialize this. +// The distance cache must be initialized to zero on the first call. +// Users should generally just zero initialize this structure for each call. +SimplexCache :: struct { // The number of stored simplex points count: u16, @@ -203,15 +203,15 @@ DistanceCache :: struct { indexB: [3]u8 `fmt:"v,count"`, } -emptyDistanceCache :: DistanceCache{} +emptySimplexCache :: SimplexCache{} // Input for b2ShapeDistance DistanceInput :: struct { // The proxy for shape A - proxyA: DistanceProxy, + proxyA: ShapeProxy, // The proxy for shape B - proxyB: DistanceProxy, + proxyB: ShapeProxy, // The world transform for shape A transformA: Transform, @@ -227,6 +227,7 @@ DistanceInput :: struct { DistanceOutput :: struct { pointA: Vec2, // Closest point on shapeA pointB: Vec2, // Closest point on shapeB + normal: Vec2, // Normal vector that points from A to B distance: f32, // The final distance, zero if overlapped iterations: i32, // Number of GJK iterations used simplexCount: i32, // The number of simplexes stored in the simplex array @@ -234,28 +235,29 @@ DistanceOutput :: struct { // Simplex vertex for debugging the GJK algorithm SimplexVertex :: struct { - wA: Vec2, // support point in proxyA - wB: Vec2, // support point in proxyB - w: Vec2, // wB - wA - a: f32, // barycentric coordinate for closest point - indexA: i32, // wA index - indexB: i32, // wB index + wA: Vec2, // support point in proxyA + wB: Vec2, // support point in proxyB + w: Vec2, // wB - wA + a: f32, // barycentric coordinate for closest point + indexA: c.int, // wA index + indexB: c.int, // wB index } // Simplex from the GJK algorithm Simplex :: struct { v1, v2, v3: SimplexVertex `fmt:"v,count"`, // vertices - count: i32, // number of valid vertices + count: c.int, // number of valid vertices } // Input parameters for b2ShapeCast ShapeCastPairInput :: struct { - proxyA: DistanceProxy, // The proxy for shape A - proxyB: DistanceProxy, // The proxy for shape B + proxyA: ShapeProxy, // The proxy for shape A + proxyB: ShapeProxy, // The proxy for shape B transformA: Transform, // The world transform for shape A transformB: Transform, // The world transform for shape B translationB: Vec2, // The translation of shape B maxFraction: f32, // The fraction of the translation to consider, typically 1 + canEncroach: bool, // Allows shapes with a radius to move slightly closer if already touching } @@ -272,11 +274,11 @@ Sweep :: struct { // Input parameters for b2TimeOfImpact TOIInput :: struct { - proxyA: DistanceProxy, // The proxy for shape A - proxyB: DistanceProxy, // The proxy for shape B - sweepA: Sweep, // The movement of shape A - sweepB: Sweep, // The movement of shape B - tMax: f32, // Defines the sweep interval [0, tMax] + proxyA: ShapeProxy, // The proxy for shape A + proxyB: ShapeProxy, // The proxy for shape B + sweepA: Sweep, // The movement of shape A + sweepB: Sweep, // The movement of shape B + maxFraction: f32, // Defines the sweep interval [0, maxFraction] } // Describes the TOI output @@ -290,8 +292,8 @@ TOIState :: enum c.int { // Output parameters for b2TimeOfImpact. TOIOutput :: struct { - state: TOIState, // The type of result - t: f32, // The time of the collision + state: TOIState, // The type of result + fraction: f32, // The sweep time of the collision } @@ -301,26 +303,30 @@ TOIOutput :: struct { * @brief Functions for colliding pairs of shapes */ -// A manifold point is a contact point belonging to a contact -// manifold. It holds details related to the geometry and dynamics -// of the contact points. +// A manifold point is a contact point belonging to a contact manifold. +// It holds details related to the geometry and dynamics of the contact points. +// Box2D uses speculative collision so some contact points may be separated. +// You may use the totalNormalImpulse to determine if there was an interaction during +// the time step. ManifoldPoint :: struct { // Location of the contact point in world space. Subject to precision loss at large coordinates. // @note Should only be used for debugging. point: Vec2, - // Location of the contact point relative to bodyA's origin in world space - // @note When used internally to the Box2D solver, these are relative to the center of mass. + // Location of the contact point relative to shapeA's origin in world space + // @note When used internally to the Box2D solver, this is relative to the body center of mass. anchorA: Vec2, - // Location of the contact point relative to bodyB's origin in world space + // Location of the contact point relative to shapeB's origin in world space + // @note When used internally to the Box2D solver, this is relative to the body center of mass. anchorB: Vec2, // The separation of the contact point, negative if penetrating separation: f32, - // The impulse along the manifold normal vector. - normalImpulse: f32, + // The total normal impulse applied across sub-stepping and restitution. This is important + // to identify speculative contact points that had an interaction in the time step. + totalNormalImpulse: f32, // The friction impulse tangentImpulse: f32, @@ -340,16 +346,21 @@ ManifoldPoint :: struct { persisted: bool, } -// A contact manifold describes the contact points between colliding shapes +// A contact manifold describes the contact points between colliding shapes. +// @note Box2D uses speculative collision so some contact points may be separated. Manifold :: struct { - // The manifold points, up to two are possible in 2D - points: [2]ManifoldPoint, - // The unit normal vector in world space, points from shape A to bodyB - normal: Vec2, + normal: Vec2, + + // Angular impulse applied for rolling resistance. N * m * s = kg * m^2 / s + rollingImpulse: f32, + + // The manifold points, up to two are possible in 2D + points: [2]ManifoldPoint, + // The number of contacts points, will be 0, 1, or 2 - pointCount: i32, + pointCount: c.int, } @@ -364,63 +375,17 @@ Manifold :: struct { * A dynamic AABB tree broad-phase, inspired by Nathanael Presson's btDbvt. * A dynamic tree arranges data in a binary tree to accelerate * queries such as AABB queries and ray casts. Leaf nodes are proxies - * with an AABB. These are used to hold a user collision object, such as a reference to a b2Shape. + * with an AABB. These are used to hold a user collision object. * Nodes are pooled and relocatable, so I use node indices rather than pointers. * The dynamic tree is made available for advanced users that would like to use it to organize * spatial game data besides rigid bodies. - * - * @note This is an advanced feature and normally not used by applications directly. */ -// The default category bit for a tree proxy. Used for collision filtering. -defaultCategoryBits :: 0x00000001 - -// Convenience mask bits to use when you don't need collision filtering and just want -// all results. -defaultMaskBits :: 0xFFFFFFFF - -// A node in the dynamic tree. This is private data placed here for performance reasons. -// 16 + 16 + 8 + pad(8) -TreeNode :: struct { - // The node bounding box - aabb: AABB, // 16 - - // Category bits for collision filtering - categoryBits: u32, // 4 - - using _: struct #raw_union { - // The node parent index - parent: i32, - - // The node freelist next index - next: i32, - }, // 4 - - // Child 1 index - child1: i32, // 4 - - // Child 2 index - child2: i32, // 4 - - // User data - // todo could be union with child index - userData: i32, // 4 - - // Leaf = 0, free node = -1 - height: i16, // 2 - - // Has the AABB been enlarged? - enlarged: bool, // 1 - - // Padding for clarity - _: [9]byte, -} - // The dynamic tree structure. This should be considered private data. // It is placed here for performance reasons. DynamicTree :: struct { // The tree nodes - nodes: [^]TreeNode `fmt"v,nodeCount"`, + nodes: rawptr, // The root index root: i32, @@ -453,16 +418,25 @@ DynamicTree :: struct { rebuildCapacity: i32, } +// These are performance results returned by dynamic tree queries. +TreeStats :: struct { + // Number of internal nodes visited during the query + nodeVisits: c.int, + + // Number of leaf nodes visited during the query + leafVisits: c.int, +} + // This function receives proxies found in the AABB query. // @return true if the query should continue -TreeQueryCallbackFcn :: #type proc "c" (proxyId: i32, userData: i32, ctx: rawptr) -> bool +TreeQueryCallbackFcn :: #type proc "c" (proxyId: i32, userData: u64, ctx: rawptr) -> bool -// This function receives clipped ray-cast input for a proxy. The function +// This function receives clipped ray cast input for a proxy. The function // returns the new ray fraction. -// - return a value of 0 to terminate the ray-cast +// - return a value of 0 to terminate the ray cast // - return a value less than input->maxFraction to clip the ray // - return a value of input->maxFraction to continue the ray cast without clipping -TreeShapeCastCallbackFcn :: #type proc "c" (#by_ptr input: ShapeCastInput, proxyId: i32, userData: i32, ctx: rawptr) -> f32 +TreeShapeCastCallbackFcn :: #type proc "c" (#by_ptr input: ShapeCastInput, proxyId: i32, userData: u64, ctx: rawptr) -> f32 // This function receives clipped raycast input for a proxy. The function @@ -470,4 +444,47 @@ TreeShapeCastCallbackFcn :: #type proc "c" (#by_ptr input: ShapeCastInput, proxy // - return a value of 0 to terminate the ray cast // - return a value less than input->maxFraction to clip the ray // - return a value of input->maxFraction to continue the ray cast without clipping -TreeRayCastCallbackFcn :: #type proc "c" (#by_ptr input: RayCastInput, proxyId: i32, userData: i32, ctx: rawptr) -> f32 +TreeRayCastCallbackFcn :: #type proc "c" (#by_ptr input: RayCastInput, proxyId: i32, userData: u64, ctx: rawptr) -> f32 + +/**@}*/ + +/** + * @defgroup character Character mover + * Character movement solver + * @{ + */ + +/// These are the collision planes returned from b2World_CollideMover +PlaneResult :: struct { + // The collision plane between the mover and convex shape + plane: Plane, + + // Did the collision register a hit? If not this plane should be ignored. + hit: bool, +} + +// These are collision planes that can be fed to b2SolvePlanes. Normally +// this is assembled by the user from plane results in b2PlaneResult +CollisionPlane :: struct { + // The collision plane between the mover and some shape + plane: Plane, + + // Setting this to FLT_MAX makes the plane as rigid as possible. Lower values can + // make the plane collision soft. Usually in meters. + pushLimit: f32, + + // The push on the mover determined by b2SolvePlanes. Usually in meters. + push: f32, + + // Indicates if b2ClipVector should clip against this plane. Should be false for soft collision. + clipVelocity: bool, +} + +// Result returned by b2SolvePlanes +PlaneSolverResult :: struct { + // The final position of the mover + position: Vec2, + + // The number of iterations used by the plane solver. For diagnostics. + iterationCount: i32, +} diff --git a/vendor/box2d/id.odin b/vendor/box2d/id.odin index 211b8b79d..cb530527a 100644 --- a/vendor/box2d/id.odin +++ b/vendor/box2d/id.odin @@ -23,45 +23,46 @@ import "base:intrinsics" /// World id references a world instance. This should be treated as an opaque handle. WorldId :: struct { - index1: u16, - revision: u16, + index1: u16, + generation: u16, } /// Body id references a body instance. This should be treated as an opaque handle. BodyId :: struct { - index1: i32, - world0: u16, - revision: u16, + index1: i32, + world0: u16, + generation: u16, } /// Shape id references a shape instance. This should be treated as an opaque handle. ShapeId :: struct { - index1: i32, - world0: u16, - revision: u16, -} - -/// Joint id references a joint instance. This should be treated as an opaque handle. -JointId :: struct { - index1: i32, - world0: u16, - revision: u16, + index1: i32, + world0: u16, + generation: u16, } /// Chain id references a chain instances. This should be treated as an opaque handle. ChainId :: struct { - index1: i32, - world0: u16, - revision: u16, + index1: i32, + world0: u16, + generation: u16, } +/// Joint id references a joint instance. This should be treated as an opaque handle. +JointId :: struct { + index1: i32, + world0: u16, + generation: u16, +} + + /// Use these to make your identifiers null. /// You may also use zero initialization to get null. nullWorldId :: WorldId{} nullBodyId :: BodyId{} nullShapeId :: ShapeId{} -nullJointId :: JointId{} nullChainId :: ChainId{} +nullJointId :: JointId{} /// Macro to determine if any id is null. IS_NULL :: #force_inline proc "c" (id: $T) -> bool @@ -82,6 +83,6 @@ ID_EQUALS :: #force_inline proc "c" (id1, id2: $T) -> bool where intrinsics.type_is_struct(T), intrinsics.type_has_field(T, "index1"), intrinsics.type_has_field(T, "world0"), - intrinsics.type_has_field(T, "revision") { - return id1.index1 == id2.index1 && id1.world0 == id2.world0 && id1.revision == id2.revision + intrinsics.type_has_field(T, "generation") { + return id1.index1 == id2.index1 && id1.world0 == id2.world0 && id1.generation == id2.generation } diff --git a/vendor/box2d/lib/box2d_darwin_amd64_avx2.a b/vendor/box2d/lib/box2d_darwin_amd64_avx2.a index 269de02fa..f98e919a4 100644 Binary files a/vendor/box2d/lib/box2d_darwin_amd64_avx2.a and b/vendor/box2d/lib/box2d_darwin_amd64_avx2.a differ diff --git a/vendor/box2d/lib/box2d_darwin_amd64_sse2.a b/vendor/box2d/lib/box2d_darwin_amd64_sse2.a index 096db9d7a..3df91e852 100644 Binary files a/vendor/box2d/lib/box2d_darwin_amd64_sse2.a and b/vendor/box2d/lib/box2d_darwin_amd64_sse2.a differ diff --git a/vendor/box2d/lib/box2d_darwin_arm64.a b/vendor/box2d/lib/box2d_darwin_arm64.a index 4feb4aac9..c5d04fc73 100644 Binary files a/vendor/box2d/lib/box2d_darwin_arm64.a and b/vendor/box2d/lib/box2d_darwin_arm64.a differ diff --git a/vendor/box2d/lib/box2d_wasm.o b/vendor/box2d/lib/box2d_wasm.o index 15d71c5a1..fc3ed1cbd 100755 Binary files a/vendor/box2d/lib/box2d_wasm.o and b/vendor/box2d/lib/box2d_wasm.o differ diff --git a/vendor/box2d/lib/box2d_wasm_simd.o b/vendor/box2d/lib/box2d_wasm_simd.o index 568900bd9..8b70dcedd 100755 Binary files a/vendor/box2d/lib/box2d_wasm_simd.o and b/vendor/box2d/lib/box2d_wasm_simd.o differ diff --git a/vendor/box2d/lib/box2d_windows_amd64_avx2.lib b/vendor/box2d/lib/box2d_windows_amd64_avx2.lib index cea3f678d..7b8626b80 100644 Binary files a/vendor/box2d/lib/box2d_windows_amd64_avx2.lib and b/vendor/box2d/lib/box2d_windows_amd64_avx2.lib differ diff --git a/vendor/box2d/lib/box2d_windows_amd64_sse2.lib b/vendor/box2d/lib/box2d_windows_amd64_sse2.lib index 1ba62c76b..f1442615f 100644 Binary files a/vendor/box2d/lib/box2d_windows_amd64_sse2.lib and b/vendor/box2d/lib/box2d_windows_amd64_sse2.lib differ diff --git a/vendor/box2d/math_functions.odin b/vendor/box2d/math_functions.odin index 2294a515c..4394feb39 100644 --- a/vendor/box2d/math_functions.odin +++ b/vendor/box2d/math_functions.odin @@ -3,9 +3,18 @@ package vendor_box2d import "core:c" import "core:math" -pi :: 3.14159265359 +EPSILON :: 1e-23 Vec2 :: [2]f32 + +// Cosine and sine pair +// This uses a custom implementation designed for cross-platform determinism +CosSin :: struct { + // cosine and sine + cosine: f32, + sine: f32, +} + Rot :: struct { c, s: f32, // cosine and sine } @@ -21,11 +30,43 @@ AABB :: struct { upperBound: Vec2, } +// separation = dot(normal, point) - offset +Plane :: struct { + normal: Vec2, + offset: f32, +} + +PI :: math.PI + Vec2_zero :: Vec2{0, 0} Rot_identity :: Rot{1, 0} Transform_identity :: Transform{{0, 0}, {1, 0}} Mat22_zero :: Mat22{0, 0, 0, 0} +// @return the minimum of two integers +@(deprecated="Prefer the built-in 'min(a, b)'", require_results) +MinInt :: proc "c" (a, b: c.int) -> c.int { + return min(a, b) +} + +// @return the maximum of two integers +@(deprecated="Prefer the built-in 'max(a, b)'", require_results) +MaxInt :: proc "c" (a, b: c.int) -> c.int { + return max(a, b) +} + +// @return the absolute value of an integer +@(deprecated="Prefer the built-in 'abs(a)'", require_results) +AbsInt :: proc "c" (a: c.int) -> c.int { + return abs(a) +} + +// @return an integer clamped between a lower and upper bound +@(deprecated="Prefer the built-in 'clamp(a, lower, upper)'", require_results) +ClampInt :: proc "c" (a, lower, upper: c.int) -> c.int { + return clamp(a, lower, upper) +} + // @return the minimum of two floats @(deprecated="Prefer the built-in 'min(a, b)'", require_results) @@ -51,28 +92,15 @@ ClampFloat :: proc "c" (a, lower, upper: f32) -> f32 { return clamp(a, lower, upper) } -// @return the minimum of two integers -@(deprecated="Prefer the built-in 'min(a, b)'", require_results) -MinInt :: proc "c" (a, b: c.int) -> c.int { - return min(a, b) +@(require_results) +Atan2 :: proc "c" (y, x: f32) -> f32 { + return math.atan2(y, x) } -// @return the maximum of two integers -@(deprecated="Prefer the built-in 'max(a, b)'", require_results) -MaxInt :: proc "c" (a, b: c.int) -> c.int { - return max(a, b) -} - -// @return the absolute value of an integer -@(deprecated="Prefer the built-in 'abs(a)'", require_results) -AbsInt :: proc "c" (a: c.int) -> c.int { - return abs(a) -} - -// @return an integer clamped between a lower and upper bound -@(deprecated="Prefer the built-in 'clamp(a, lower, upper)'", require_results) -ClampInt :: proc "c" (a, lower, upper: c.int) -> c.int { - return clamp(a, lower, upper) +@(require_results) +ComputeCosSin :: proc "c" (radians: f32) -> (res: CosSin) { + res.sine, res.cosine = math.sincos(radians) + return } // Vector dot product @@ -198,12 +226,6 @@ Length :: proc "c" (v: Vec2) -> f32 { return math.sqrt(v.x * v.x + v.y * v.y) } -// Get the length squared of this vector -@(require_results) -LengthSquared :: proc "c" (v: Vec2) -> f32 { - return v.x * v.x + v.y * v.y -} - // Get the distance between two points @(require_results) Distance :: proc "c" (a, b: Vec2) -> f32 { @@ -212,45 +234,41 @@ Distance :: proc "c" (a, b: Vec2) -> f32 { return math.sqrt(dx * dx + dy * dy) } -// Get the distance squared between points @(require_results) -DistanceSquared :: proc "c" (a, b: Vec2) -> f32 { - c := Vec2{b.x - a.x, b.y - a.y} - return c.x * c.x + c.y * c.y +Normalize :: proc "c" (v: Vec2) -> Vec2 { + length := Length(v) + if length < EPSILON { + return Vec2_zero + } + invLength := 1 / length + return invLength * v } -// Make a rotation using an angle in radians @(require_results) -MakeRot :: proc "c" (angle: f32) -> Rot { - // todo determinism - return {math.cos(angle), math.sin(angle)} +IsNormalized :: proc "c" (v: Vec2) -> bool { + aa := Dot(v, v) + return abs(1. - aa) < 10. * EPSILON } -// Normalize rotation @(require_results) -NormalizeRot :: proc "c" (q: Rot) -> Rot { - mag := math.sqrt(q.s * q.s + q.c * q.c) - invMag := f32(mag > 0.0 ? 1.0 / mag : 0.0) - return {q.c * invMag, q.s * invMag} +NormalizeChecked :: proc "odin" (v: Vec2) -> Vec2 { + length := Length(v) + if length < 1e-23 { + panic("zero-length Vec2") + } + invLength := 1 / length + return invLength * v } -// Is this rotation normalized? @(require_results) -IsNormalized :: proc "c" (q: Rot) -> bool { - // larger tolerance due to failure on mingw 32-bit - qq := q.s * q.s + q.c * q.c - return 1.0 - 0.0006 < qq && qq < 1 + 0.0006 -} - -// Normalized linear interpolation -// https://fgiesen.wordpress.com/2012/08/15/linear-interpolation-past-present-and-future/ -@(require_results) -NLerp :: proc "c" (q1: Rot, q2: Rot, t: f32) -> Rot { - omt := 1 - t - return NormalizeRot({ - omt * q1.c + t * q2.c, - omt * q1.s + t * q2.s, - }) +GetLengthAndNormalize :: proc "c" (v: Vec2) -> (length: f32, vn: Vec2) { + length = Length(v) + if length < 1e-23 { + return + } + invLength := 1 / length + vn = invLength * v + return } // Integration rotation from angular velocity @@ -268,6 +286,63 @@ IntegrateRotation :: proc "c" (q1: Rot, deltaAngle: f32) -> Rot { return {q2.c * invMag, q2.s * invMag} } +// Get the length squared of this vector +@(require_results) +LengthSquared :: proc "c" (v: Vec2) -> f32 { + return v.x * v.x + v.y * v.y +} + +// Get the distance squared between points +@(require_results) +DistanceSquared :: proc "c" (a, b: Vec2) -> f32 { + c := Vec2{b.x - a.x, b.y - a.y} + return c.x * c.x + c.y * c.y +} + +// Make a rotation using an angle in radians +@(require_results) +MakeRot :: proc "c" (angle: f32) -> Rot { + cs := ComputeCosSin(angle) + return Rot{c=cs.cosine, s=cs.sine} +} + +// Compute the rotation between two unit vectors +@(require_results) +ComputeRotationBetweenUnitVectors :: proc(v1, v2: Vec2) -> Rot { + return NormalizeRot({ + c = Dot(v1, v2), + s = Cross(v1, v2), + }) +} + +// Is this rotation normalized? +@(require_results) +IsNormalizedRot :: proc "c" (q: Rot) -> bool { + // larger tolerance due to failure on mingw 32-bit + qq := q.s * q.s + q.c * q.c + return 1.0 - 0.0006 < qq && qq < 1 + 0.0006 +} + +// Normalize rotation +@(require_results) +NormalizeRot :: proc "c" (q: Rot) -> Rot { + mag := math.sqrt(q.s * q.s + q.c * q.c) + invMag := f32(mag > 0.0 ? 1.0 / mag : 0.0) + return {q.c * invMag, q.s * invMag} +} + +// Normalized linear interpolation +// https://fgiesen.wordpress.com/2012/08/15/linear-interpolation-past-present-and-future/ +// https://web.archive.org/web/20170825184056/http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/ +@(require_results) +NLerp :: proc "c" (q1: Rot, q2: Rot, t: f32) -> Rot { + omt := 1 - t + return NormalizeRot({ + omt * q1.c + t * q2.c, + omt * q1.s + t * q2.s, + }) +} + // Compute the angular velocity necessary to rotate between two rotations over a give time // @param q1 initial rotation // @param q2 final rotation @@ -291,8 +366,7 @@ ComputeAngularVelocity :: proc "c" (q1: Rot, q2: Rot, inv_h: f32) -> f32 { // Get the angle in radians in the range [-pi, pi] @(require_results) Rot_GetAngle :: proc "c" (q: Rot) -> f32 { - // todo determinism - return math.atan2(q.s, q.c) + return Atan2(q.s, q.c) } // Get the x-axis @@ -338,18 +412,34 @@ RelativeAngle :: proc "c" (b, a: Rot) -> f32 { // cos(b - a) = bc * ac + bs * as s := b.s * a.c - b.c * a.s c := b.c * a.c + b.s * a.s - return math.atan2(s, c) + return Atan2(s, c) } // Convert an angle in the range [-2*pi, 2*pi] into the range [-pi, pi] @(require_results) -UnwindAngle :: proc "c" (angle: f32) -> f32 { - if angle < -pi { - return angle + 2.0 * pi - } else if angle > pi { - return angle - 2.0 * pi +UnwindAngle :: proc "c" (radians: f32) -> f32 { + if radians < -PI { + return radians + 2.0 * PI + } else if radians > PI { + return radians - 2.0 * PI } - return angle + return radians +} + +// Convert any into the range [-pi, pi] (slow) +@(require_results) +UnwindLargeAngle :: proc "c" (radians: f32) -> f32 { + radians := radians + + for radians > PI { + radians -= 2. * PI + } + + for radians < -PI { + radians += 2. * PI + } + + return radians } // Rotate a vector @@ -380,6 +470,9 @@ InvTransformPoint :: proc "c" (t: Transform, p: Vec2) -> Vec2 { return {t.q.c * vx + t.q.s * vy, -t.q.s * vx + t.q.c * vy} } +// Multiply two transforms. If the result is applied to a point p local to frame B, +// the transform would first convert p to a point local to frame A, then into a point +// in the world frame. // v2 = A.q.Rot(B.q.Rot(v1) + B.p) + A.p // = (A.q * B.q).Rot(v1) + A.q.Rot(B.p) + A.p @(require_results) @@ -389,6 +482,7 @@ MulTransforms :: proc "c" (A, B: Transform) -> (C: Transform) { return } +// Creates a transform that converts a local point in frame B to a local point in frame A. // v2 = A.q' * (B.q * v1 + B.p - A.p) // = A.q' * B.q * v1 + A.q' * (B.p - A.p) @(require_results) @@ -469,54 +563,65 @@ AABB_Union :: proc "c" (a, b: AABB) -> (c: AABB) { return } +// Compute the bounding box of an array of circles @(require_results) -Float_IsValid :: proc "c" (a: f32) -> bool { - math.is_nan(a) or_return - math.is_inf(a) or_return +MakeAABB :: proc "c" (points: []Vec2, radius: f32) -> AABB { + a := AABB{points[0], points[0]} + for point in points { + a.lowerBound = Min(a.lowerBound, point) + a.upperBound = Max(a.upperBound, point) + } + + r := Vec2{radius, radius} + a.lowerBound = a.lowerBound - r + a.upperBound = a.upperBound + r + + return a +} + +// Signed separation of a point from a plane +@(require_results) +PlaneSeparation :: proc "c" (plane: Plane, point: Vec2) -> f32 { + return Dot(plane.normal, point) - plane.offset +} + +@(require_results) +IsValidFloat :: proc "c" (a: f32) -> bool { + #partial switch math.classify(a) { + case .NaN, .Inf, .Neg_Inf: return false + case: return true + } +} + +@(require_results) +IsValidVec2 :: proc "c" (v: Vec2) -> bool { + IsValidFloat(v.x) or_return + IsValidFloat(v.y) or_return return true } @(require_results) -Vec2_IsValid :: proc "c" (v: Vec2) -> bool { - (math.is_nan(v.x) || math.is_nan(v.y)) or_return - (math.is_inf(v.x) || math.is_inf(v.y)) or_return +IsValidRotation :: proc "c" (q: Rot) -> bool { + IsValidFloat(q.s) or_return + IsValidFloat(q.c) or_return + return IsNormalizedRot(q) +} + +// Is this a valid bounding box? Not Nan or infinity. Upper bound greater than or equal to lower bound. +@(require_results) +IsValidAABB :: proc "c" (aabb: AABB) -> bool { + IsValidVec2(aabb.lowerBound) or_return + IsValidVec2(aabb.upperBound) or_return + (aabb.upperBound.x >= aabb.lowerBound.x) or_return + (aabb.upperBound.y >= aabb.lowerBound.y) or_return return true } +// Is this a valid plane? Normal is a unit vector. Not Nan or infinity. @(require_results) -Rot_IsValid :: proc "c" (q: Rot) -> bool { - (math.is_nan(q.s) || math.is_nan(q.c)) or_return - (math.is_inf(q.s) || math.is_inf(q.c)) or_return - return IsNormalized(q) -} - -@(require_results) -Normalize :: proc "c" (v: Vec2) -> Vec2 { - length := Length(v) - if length < 1e-23 { - return Vec2_zero - } - invLength := 1 / length - return invLength * v -} - -@(require_results) -NormalizeChecked :: proc "odin" (v: Vec2) -> Vec2 { - length := Length(v) - if length < 1e-23 { - panic("zero-length Vec2") - } - invLength := 1 / length - return invLength * v -} - -@(require_results) -GetLengthAndNormalize :: proc "c" (v: Vec2) -> (length: f32, vn: Vec2) { - length = Length(v) - if length < 1e-23 { - return - } - invLength := 1 / length - vn = invLength * v - return +IsValidPlane :: proc "c" (plane: Plane) -> bool { + IsValidFloat(plane.offset) or_return + IsValidVec2(plane.normal) or_return + IsNormalized(plane.normal) or_return + return true } diff --git a/vendor/box2d/types.odin b/vendor/box2d/types.odin index 1ea7cb65a..c4ef0cd0c 100644 --- a/vendor/box2d/types.odin +++ b/vendor/box2d/types.odin @@ -37,14 +37,28 @@ EnqueueTaskCallback :: #type proc "c" (task: TaskCallback, itemCount: i32, minRa // @ingroup world FinishTaskCallback :: #type proc "c" (userTask: rawptr, userContext: rawptr) +// Optional friction mixing callback. This intentionally provides no context objects because this is called +// from a worker thread. +// @warning This function should not attempt to modify Box2D state or user application state. +// @ingroup world +FrictionCallback :: #type proc "c" (frictionA: f32, userMaterialIdA: i32, frictionB: f32, userMaterialIdB: i32) + +// Optional restitution mixing callback. This intentionally provides no context objects because this is called +// from a worker thread. +// @warning This function should not attempt to modify Box2D state or user application state. +// @ingroup world +RestitutionCallback :: #type proc "c" (restitutionA: f32, userMaterialIdA: i32, restitutuionB: f32, userMaterialIdB: i32) + // Result from b2World_RayCastClosest // @ingroup world RayResult :: struct { - shapeId: ShapeId, - point: Vec2, - normal: Vec2, - fraction: f32, - hit: bool, + shapeId: ShapeId, + point: Vec2, + normal: Vec2, + fraction: f32, + nodeVisits: i32, + leafVisits: i32, + hit: bool, } // World definition used to create a simulation world. @@ -54,37 +68,53 @@ WorldDef :: struct { // Gravity vector. Box2D has no up-vector defined. gravity: Vec2, - // Restitution velocity threshold, usually in m/s. Collisions above this + // Restitution speed threshold, usually in m/s. Collisions above this // speed have restitution applied (will bounce). restitutionThreshold: f32, - // This parameter controls how fast overlap is resolved and has units of meters per second - contactPushoutVelocity: f32, - - // Threshold velocity for hit events. Usually meters per second. + // Threshold speed for hit events. Usually meters per second. hitEventThreshold: f32, - // Contact stiffness. Cycles per second. + // Contact stiffness. Cycles per second. Increasing this increases the speed of overlap recovery, but can introduce jitter. contactHertz: f32, - // Contact bounciness. Non-dimensional. + // Contact bounciness. Non-dimensional. You can speed up overlap recovery by decreasing this with + // the trade-off that overlap resolution becomes more energetic. contactDampingRatio: f32, + // This parameter controls how fast overlap is resolved and usually has units of meters per second. This only + // puts a cap on the resolution speed. The resolution speed is increased by increasing the hertz and/or + // decreasing the damping ratio. + maxContactPushSpeed: f32, + // Joint stiffness. Cycles per second. jointHertz: f32, // Joint bounciness. Non-dimensional. jointDampingRatio: f32, + // Maximum linear speed. Usually meters per second. + maximumLinearSpeed: f32, + + // Optional mixing callback for friction. The default uses sqrt(frictionA * frictionB). + frictionCallback: FrictionCallback, + + // Optional mixing callback for restitution. The default uses max(restitutionA, restitutionB). + restitutionCallback: RestitutionCallback, + // Can bodies go to sleep to improve performance enableSleep: bool, // Enable continuous collision - enableContinous: bool, + enableContinuous: bool, // Number of workers to use with the provided task system. Box2D performs best when using only - // performance cores and accessing a single L2 cache. Efficiency cores and hyper-threading provide - // little benefit and may even harm performance. + // performance cores and accessing a single L2 cache. Efficiency cores and hyper-threading provide + // little benefit and may even harm performance. + // @note Box2D does not create threads. This is the number of threads your applications has created + // that you are allocating to b2World_Step. + // @warning Do not modify the default value unless you are also providing a task system and providing + // task callbacks (enqueueTask and finishTask). workerCount: i32, // Function to spawn tasks @@ -96,6 +126,9 @@ WorldDef :: struct { // User context that is provided to enqueueTask and finishTask userTaskContext: rawptr, + // User data + userData: rawptr, + // Used internally to detect a valid definition. DO NOT SET. internalValue: i32, } @@ -138,20 +171,20 @@ BodyDef :: struct { // The initial world rotation of the body. Use b2MakeRot() if you have an angle. rotation: Rot, - // The initial linear velocity of the body's origin. Typically in meters per second. + // The initial linear velocity of the body's origin. Usually in meters per second. linearVelocity: Vec2, // The initial angular velocity of the body. Radians per second. angularVelocity: f32, - // Linear damping is use to reduce the linear velocity. The damping parameter + // Linear damping is used to reduce the linear velocity. The damping parameter // can be larger than 1 but the damping effect becomes sensitive to the // time step when the damping parameter is large. // Generally linear damping is undesirable because it makes objects move slowly // as if they are f32ing. linearDamping: f32, - // Angular damping is use to reduce the angular velocity. The damping parameter + // Angular damping is used to reduce the angular velocity. The damping parameter // can be larger than 1.0f but the damping effect becomes sensitive to the // time step when the damping parameter is large. // Angular damping can be use slow down rotating bodies. @@ -160,9 +193,12 @@ BodyDef :: struct { // Scale the gravity applied to this body. Non-dimensional. gravityScale: f32, - // Sleep velocity threshold, default is 0.05 meter per second + // Sleep speed threshold, default is 0.05 meters per second sleepThreshold: f32, + // Optional body name for debugging. Up to 32 characters (excluding null termination) + name: cstring, + // Use this to store application specific body data. userData: rawptr, @@ -184,9 +220,9 @@ BodyDef :: struct { // Used to disable a body. A disabled body does not move or collide. isEnabled: bool, - // Automatically compute mass and related properties on this body from shapes. - // Triggers whenever a shape is add/removed/changed. Default is true. - automaticMass: bool, + // This allows this body to bypass rotational speed limits. Should only be used + // for circular objects, like wheels. + allowFastRotation: bool, // Used internally to detect a valid definition. DO NOT SET. internalValue: i32, @@ -200,7 +236,7 @@ Filter :: struct { // The collision category bits. Normally you would just set one bit. The category bits should // represent your application object types. For example: // @code{.odin} - // My_Categories :: enum u32 { + // My_Categories :: enum u64 { // Static = 0x00000001, // Dynamic = 0x00000002, // Debris = 0x00000004, @@ -209,16 +245,16 @@ Filter :: struct { // }; // @endcode // Or use a bit_set. - categoryBits: u32, + categoryBits: u64, // The collision mask bits. This states the categories that this // shape would accept for collision. // For example, you may want your player to only collide with static objects // and other players. // @code{.odin} - // maskBits = u32(My_Categories.Static | My_Categories.Player); + // maskBits = u64(My_Categories.Static | My_Categories.Player); // @endcode - maskBits: u32, + maskBits: u64, // Collision groups allow a certain group of objects to never collide (negative) // or always collide (positive). A group index of zero has no effect. Non-zero group filtering @@ -236,11 +272,11 @@ Filter :: struct { // @ingroup shape QueryFilter :: struct { // The collision category bits of this query. Normally you would just set one bit. - categoryBits: u32, + categoryBits: u64, // The collision mask bits. This states the shape categories that this // query would accept for collision. - maskBits: u32, + maskBits: u64, } @@ -259,13 +295,37 @@ ShapeType :: enum c.int { // A convex polygon polygonShape, - // A smooth segment owned by a chain shape - smoothSegmentShape, + // A line segment owned by a chain shape + chainSegmentShape, } // The number of shape types shapeTypeCount :: len(ShapeType) +// Surface materials allow chain shapes to have per segment surface properties. +// @ingroup shape +SurfaceMaterial :: struct { + // The Coulomb (dry) friction coefficient, usually in the range [0,1]. + friction: f32, + + // The coefficient of restitution (bounce) usually in the range [0,1]. + // https://en.wikipedia.org/wiki/Coefficient_of_restitution + restitution: f32, + + // The rolling resistance usually in the range [0,1]. + rollingResistance: f32, + + // The tangent speed for conveyor belts + tangentSpeed: f32, + + // User material identifier. This is passed with query results and to friction and restitution + // combining functions. It is not used internally. + userMaterialId: i32, + + // Custom debug draw color. + customColor: u32, +} + // Used to create a shape. // This is a temporary object used to bundle shape creation parameters. You may use // the same shape definition to create multiple shapes. @@ -275,58 +335,61 @@ ShapeDef :: struct { // Use this to store application specific shape data. userData: rawptr, - // The Coulomb (dry) friction coefficient, usually in the range [0,1]. - friction: f32, - - // The restitution (bounce) usually in the range [0,1]. - restitution: f32, + // The surface material for this shape. + material: SurfaceMaterial, // The density, usually in kg/m^2. + // This is not part of the surface material because this is for the interior, which may have + // other considerations, such as being hollow. For example a wood barrel may be hollow or full of water. density: f32, // Collision filtering data. filter: Filter, - // Custom debug draw color. - customColor: u32, - // A sensor shape generates overlap events but never generates a collision response. + // Sensors do not have continuous collision. Instead, use a ray or shape cast for those scenarios. + // Sensors still contribute to the body mass if they have non-zero density. + // @note Sensor events are disabled by default. + // @see enableSensorEvents isSensor: bool, - // Enable sensor events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. + // Enable sensor events for this shape. This applies to sensors and non-sensors. False by default, even for sensors. enableSensorEvents: bool, - // Enable contact events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. + // Enable contact events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. False by default. enableContactEvents: bool, - // Enable hit events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. + // Enable hit events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. False by default. enableHitEvents: bool, // Enable pre-solve contact events for this shape. Only applies to dynamic bodies. These are expensive // and must be carefully handled due to threading. Ignored for sensors. enablePreSolveEvents: bool, - // Normally shapes on static bodies don't invoke contact creation when they are added to the world. This overrides - // that behavior and causes contact creation. This significantly slows down static body creation which can be important - // when there are many static shapes. - forceContactCreation: bool, + // When shapes are created they will scan the environment for collision the next time step. This can significantly slow down + // static body creation when there are many static shapes. + // This is flag is ignored for dynamic and kinematic shapes which always invoke contact creation. + invokeContactCreation: bool, + + // Should the body update the mass properties when this shape is created. Default is true. + updateBodyMass: bool, // Used internally to detect a valid definition. DO NOT SET. internalValue: i32, } -// Used to create a chain of edges. This is designed to eliminate ghost collisions with some limitations. +// Used to create a chain of line segments. This is designed to eliminate ghost collisions with some limitations. // - chains are one-sided // - chains have no mass and should be used on static bodies -// - chains have a counter-clockwise winding order +// - chains have a counter-clockwise winding order (normal points right of segment direction) // - chains are either a loop or open // - a chain must have at least 4 points -// - the distance between any two points must be greater than b2_linearSlop +// - the distance between any two points must be greater than B2_LINEAR_SLOP // - a chain shape should not self intersect (this is not validated) // - an open chain shape has NO COLLISION on the first and final edge // - you may overlap two open chains on their first three and/or last three points to get smooth collision -// - a chain shape creates multiple smooth edges shapes on the body +// - a chain shape creates multiple line segment shapes on the body // https://en.wikipedia.org/wiki/Polygonal_chain // Must be initialized using b2DefaultChainDef(). // @warning Do not use chain shapes unless you understand the limitations. This is an advanced feature. @@ -341,11 +404,12 @@ ChainDef :: struct { // The point count, must be 4 or more. count: i32, - // The friction coefficient, usually in the range [0,1]. - friction: f32, + // Surface materials for each segment. These are cloned. + materials: [^]SurfaceMaterial `fmt:"v,materialCount"`, - // The restitution (elasticity) usually in the range [0,1]. - restitution: f32, + // The material count. Must be 1 or count. This allows you to provide one + // material for all segments or a unique material per segment. + materialCount: i32, // Contact filtering data. filter: Filter, @@ -353,6 +417,9 @@ ChainDef :: struct { // Indicates a closed chain formed by connecting the first and last points isLoop: bool, + // Enable sensors to detect this chain. False by default. + enableSensorEvents: bool, + // Used internally to detect a valid definition. DO NOT SET. internalValue: i32, } @@ -365,29 +432,28 @@ Profile :: struct { pairs: f32, collide: f32, solve: f32, - buildIslands: f32, + mergeIslands: f32, + prepareStages: f32, solveConstraints: f32, - prepareTasks: f32, - solverTasks: f32, prepareConstraints: f32, integrateVelocities: f32, warmStart: f32, - solveVelocities: f32, + solveImpulses: f32, integratePositions: f32, - relaxVelocities: f32, + relaxImpulses: f32, applyRestitution: f32, storeImpulses: f32, - finalizeBodies: f32, splitIslands: f32, - sleepIslands: f32, + transforms: f32, hitEvents: f32, - broadphase: f32, - continuous: f32, + refit: f32, + bullets: f32, + sleepIslands: f32, + sensors: f32, } // Counters that give details of the simulation size. Counters :: struct { - staticBodyCount: i32, bodyCount: i32, shapeCount: i32, contactCount: i32, @@ -409,6 +475,7 @@ Counters :: struct { // @ingroup joint JointType :: enum c.int { distanceJoint, + filterJoint, motorJoint, mouseJoint, prismaticJoint, @@ -522,7 +589,7 @@ MotorJointDef :: struct { // applying huge forces. This also applies rotation constraint heuristic to improve control. // @ingroup mouse_joint MouseJointDef :: struct { - // The first attached body. + // The first attached body. This is assumed to be static. bodyIdA: BodyId, // The second attached body. @@ -550,6 +617,22 @@ MouseJointDef :: struct { internalValue: i32, } +// A filter joint is used to disable collision between two specific bodies. +// +// @ingroup filter_joint +FilterJointDef :: struct { + /// The first attached body. + bodyIdA: BodyId, + + /// The second attached body. + bodyIdB: BodyId, + + /// User data pointer + userData: rawptr, + + /// Used internally to detect a valid definition. DO NOT SET. + internalValue: i32, +} // Prismatic joint definition // @@ -656,10 +739,10 @@ RevoluteJointDef :: struct { // A flag to enable joint limits enableLimit: bool, - // The lower angle for the joint limit in radians + // The lower angle for the joint limit in radians. Minimum of -0.95*pi radians. lowerAngle: f32, - // The upper angle for the joint limit in radians + // The upper angle for the joint limit in radians. Maximum of 0.95*pi radians. upperAngle: f32, // A flag to enable the joint motor @@ -790,6 +873,27 @@ WheelJointDef :: struct { internalValue: i32, } +// The explosion definition is used to configure options for explosions. Explosions +// consider shape geometry when computing the impulse. +// @ingroup world +ExplosionDef :: struct { + /// Mask bits to filter shapes + maskBits: u64, + + /// The center of the explosion in world space + position: Vec2, + + /// The radius of the explosion + radius: f32, + + /// The falloff distance beyond the radius. Impulse is reduced to zero at this distance. + falloff: f32, + + /// Impulse per unit length. This applies an impulse according to the shape perimeter that + /// is facing the explosion. Explosions only apply to circles, capsules, and polygons. This + /// may be negative for implosions. + impulsePerLength: f32, +} /** * @defgroup events Events @@ -818,11 +922,18 @@ SensorBeginTouchEvent :: struct { } // An end touch event is generated when a shape stops overlapping a sensor shape. +// These include things like setting the transform, destroying a body or shape, or changing +// a filter. You will also get an end event if the sensor or visitor are destroyed. +// Therefore you should always confirm the shape id is valid using b2Shape_IsValid. SensorEndTouchEvent :: struct { // The id of the sensor shape + // @warning this shape may have been destroyed + // @see b2Shape_IsValid sensorShapeId: ShapeId, // The id of the dynamic shape that stopped touching the sensor shape + // @warning this shape may have been destroyed + // @see b2Shape_IsValid visitorShapeId: ShapeId, } @@ -850,14 +961,25 @@ ContactBeginTouchEvent :: struct { // Id of the second shape shapeIdB: ShapeId, + + // The initial contact manifold. This is recorded before the solver is called, + // so all the impulses will be zero. + manifold: Manifold, } // An end touch event is generated when two shapes stop touching. +// You will get an end event if you do anything that destroys contacts previous to the last +// world step. These include things like setting the transform, destroying a body +// or shape, or changing a filter or body type. ContactEndTouchEvent :: struct { // Id of the first shape + // @warning this shape may have been destroyed + // @see b2Shape_IsValid shapeIdA: ShapeId, // Id of the second shape + // @warning this shape may have been destroyed + // @see b2Shape_IsValid shapeIdB: ShapeId, } @@ -994,201 +1116,191 @@ OverlapResultFcn :: #type proc "c" (shapeId: ShapeId, ctx: rawptr) -> bool // @ingroup world CastResultFcn :: #type proc "c" (shapeId: ShapeId, point: Vec2, normal: Vec2, fraction: f32, ctx: rawptr) -> f32 -// These colors are used for debug draw. -// See https://www.rapidtables.com/web/color/index.html +// Used to collect collision planes for character movers. +// Return true to continue gathering planes. +PlaneResultFcn :: #type proc "c" (shapeId: ShapeId, plane: ^PlaneResult, ctx: rawptr) -> bool + +// These colors are used for debug draw and mostly match the named SVG colors. +// See https://www.rapidtables.com/web/color/index.html +// https://johndecember.com/html/spec/colorsvg.html +// https://upload.wikimedia.org/wikipedia/commons/2/2b/SVG_Recognized_color_keyword_names.svg HexColor :: enum c.int { - AliceBlue = 0xf0f8ff, - AntiqueWhite = 0xfaebd7, - Aqua = 0x00ffff, - Aquamarine = 0x7fffd4, - Azure = 0xf0ffff, - Beige = 0xf5f5dc, - Bisque = 0xffe4c4, + AliceBlue = 0xF0F8FF, + AntiqueWhite = 0xFAEBD7, + Aqua = 0x00FFFF, + Aquamarine = 0x7FFFD4, + Azure = 0xF0FFFF, + Beige = 0xF5F5DC, + Bisque = 0xFFE4C4, Black = 0x000000, - BlanchedAlmond = 0xffebcd, - Blue = 0x0000ff, - BlueViolet = 0x8a2be2, - Brown = 0xa52a2a, - Burlywood = 0xdeb887, - CadetBlue = 0x5f9ea0, - Chartreuse = 0x7fff00, - Chocolate = 0xd2691e, - Coral = 0xff7f50, - CornflowerBlue = 0x6495ed, - Cornsilk = 0xfff8dc, - Crimson = 0xdc143c, - Cyan = 0x00ffff, - DarkBlue = 0x00008b, - DarkCyan = 0x008b8b, - DarkGoldenrod = 0xb8860b, - DarkGray = 0xa9a9a9, + BlanchedAlmond = 0xFFEBCD, + Blue = 0x0000FF, + BlueViolet = 0x8A2BE2, + Brown = 0xA52A2A, + Burlywood = 0xDEB887, + CadetBlue = 0x5F9EA0, + Chartreuse = 0x7FFF00, + Chocolate = 0xD2691E, + Coral = 0xFF7F50, + CornflowerBlue = 0x6495ED, + Cornsilk = 0xFFF8DC, + Crimson = 0xDC143C, + Cyan = 0x00FFFF, + DarkBlue = 0x00008B, + DarkCyan = 0x008B8B, + DarkGoldenRod = 0xB8860B, + DarkGray = 0xA9A9A9, DarkGreen = 0x006400, - DarkKhaki = 0xbdb76b, - DarkMagenta = 0x8b008b, - DarkOliveGreen = 0x556b2f, - DarkOrange = 0xff8c00, - DarkOrchid = 0x9932cc, - DarkRed = 0x8b0000, - DarkSalmon = 0xe9967a, - DarkSeaGreen = 0x8fbc8f, - DarkSlateBlue = 0x483d8b, - DarkSlateGray = 0x2f4f4f, - DarkTurquoise = 0x00ced1, - DarkViolet = 0x9400d3, - DeepPink = 0xff1493, - DeepSkyBlue = 0x00bfff, + DarkKhaki = 0xBDB76B, + DarkMagenta = 0x8B008B, + DarkOliveGreen = 0x556B2F, + DarkOrange = 0xFF8C00, + DarkOrchid = 0x9932CC, + DarkRed = 0x8B0000, + DarkSalmon = 0xE9967A, + DarkSeaGreen = 0x8FBC8F, + DarkSlateBlue = 0x483D8B, + DarkSlateGray = 0x2F4F4F, + DarkTurquoise = 0x00CED1, + DarkViolet = 0x9400D3, + DeepPink = 0xFF1493, + DeepSkyBlue = 0x00BFFF, DimGray = 0x696969, - DodgerBlue = 0x1e90ff, - Firebrick = 0xb22222, - FloralWhite = 0xfffaf0, - ForestGreen = 0x228b22, - Fuchsia = 0xff00ff, - Gainsboro = 0xdcdcdc, - GhostWhite = 0xf8f8ff, - Gold = 0xffd700, - Goldenrod = 0xdaa520, - Gray = 0xbebebe, - Gray1 = 0x1a1a1a, - Gray2 = 0x333333, - Gray3 = 0x4d4d4d, - Gray4 = 0x666666, - Gray5 = 0x7f7f7f, - Gray6 = 0x999999, - Gray7 = 0xb3b3b3, - Gray8 = 0xcccccc, - Gray9 = 0xe5e5e5, - Green = 0x00ff00, - GreenYellow = 0xadff2f, - Honeydew = 0xf0fff0, - HotPink = 0xff69b4, - IndianRed = 0xcd5c5c, - Indigo = 0x4b0082, - Ivory = 0xfffff0, - Khaki = 0xf0e68c, - Lavender = 0xe6e6fa, - LavenderBlush = 0xfff0f5, - LawnGreen = 0x7cfc00, - LemonChiffon = 0xfffacd, - LightBlue = 0xadd8e6, - LightCoral = 0xf08080, - LightCyan = 0xe0ffff, - LightGoldenrod = 0xeedd82, - LightGoldenrodYellow = 0xfafad2, - LightGray = 0xd3d3d3, - LightGreen = 0x90ee90, - LightPink = 0xffb6c1, - LightSalmon = 0xffa07a, - LightSeaGreen = 0x20b2aa, - LightSkyBlue = 0x87cefa, - LightSlateBlue = 0x8470ff, + DodgerBlue = 0x1E90FF, + FireBrick = 0xB22222, + FloralWhite = 0xFFFAF0, + ForestGreen = 0x228B22, + Fuchsia = 0xFF00FF, + Gainsboro = 0xDCDCDC, + GhostWhite = 0xF8F8FF, + Gold = 0xFFD700, + GoldenRod = 0xDAA520, + Gray = 0x808080, + Green = 0x008000, + GreenYellow = 0xADFF2F, + HoneyDew = 0xF0FFF0, + HotPink = 0xFF69B4, + IndianRed = 0xCD5C5C, + Indigo = 0x4B0082, + Ivory = 0xFFFFF0, + Khaki = 0xF0E68C, + Lavender = 0xE6E6FA, + LavenderBlush = 0xFFF0F5, + LawnGreen = 0x7CFC00, + LemonChiffon = 0xFFFACD, + LightBlue = 0xADD8E6, + LightCoral = 0xF08080, + LightCyan = 0xE0FFFF, + LightGoldenRodYellow = 0xFAFAD2, + LightGray = 0xD3D3D3, + LightGreen = 0x90EE90, + LightPink = 0xFFB6C1, + LightSalmon = 0xFFA07A, + LightSeaGreen = 0x20B2AA, + LightSkyBlue = 0x87CEFA, LightSlateGray = 0x778899, - LightSteelBlue = 0xb0c4de, - LightYellow = 0xffffe0, - Lime = 0x00ff00, - LimeGreen = 0x32cd32, - Linen = 0xfaf0e6, - Magenta = 0xff00ff, - Maroon = 0xb03060, - MediumAquamarine = 0x66cdaa, - MediumBlue = 0x0000cd, - MediumOrchid = 0xba55d3, - MediumPurple = 0x9370db, - MediumSeaGreen = 0x3cb371, - MediumSlateBlue = 0x7b68ee, - MediumSpringGreen = 0x00fa9a, - MediumTurquoise = 0x48d1cc, - MediumVioletRed = 0xc71585, + LightSteelBlue = 0xB0C4DE, + LightYellow = 0xFFFFE0, + Lime = 0x00FF00, + LimeGreen = 0x32CD32, + Linen = 0xFAF0E6, + Magenta = 0xFF00FF, + Maroon = 0x800000, + MediumAquaMarine = 0x66CDAA, + MediumBlue = 0x0000CD, + MediumOrchid = 0xBA55D3, + MediumPurple = 0x9370DB, + MediumSeaGreen = 0x3CB371, + MediumSlateBlue = 0x7B68EE, + MediumSpringGreen = 0x00FA9A, + MediumTurquoise = 0x48D1CC, + MediumVioletRed = 0xC71585, MidnightBlue = 0x191970, - MintCream = 0xf5fffa, - MistyRose = 0xffe4e1, - Moccasin = 0xffe4b5, - NavajoWhite = 0xffdead, + MintCream = 0xF5FFFA, + MistyRose = 0xFFE4E1, + Moccasin = 0xFFE4B5, + NavajoWhite = 0xFFDEAD, Navy = 0x000080, - NavyBlue = 0x000080, - OldLace = 0xfdf5e6, + OldLace = 0xFDF5E6, Olive = 0x808000, - OliveDrab = 0x6b8e23, - Orange = 0xffa500, - OrangeRed = 0xff4500, - Orchid = 0xda70d6, - PaleGoldenrod = 0xeee8aa, - PaleGreen = 0x98fb98, - PaleTurquoise = 0xafeeee, - PaleVioletRed = 0xdb7093, - PapayaWhip = 0xffefd5, - PeachPuff = 0xffdab9, - Peru = 0xcd853f, - Pink = 0xffc0cb, - Plum = 0xdda0dd, - PowderBlue = 0xb0e0e6, - Purple = 0xa020f0, + OliveDrab = 0x6B8E23, + Orange = 0xFFA500, + OrangeRed = 0xFF4500, + Orchid = 0xDA70D6, + PaleGoldenRod = 0xEEE8AA, + PaleGreen = 0x98FB98, + PaleTurquoise = 0xAFEEEE, + PaleVioletRed = 0xDB7093, + PapayaWhip = 0xFFEFD5, + PeachPuff = 0xFFDAB9, + Peru = 0xCD853F, + Pink = 0xFFC0CB, + Plum = 0xDDA0DD, + PowderBlue = 0xB0E0E6, + Purple = 0x800080, RebeccaPurple = 0x663399, - Red = 0xff0000, - RosyBrown = 0xbc8f8f, - RoyalBlue = 0x4169e1, - SaddleBrown = 0x8b4513, - Salmon = 0xfa8072, - SandyBrown = 0xf4a460, - SeaGreen = 0x2e8b57, - Seashell = 0xfff5ee, - Sienna = 0xa0522d, - Silver = 0xc0c0c0, - SkyBlue = 0x87ceeb, - SlateBlue = 0x6a5acd, + Red = 0xFF0000, + RosyBrown = 0xBC8F8F, + RoyalBlue = 0x4169E1, + SaddleBrown = 0x8B4513, + Salmon = 0xFA8072, + SandyBrown = 0xF4A460, + SeaGreen = 0x2E8B57, + SeaShell = 0xFFF5EE, + Sienna = 0xA0522D, + Silver = 0xC0C0C0, + SkyBlue = 0x87CEEB, + SlateBlue = 0x6A5ACD, SlateGray = 0x708090, - Snow = 0xfffafa, - SpringGreen = 0x00ff7f, - SteelBlue = 0x4682b4, - Tan = 0xd2b48c, + Snow = 0xFFFAFA, + SpringGreen = 0x00FF7F, + SteelBlue = 0x4682B4, + Tan = 0xD2B48C, Teal = 0x008080, - Thistle = 0xd8bfd8, - Tomato = 0xff6347, - Turquoise = 0x40e0d0, - Violet = 0xee82ee, - VioletRed = 0xd02090, - Wheat = 0xf5deb3, - White = 0xffffff, - WhiteSmoke = 0xf5f5f5, - Yellow = 0xffff00, - YellowGreen = 0x9acd32, - Box2DRed = 0xdc3132, - Box2DBlue = 0x30aebf, - Box2DGreen = 0x8cc924, - Box2DYellow = 0xffee8c, + Thistle = 0xD8BFD8, + Tomato = 0xFF6347, + Turquoise = 0x40E0D0, + Violet = 0xEE82EE, + Wheat = 0xF5DEB3, + White = 0xFFFFFF, + WhiteSmoke = 0xF5F5F5, + Yellow = 0xFFFF00, + YellowGreen = 0x9ACD32, + Box2DRed = 0xDC3132, + Box2DBlue = 0x30AEBF, + Box2DGreen = 0x8CC924, + Box2DYellow = 0xFFEE8C, } // This struct holds callbacks you can implement to draw a Box2D world. // @ingroup world DebugDraw :: struct { // Draw a closed polygon provided in CCW order. - DrawPolygon: proc "c" (vertices: [^]Vec2, vertexCount: c.int, color: HexColor, ctx: rawptr), + DrawPolygonFcn: proc "c" (vertices: [^]Vec2, vertexCount: c.int, color: HexColor, ctx: rawptr), // Draw a solid closed polygon provided in CCW order. - DrawSolidPolygon: proc "c" (transform: Transform, vertices: [^]Vec2, vertexCount: c.int, radius: f32, colr: HexColor, ctx: rawptr ), + DrawSolidPolygonFcn: proc "c" (transform: Transform, vertices: [^]Vec2, vertexCount: c.int, radius: f32, colr: HexColor, ctx: rawptr ), // Draw a circle. - DrawCircle: proc "c" (center: Vec2, radius: f32, color: HexColor, ctx: rawptr), + DrawCircleFcn: proc "c" (center: Vec2, radius: f32, color: HexColor, ctx: rawptr), // Draw a solid circle. - DrawSolidCircle: proc "c" (transform: Transform, radius: f32, color: HexColor, ctx: rawptr), - - // Draw a capsule. - DrawCapsule: proc "c" (p1, p2: Vec2, radius: f32, color: HexColor, ctx: rawptr), + DrawSolidCircleFcn: proc "c" (transform: Transform, radius: f32, color: HexColor, ctx: rawptr), // Draw a solid capsule. - DrawSolidCapsule: proc "c" (p1, p2: Vec2, radius: f32, color: HexColor, ctx: rawptr), + DrawSolidCapsuleFcn: proc "c" (p1, p2: Vec2, radius: f32, color: HexColor, ctx: rawptr), // Draw a line segment. - DrawSegment: proc "c" (p1, p2: Vec2, color: HexColor, ctx: rawptr), + DrawSegmentFcn: proc "c" (p1, p2: Vec2, color: HexColor, ctx: rawptr), // Draw a transform. Choose your own length scale. - DrawTransform: proc "c" (transform: Transform, ctx: rawptr), + DrawTransformFcn: proc "c" (transform: Transform, ctx: rawptr), // Draw a point. - DrawPoint: proc "c" (p: Vec2, size: f32, color: HexColor, ctx: rawptr), + DrawPointFcn: proc "c" (p: Vec2, size: f32, color: HexColor, ctx: rawptr), - // Draw a string. - DrawString: proc "c" (p: Vec2, s: cstring, ctx: rawptr), + // Draw a string in world space. + DrawStringFcn: proc "c" (p: Vec2, s: cstring, color: HexColor, ctx: rawptr), // Bounds to use if restricting drawing to a rectangular region drawingBounds: AABB, @@ -1206,11 +1318,14 @@ DebugDraw :: struct { drawJointExtras: bool, // Option to draw the bounding boxes for shapes - drawAABBs: bool, + drawBounds: bool, // Option to draw the mass and center of mass of dynamic bodies drawMass: bool, + // Option to draw body names + drawBodyNames: bool, + // Option to draw contact points drawContacts: bool, @@ -1223,9 +1338,15 @@ DebugDraw :: struct { // Option to draw contact normal impulses drawContactImpulses: bool, + // Option to draw contact feature ids + drawContactFeatures: bool, + // Option to draw contact friction impulses drawFrictionImpulses: bool, + // Option to draw islands as bounding boxes + drawIslands: bool, + // User context that is passed as an argument to drawing callback functions userContext: rawptr, -} \ No newline at end of file +} diff --git a/vendor/box2d/wasm.Makefile b/vendor/box2d/wasm.Makefile index e8ecb485e..74f4e4eff 100644 --- a/vendor/box2d/wasm.Makefile +++ b/vendor/box2d/wasm.Makefile @@ -7,20 +7,20 @@ # CC = $(shell brew --prefix llvm)/bin/clang # LD = $(shell brew --prefix llvm)/bin/wasm-ld -VERSION = 3.0.0 +VERSION = 3.1.0 SRCS = $(wildcard box2d-$(VERSION)/src/*.c) OBJS_SIMD = $(SRCS:.c=_simd.o) OBJS = $(SRCS:.c=.o) SYSROOT = $(shell odin root)/vendor/libc -CFLAGS = -Ibox2d-$(VERSION)/include -Ibox2d-$(VERSION)/extern/simde --target=wasm32 -D__EMSCRIPTEN__ -DNDEBUG -O3 --sysroot=$(SYSROOT) +CFLAGS = -Ibox2d-$(VERSION)/include --target=wasm32 -D__EMSCRIPTEN__ -DNDEBUG -O3 --sysroot=$(SYSROOT) -all: lib/box2d_wasm.o lib/box2d_wasm_simd.o clean +all: lib/box2d_wasm.o lib/box2d_wasm_simd.o %.o: %.c - $(CC) -c $(CFLAGS) $< -o $@ + $(CC) -c $(CFLAGS) -DBOX2D_DISABLE_SIMD $< -o $@ %_simd.o: %.c - $(CC) -c $(CFLAGS) -msimd128 $< -o $@ + $(CC) -c $(CFLAGS) -DBOX2D_DISABLE_SIMD -msimd128 $< -o $@ lib/box2d_wasm.o: $(OBJS) $(LD) -r -o lib/box2d_wasm.o $(OBJS) diff --git a/vendor/directx/d3d12/d3d12.odin b/vendor/directx/d3d12/d3d12.odin index a533ab7ae..9cb1eec48 100644 --- a/vendor/directx/d3d12/d3d12.odin +++ b/vendor/directx/d3d12/d3d12.odin @@ -837,6 +837,16 @@ FEATURE :: enum i32 { OPTIONS8 = 36, OPTIONS9 = 37, WAVE_MMA = 38, + OPTIONS10 = 39, + OPTIONS11 = 40, + OPTIONS12 = 41, + OPTIONS13 = 42, + OPTIONS14 = 43, + OPTIONS15 = 44, + OPTIONS16 = 45, + OPTIONS17 = 46, + OPTIONS18 = 47, + OPTIONS19 = 48, } SHADER_MIN_PRECISION_SUPPORT :: enum i32 { @@ -1195,6 +1205,74 @@ FEATURE_DATA_OPTIONS9 :: struct { WaveMMATier: WAVE_MMA_TIER, } +FEATURE_DATA_OPTIONS10 :: struct { + VariableRateShadingSumCombinerSupported: BOOL, + MeshShaderPerPrimitiveShadingRateSupported: BOOL, +} + +FEATURE_DATA_OPTIONS11 :: struct { + AtomicInt64OnDescriptorHeapResourceSupported: BOOL, +} + +TRI_STATE :: enum i32 { + UNKNOWN = -1, + FALSE = 0, + TRUE = 1, +} + +FEATURE_DATA_OPTIONS12 :: struct { + MSPrimitivesPipelineStatisticIncludesCulledPrimitives: TRI_STATE, + EnhancedBarriersSupported: BOOL, + RelaxedFormatCastingSupported: BOOL, +} + +FEATURE_DATA_OPTIONS13 :: struct { + UnrestrictedBufferTextureCopyPitchSupported: BOOL, + UnrestrictedVertexElementAlignmentSupported: BOOL, + InvertedViewportHeightFlipsYSupported: BOOL, + InvertedViewportDepthFlipsZSupported: BOOL, + TextureCopyBetweenDimensionsSupported: BOOL, + AlphaBlendFactorSupported: BOOL, +} + +FEATURE_DATA_OPTIONS14 :: struct { + AdvancedTextureOpsSupported: BOOL, + WriteableMSAATexturesSupported: BOOL, + IndependentFrontAndBackStencilRefMaskSupported: BOOL, +} + +FEATURE_DATA_OPTIONS15 :: struct { + TriangleFanSupported: BOOL, + DynamicIndexBufferStripCutSupported: BOOL, +} + +FEATURE_DATA_OPTIONS16 :: struct { + DynamicDepthBiasSupported: BOOL, + GPUUploadHeapSupported: BOOL, +} + +FEATURE_DATA_OPTIONS17 :: struct { + NonNormalizedCoordinateSamplersSupported: BOOL, + ManualWriteTrackingResourceSupported: BOOL, +} + +FEATURE_DATA_OPTIONS18 :: struct { + RenderPassesValid: BOOL, +} + +FEATURE_DATA_OPTIONS19 :: struct { + MismatchingOutputDimensionsSupported: BOOL, + SupportedSampleCountsWithNoOutputs: u32, + PointSamplingAddressesNeverRoundUp: BOOL, + RasterizerDesc2Supported: BOOL, + NarrowQuadrilateralLinesSupported: BOOL, + AnisoFilterWithPointMipSupported: BOOL, + MaxSamplerDescriptorHeapSize: u32, + MaxSamplerDescriptorHeapSizeWithStaticSamplers: u32, + MaxViewDescriptorHeapSize: u32, + ComputeOnlyCustomHeapSupported: BOOL, +} + WAVE_MMA_INPUT_DATATYPE :: enum i32 { INVALID = 0, BYTE = 1, @@ -1238,10 +1316,11 @@ RESOURCE_ALLOCATION_INFO1 :: struct { } HEAP_TYPE :: enum i32 { - DEFAULT = 1, - UPLOAD = 2, - READBACK = 3, - CUSTOM = 4, + DEFAULT = 1, + UPLOAD = 2, + READBACK = 3, + CUSTOM = 4, + GPU_UPLOAD = 5, } CPU_PAGE_PROPERTY :: enum i32 { @@ -1473,7 +1552,7 @@ RESOURCE_STATE_GENERIC_READ :: RESOURCE_STATES{ .VERTEX_AND_CONSTANT_BUFFER, .INDEX_BUFFER, .NON_PIXEL_SHADER_RESOURCE, .PIXEL_SHADER_RESOURCE, .INDIRECT_ARGUMENT, .COPY_SOURCE, } RESOURCE_STATE_ALL_SHADER_RESOURCE :: RESOURCE_STATES{ - .SHADING_RATE_SOURCE, .INDEX_BUFFER, + .NON_PIXEL_SHADER_RESOURCE, .PIXEL_SHADER_RESOURCE, } RESOURCE_BARRIER_TYPE :: enum i32 { diff --git a/vendor/glfw/bindings/bindings.odin b/vendor/glfw/bindings/bindings.odin index e59239483..75cdab4cd 100644 --- a/vendor/glfw/bindings/bindings.odin +++ b/vendor/glfw/bindings/bindings.odin @@ -193,7 +193,6 @@ foreign glfw { SetWindowPosCallback :: proc(window: WindowHandle, cbfun: WindowPosProc) -> WindowPosProc --- SetFramebufferSizeCallback :: proc(window: WindowHandle, cbfun: FramebufferSizeProc) -> FramebufferSizeProc --- SetDropCallback :: proc(window: WindowHandle, cbfun: DropProc) -> DropProc --- - SetMonitorCallback :: proc(window: WindowHandle, cbfun: MonitorProc) -> MonitorProc --- SetWindowMaximizeCallback :: proc(window: WindowHandle, cbfun: WindowMaximizeProc) -> WindowMaximizeProc --- SetWindowContentScaleCallback :: proc(window: WindowHandle, cbfun: WindowContentScaleProc) -> WindowContentScaleProc --- @@ -204,7 +203,9 @@ foreign glfw { SetCharCallback :: proc(window: WindowHandle, cbfun: CharProc) -> CharProc --- SetCharModsCallback :: proc(window: WindowHandle, cbfun: CharModsProc) -> CharModsProc --- SetCursorEnterCallback :: proc(window: WindowHandle, cbfun: CursorEnterProc) -> CursorEnterProc --- - SetJoystickCallback :: proc(cbfun: JoystickProc) -> JoystickProc --- + + SetMonitorCallback :: proc(cbfun: MonitorProc) -> MonitorProc --- + SetJoystickCallback :: proc(cbfun: JoystickProc) -> JoystickProc --- SetErrorCallback :: proc(cbfun: ErrorProc) -> ErrorProc --- diff --git a/vendor/glfw/bindings/types.odin b/vendor/glfw/bindings/types.odin index 5bdbf9cb9..bb1c7e431 100644 --- a/vendor/glfw/bindings/types.odin +++ b/vendor/glfw/bindings/types.odin @@ -48,7 +48,7 @@ WindowMaximizeProc :: #type proc "c" (window: WindowHandle, iconified: c.int WindowContentScaleProc :: #type proc "c" (window: WindowHandle, xscale, yscale: f32) FramebufferSizeProc :: #type proc "c" (window: WindowHandle, width, height: c.int) DropProc :: #type proc "c" (window: WindowHandle, count: c.int, paths: [^]cstring) -MonitorProc :: #type proc "c" (window: WindowHandle, event: c.int) +MonitorProc :: #type proc "c" (monitor: MonitorHandle, event: c.int) KeyProc :: #type proc "c" (window: WindowHandle, key, scancode, action, mods: c.int) MouseButtonProc :: #type proc "c" (window: WindowHandle, button, action, mods: c.int) diff --git a/vendor/libc/include/math.h b/vendor/libc/include/math.h index 9903ac3f9..fba520e4a 100644 --- a/vendor/libc/include/math.h +++ b/vendor/libc/include/math.h @@ -47,19 +47,19 @@ bool __isnanf(float); bool __isnand(double); #define isnan(x) \ ( sizeof(x) == sizeof(float) ? __isnanf((float)(x)) \ - : : __isnand((double)(x))) + : __isnand((double)(x))) bool __isinff(float); bool __isinfd(double); #define isinf(x) \ ( sizeof(x) == sizeof(float) ? __isinff((float)(x)) \ - : : __isinfd((double)(x))) + : __isinfd((double)(x))) bool __isfinitef(float); bool __isfinited(double); #define isfinite(x) \ ( sizeof(x) == sizeof(float) ? __isfinitef((float)(x)) \ - : : __isfinited((double)(x))) + : __isfinited((double)(x))) #ifdef __cplusplus } diff --git a/vendor/libc/include/sched.h b/vendor/libc/include/sched.h new file mode 100644 index 000000000..6e4397536 --- /dev/null +++ b/vendor/libc/include/sched.h @@ -0,0 +1,23 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#pragma once + +#include + +#define CLOCK_MONOTONIC 1 + +struct timespec +{ + int64_t tv_sec; + int64_t tv_nsec; +}; + +int clock_gettime(int clockid, struct timespec *tp); + +int sched_yield(); + +#ifdef __cplusplus +} +#endif diff --git a/vendor/libc/sched.odin b/vendor/libc/sched.odin new file mode 100644 index 000000000..85fad3c05 --- /dev/null +++ b/vendor/libc/sched.odin @@ -0,0 +1,35 @@ +package odin_libc + +import "core:time" +import "core:thread" + +Clock :: enum i32 { + Monotonic = 1, +} + +Time_Spec :: struct { + tv_sec: i64, + tv_nsec: i64, +} + +@(require, linkage="strong", link_name="clock_gettime") +clock_gettine :: proc "c" (clockid: Clock, tp: ^Time_Spec) -> i32 { + switch clockid { + case .Monotonic: + tick := time.tick_now() + tp.tv_sec = tick._nsec/1e9 + tp.tv_nsec = tick._nsec%1e9/1000 + return 0 + + case: return -1 + } +} + +@(require, linkage="strong", link_name="sched_yield") +sched_yield :: proc "c" () -> i32 { + when thread.IS_SUPPORTED { + context = g_ctx + thread.yield() + } + return 0 +} diff --git a/vendor/miniaudio/common.odin b/vendor/miniaudio/common.odin index d72c3f251..0263278bc 100644 --- a/vendor/miniaudio/common.odin +++ b/vendor/miniaudio/common.odin @@ -20,9 +20,9 @@ foreign import lib { LIB } BINDINGS_VERSION_MAJOR :: 0 BINDINGS_VERSION_MINOR :: 11 -BINDINGS_VERSION_REVISION :: 21 +BINDINGS_VERSION_REVISION :: 22 BINDINGS_VERSION :: [3]u32{BINDINGS_VERSION_MAJOR, BINDINGS_VERSION_MINOR, BINDINGS_VERSION_REVISION} -BINDINGS_VERSION_STRING :: "0.11.21" +BINDINGS_VERSION_STRING :: "0.11.22" @(init) version_check :: proc() { diff --git a/vendor/miniaudio/data_conversion.odin b/vendor/miniaudio/data_conversion.odin index c33f54707..d95607dd9 100644 --- a/vendor/miniaudio/data_conversion.odin +++ b/vendor/miniaudio/data_conversion.odin @@ -194,7 +194,7 @@ foreign lib { resampler_get_expected_output_frame_count :: proc(pResampler: ^resampler, inputFrameCount: u64, pOutputFrameCount: ^u64) -> result --- /* - Resets the resampler's timer and clears it's internal cache. + Resets the resampler's timer and clears its internal cache. */ resampler_reset :: proc(pResampler: ^resampler) -> result --- } @@ -421,7 +421,7 @@ foreign lib { /* Copies a channel map. - Both input and output channel map buffers must have a capacity of at at least `channels`. + Both input and output channel map buffers must have a capacity of at least `channels`. */ channel_map_copy :: proc(pOut: [^]channel, pIn: [^]channel, channels: u32) --- diff --git a/vendor/miniaudio/decoding.odin b/vendor/miniaudio/decoding.odin index f1fa279ac..e2f33b8e7 100644 --- a/vendor/miniaudio/decoding.odin +++ b/vendor/miniaudio/decoding.odin @@ -71,7 +71,7 @@ decoder :: struct { pInputCache: rawptr, /* In input format. Can be null if it's not needed. */ inputCacheCap: u64, /* The capacity of the input cache. */ inputCacheConsumed: u64, /* The number of frames that have been consumed in the cache. Used for determining the next valid frame. */ - inputCacheRemaining: u64, /* The number of valid frames remaining in the cahce. */ + inputCacheRemaining: u64, /* The number of valid frames remaining in the cache. */ allocationCallbacks: allocation_callbacks, data: struct #raw_union { vfs: struct { @@ -111,7 +111,7 @@ foreign lib { decoder_read_pcm_frames :: proc(pDecoder: ^decoder, pFramesOut: rawptr, frameCount: u64, pFramesRead: ^u64) -> result --- /* - Seeks to a PCM frame based on it's absolute index. + Seeks to a PCM frame based on its absolute index. This is not thread safe without your own synchronization. */ diff --git a/vendor/miniaudio/device_io_procs.odin b/vendor/miniaudio/device_io_procs.odin index 21ac1afd7..a14de807c 100644 --- a/vendor/miniaudio/device_io_procs.odin +++ b/vendor/miniaudio/device_io_procs.odin @@ -12,6 +12,8 @@ foreign lib { device_job_thread_uninit :: proc(pJobThread: ^device_job_thread, pAllocationCallbacks: ^allocation_callbacks) --- device_job_thread_post :: proc(pJobThread: ^device_job_thread, pJob: ^job) -> result --- device_job_thread_next :: proc(pJobThread: ^device_job_thread, pJob: ^job) -> result --- + + device_id_equal :: proc(pA: ^device_id, pB: ^device_id) -> b32 --- /* Initializes a `ma_context_config` object. @@ -370,6 +372,9 @@ foreign lib { This function will allocate memory internally for the device lists and return a pointer to them through the `ppPlaybackDeviceInfos` and `ppCaptureDeviceInfos` parameters. If you do not want to incur the overhead of these allocations consider using `ma_context_enumerate_devices()` which will instead use a callback. + Note that this only retrieves the ID and name/description of the device. The reason for only retrieving basic information is that it would otherwise require + opening the backend device in order to probe it for more detailed information which can be inefficient. Consider using `ma_context_get_device_info()` for this, + but don't call it from within the enumeration callback. Parameters ---------- @@ -411,7 +416,7 @@ foreign lib { See Also -------- - ma_context_get_devices() + ma_context_enumerate_devices() */ context_get_devices :: proc(pContext: ^context_type, ppPlaybackDeviceInfos: ^[^]device_info, pPlaybackDeviceCount: ^u32, ppCaptureDeviceInfos: ^[^]device_info, pCaptureDeviceCount: ^u32) -> result --- @@ -550,7 +555,7 @@ foreign lib { playback, capture, full-duplex or loopback. (Note that loopback mode is only supported on select backends.) Sending and receiving audio data to and from the device is done via a callback which is fired by miniaudio at periodic time intervals. - The frequency at which data is delivered to and from a device depends on the size of it's period. The size of the period can be defined in terms of PCM frames + The frequency at which data is delivered to and from a device depends on the size of its period. The size of the period can be defined in terms of PCM frames or milliseconds, whichever is more convenient. Generally speaking, the smaller the period, the lower the latency at the expense of higher CPU usage and increased risk of glitching due to the more frequent and granular data deliver intervals. The size of a period will depend on your requirements, but miniaudio's defaults should work fine for most scenarios. If you're building a game you should leave this fairly small, whereas if you're building a simple @@ -624,7 +629,7 @@ foreign lib { performanceProfile A hint to miniaudio as to the performance requirements of your program. Can be either `ma_performance_profile_low_latency` (default) or - `ma_performance_profile_conservative`. This mainly affects the size of default buffers and can usually be left at it's default value. + `ma_performance_profile_conservative`. This mainly affects the size of default buffers and can usually be left at its default value. noPreSilencedOutputBuffer When set to true, the contents of the output buffer passed into the data callback will be left undefined. When set to false (default), the contents of @@ -664,7 +669,7 @@ foreign lib { A pointer that will passed to callbacks in pBackendVTable. resampling.linear.lpfOrder - The linear resampler applies a low-pass filter as part of it's processing for anti-aliasing. This setting controls the order of the filter. The higher + The linear resampler applies a low-pass filter as part of its processing for anti-aliasing. This setting controls the order of the filter. The higher the value, the better the quality, in general. Setting this to 0 will disable low-pass filtering altogether. The maximum value is `MA_MAX_FILTER_ORDER`. The default value is `min(4, MA_MAX_FILTER_ORDER)`. @@ -741,6 +746,9 @@ foreign lib { pulse.pStreamNameCapture PulseAudio only. Sets the stream name for capture. + pulse.channelMap + PulseAudio only. Sets the channel map that is requested from PulseAudio. See MA_PA_CHANNEL_MAP_* constants. Defaults to MA_PA_CHANNEL_MAP_AIFF. + coreaudio.allowNominalSampleRateChange Core Audio only. Desktop only. When enabled, allows the sample rate of the device to be changed at the operating system level. This is disabled by default in order to prevent intrusive changes to the user's system. This is useful if you want to use a sample rate @@ -914,7 +922,7 @@ foreign lib { Remarks ------- - You only need to use this function if you want to configure the context differently to it's defaults. You should never use this function if you want to manage + You only need to use this function if you want to configure the context differently to its defaults. You should never use this function if you want to manage your own context. See the documentation for `ma_context_init()` for information on the different context configuration options. diff --git a/vendor/miniaudio/device_io_types.odin b/vendor/miniaudio/device_io_types.odin index b52a3f423..9d64602f8 100644 --- a/vendor/miniaudio/device_io_types.odin +++ b/vendor/miniaudio/device_io_types.odin @@ -427,6 +427,7 @@ device_config :: struct { pulse: struct { pStreamNamePlayback: cstring, pStreamNameCapture: cstring, + channelMap: i32, }, coreaudio: struct { allowNominalSampleRateChange: b32, /* Desktop only. When enabled, allows changing of the sample rate at the operating system level. */ @@ -443,6 +444,7 @@ device_config :: struct { allowedCapturePolicy: aaudio_allowed_capture_policy, noAutoStartAfterReroute: b32, enableCompatibilityWorkarounds: b32, + allowSetBufferCapacity: b32, }, } @@ -514,7 +516,7 @@ and on output returns detailed information about the device in `ma_device_info`. case when the device ID is NULL, in which case information about the default device needs to be retrieved. Once the context has been created and the device ID retrieved (if using anything other than the default device), the device can be created. -This is a little bit more complicated than initialization of the context due to it's more complicated configuration. When initializing a +This is a little bit more complicated than initialization of the context due to its more complicated configuration. When initializing a device, a duplex device may be requested. This means a separate data format needs to be specified for both playback and capture. On input, the data format is set to what the application wants. On output it's set to the native format which should match as closely as possible to the requested format. The conversion between the format requested by the application and the device's native format will be handled @@ -535,10 +537,10 @@ asynchronous reading and writing, `onDeviceStart()` and `onDeviceStop()` should The handling of data delivery between the application and the device is the most complicated part of the process. To make this a bit easier, some helper callbacks are available. If the backend uses a blocking read/write style of API, the `onDeviceRead()` and `onDeviceWrite()` callbacks can optionally be implemented. These are blocking and work just like reading and writing from a file. If the -backend uses a callback for data delivery, that callback must call `ma_device_handle_backend_data_callback()` from within it's callback. +backend uses a callback for data delivery, that callback must call `ma_device_handle_backend_data_callback()` from within its callback. This allows miniaudio to then process any necessary data conversion and then pass it to the miniaudio data callback. -If the backend requires absolute flexibility with it's data delivery, it can optionally implement the `onDeviceDataLoop()` callback +If the backend requires absolute flexibility with its data delivery, it can optionally implement the `onDeviceDataLoop()` callback which will allow it to implement the logic that will run on the audio thread. This is much more advanced and is completely optional. The audio thread should run data delivery logic in a loop while `ma_device_get_state() == ma_device_state_started` and no errors have been @@ -575,6 +577,9 @@ context_config :: struct { threadStackSize: c.size_t, pUserData: rawptr, allocationCallbacks: allocation_callbacks, + dsound: struct { + hWnd: handle, /* HWND. Optional window handle to pass into SetCooperativeLevel(). Will default to the foreground window, and if that fails, the desktop window. */ + }, alsa: struct { useVerboseDeviceEnumeration: b32, }, @@ -649,6 +654,7 @@ context_type :: struct { } when SUPPORT_WASAPI else struct {}), dsound: (struct { + hWnd: handle, /* Can be null. */ hDSoundDLL: handle, DirectSoundCreate: proc "system" (), DirectSoundEnumerateA: proc "system" (), @@ -1195,6 +1201,7 @@ device :: struct { aaudio: (struct { /*AAudioStream**/ pStreamPlayback: rawptr, /*AAudioStream**/ pStreamCapture: rawptr, + rerouteLock: mutex, usage: aaudio_usage, contentType: aaudio_content_type, inputPreset: aaudio_input_preset, diff --git a/vendor/miniaudio/doc.odin b/vendor/miniaudio/doc.odin index 33c613ae4..ff7924b89 100644 --- a/vendor/miniaudio/doc.odin +++ b/vendor/miniaudio/doc.odin @@ -295,7 +295,7 @@ avoids the same sound being loaded multiple times. The node graph is used for mixing and effect processing. The idea is that you connect a number of nodes into the graph by connecting each node's outputs to another node's inputs. Each node can -implement it's own effect. By chaining nodes together, advanced mixing and effect processing can +implement its own effect. By chaining nodes together, advanced mixing and effect processing can be achieved. The engine encapsulates both the resource manager and the node graph to create a simple, easy to @@ -400,7 +400,7 @@ the be started and/or stopped at a specific time. This can be done with the foll ``` The start/stop time needs to be specified based on the absolute timer which is controlled by the -engine. The current global time time in PCM frames can be retrieved with +engine. The current global time in PCM frames can be retrieved with `ma_engine_get_time_in_pcm_frames()`. The engine's global time can be changed with `ma_engine_set_time_in_pcm_frames()` for synchronization purposes if required. Note that scheduling a start time still requires an explicit call to `ma_sound_start()` before anything will play: @@ -432,11 +432,11 @@ Sounds and sound groups are nodes in the engine's node graph and can be plugged API. This makes it possible to connect sounds and sound groups to effect nodes to produce complex effect chains. -A sound can have it's volume changed with `ma_sound_set_volume()`. If you prefer decibel volume +A sound can have its volume changed with `ma_sound_set_volume()`. If you prefer decibel volume control you can use `ma_volume_db_to_linear()` to convert from decibel representation to linear. Panning and pitching is supported with `ma_sound_set_pan()` and `ma_sound_set_pitch()`. If you know -a sound will never have it's pitch changed with `ma_sound_set_pitch()` or via the doppler effect, +a sound will never have its pitch changed with `ma_sound_set_pitch()` or via the doppler effect, you can specify the `MA_SOUND_FLAG_NO_PITCH` flag when initializing the sound for an optimization. By default, sounds and sound groups have spatialization enabled. If you don't ever want to @@ -485,21 +485,12 @@ link the relevant frameworks but should compile cleanly out of the box with Xcod through the command line requires linking to `-lpthread` and `-lm`. Due to the way miniaudio links to frameworks at runtime, your application may not pass Apple's -notarization process. To fix this there are two options. The first is to use the -`MA_NO_RUNTIME_LINKING` option, like so: - - ```c - #ifdef __APPLE__ - #define MA_NO_RUNTIME_LINKING - #endif - #define MINIAUDIO_IMPLEMENTATION - #include "miniaudio.h" - ``` - -This will require linking with `-framework CoreFoundation -framework CoreAudio -framework AudioToolbox`. -If you get errors about AudioToolbox, try with `-framework AudioUnit` instead. You may get this when -using older versions of iOS. Alternatively, if you would rather keep using runtime linking you can -add the following to your entitlements.xcent file: +notarization process. To fix this there are two options. The first is to compile with +`-DMA_NO_RUNTIME_LINKING` which in turn will require linking with +`-framework CoreFoundation -framework CoreAudio -framework AudioToolbox`. If you get errors about +AudioToolbox, try with `-framework AudioUnit` instead. You may get this when using older versions +of iOS. Alternatively, if you would rather keep using runtime linking you can add the following to +your entitlements.xcent file: ``` com.apple.security.cs.allow-dyld-environment-variables @@ -557,7 +548,7 @@ To run locally, you'll need to use emrun: 2.7. Build Options ------------------ -`#define` these options before including miniaudio.h. +`#define` these options before including miniaudio.c, or pass them as compiler flags: +----------------------------------+--------------------------------------------------------------------+ | Option | Description | @@ -588,6 +579,8 @@ To run locally, you'll need to use emrun: +----------------------------------+--------------------------------------------------------------------+ | MA_NO_WEBAUDIO | Disables the Web Audio backend. | +----------------------------------+--------------------------------------------------------------------+ + | MA_NO_CUSTOM | Disables support for custom backends. | + +----------------------------------+--------------------------------------------------------------------+ | MA_NO_NULL | Disables the null backend. | +----------------------------------+--------------------------------------------------------------------+ | MA_ENABLE_ONLY_SPECIFIC_BACKENDS | Disables all backends by default and requires `MA_ENABLE_*` to | @@ -632,6 +625,9 @@ To run locally, you'll need to use emrun: | MA_ENABLE_WEBAUDIO | Used in conjunction with MA_ENABLE_ONLY_SPECIFIC_BACKENDS to | | | enable the Web Audio backend. | +----------------------------------+--------------------------------------------------------------------+ + | MA_ENABLE_CUSTOM | Used in conjunction with MA_ENABLE_ONLY_SPECIFIC_BACKENDS to | + | | enable custom backends. | + +----------------------------------+--------------------------------------------------------------------+ | MA_ENABLE_NULL | Used in conjunction with MA_ENABLE_ONLY_SPECIFIC_BACKENDS to | | | enable the null backend. | +----------------------------------+--------------------------------------------------------------------+ @@ -695,11 +691,30 @@ To run locally, you'll need to use emrun: | | You may need to enable this if your target platform does not allow | | | runtime linking via `dlopen()`. | +----------------------------------+--------------------------------------------------------------------+ + | MA_USE_STDINT | (Pass this in a compiler flag. Do not #define this before | + | | miniaudio.c) Forces the use of stdint.h for sized types. | + +----------------------------------+--------------------------------------------------------------------+ | MA_DEBUG_OUTPUT | Enable `printf()` output of debug logs (`MA_LOG_LEVEL_DEBUG`). | +----------------------------------+--------------------------------------------------------------------+ | MA_COINIT_VALUE | Windows only. The value to pass to internal calls to | | | `CoInitializeEx()`. Defaults to `COINIT_MULTITHREADED`. | +----------------------------------+--------------------------------------------------------------------+ + | MA_FORCE_UWP | Windows only. Affects only the WASAPI backend. Will force the | + | | WASAPI backend to use the UWP code path instead of the regular | + | | desktop path. This is normally auto-detected and should rarely be | + | | needed to be used explicitly, but can be useful for debugging. | + +----------------------------------+--------------------------------------------------------------------+ + | MA_ON_THREAD_ENTRY | Defines some code that will be executed as soon as an internal | + | | miniaudio-managed thread is created. This will be the first thing | + | | to be executed by the thread entry point. | + +----------------------------------+--------------------------------------------------------------------+ + | MA_ON_THREAD_EXIT | Defines some code that will be executed from the entry point of an | + | | internal miniaudio-managed thread upon exit. This will be the last | + | | thing to be executed before the thread's entry point exits. | + +----------------------------------+--------------------------------------------------------------------+ + | MA_THREAD_DEFAULT_STACK_SIZE | If set, specifies the default stack size used by miniaudio-managed | + | | threads. | + +----------------------------------+--------------------------------------------------------------------+ | MA_API | Controls how public APIs should be decorated. Default is `extern`. | +----------------------------------+--------------------------------------------------------------------+ @@ -1311,7 +1326,7 @@ only works for sounds that were initialized with `ma_sound_init_from_file()` and When you initialize a sound, if you specify a sound group the sound will be attached to that group automatically. If you set it to NULL, it will be automatically attached to the engine's endpoint. -If you would instead rather leave the sound unattached by default, you can can specify the +If you would instead rather leave the sound unattached by default, you can specify the `MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT` flag. This is useful if you want to set up a complex node graph. @@ -1688,6 +1703,7 @@ combination of the following flags: MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING ``` When no flags are specified (set to 0), the sound will be fully loaded into memory, but not @@ -1708,6 +1724,14 @@ can instead stream audio data which you can do by specifying the second pages. When a new page needs to be decoded, a job will be posted to the job queue and then subsequently processed in a job thread. +The `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flag can be used so that the sound will loop +when it reaches the end by default. It's recommended you use this flag when you want to have a +looping streaming sound. If you try loading a very short sound as a stream, you will get a glitch. +This is because the resource manager needs to pre-fill the initial buffer at initialization time, +and if you don't specify the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flag, the resource +manager will assume the sound is not looping and will stop filling the buffer when it reaches the +end, therefore resulting in a discontinuous buffer. + For in-memory sounds, reference counting is used to ensure the data is loaded only once. This means multiple calls to `ma_resource_manager_data_source_init()` with the same file path will result in the file data only being loaded once. Each call to `ma_resource_manager_data_source_init()` must be @@ -1722,7 +1746,7 @@ actual file paths. When `ma_resource_manager_data_source_init()` is called (with `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM` flag), the resource manager will look for these explicitly registered data buffers and, if found, will use it as the backing data for the data source. Note that the resource manager does *not* make a copy of this data so it is up to the -caller to ensure the pointer stays valid for it's lifetime. Use +caller to ensure the pointer stays valid for its lifetime. Use `ma_resource_manager_unregister_data()` to unregister the self-managed data. You can also use `ma_resource_manager_register_file()` and `ma_resource_manager_unregister_file()` to register and unregister a file. It does not make sense to use the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM` @@ -2033,7 +2057,7 @@ In the above graph, it starts with two data sources whose outputs are attached t splitter node. It's at this point that the two data sources are mixed. After mixing, the splitter performs it's processing routine and produces two outputs which is simply a duplication of the input stream. One output is attached to a low pass filter, whereas the other output is attached to -a echo/delay. The outputs of the the low pass filter and the echo are attached to the endpoint, and +a echo/delay. The outputs of the low pass filter and the echo are attached to the endpoint, and since they're both connected to the same input bus, they'll be mixed. Each input bus must be configured to accept the same number of channels, but the number of channels @@ -2074,7 +2098,7 @@ data from the graph: ``` When you read audio data, miniaudio starts at the node graph's endpoint node which then pulls in -data from it's input attachments, which in turn recursively pull in data from their inputs, and so +data from its input attachments, which in turn recursively pull in data from their inputs, and so on. At the start of the graph there will be some kind of data source node which will have zero inputs and will instead read directly from a data source. The base nodes don't literally need to read from a `ma_data_source` object, but they will always have some kind of underlying object that @@ -2320,7 +2344,7 @@ You can start and stop a node with the following: By default the node is in a started state, but since it won't be connected to anything won't actually be invoked by the node graph until it's connected. When you stop a node, data will not be -read from any of it's input connections. You can use this property to stop a group of sounds +read from any of its input connections. You can use this property to stop a group of sounds atomically. You can configure the initial state of a node in it's config: @@ -2413,29 +2437,29 @@ audio thread is finished so that control is not handed back to the caller thereb chance to free the node's memory. When the audio thread is processing a node, it does so by reading from each of the output buses of -the node. In order for a node to process data for one of it's output buses, it needs to read from -each of it's input buses, and so on an so forth. It follows that once all output buses of a node +the node. In order for a node to process data for one of its output buses, it needs to read from +each of its input buses, and so on an so forth. It follows that once all output buses of a node are detached, the node as a whole will be disconnected and no further processing will occur unless it's output buses are reattached, which won't be happening when the node is being uninitialized. By having `ma_node_detach_output_bus()` wait until the audio thread is finished with it, we can simplify a few things, at the expense of making `ma_node_detach_output_bus()` a bit slower. By doing this, the implementation of `ma_node_uninit()` becomes trivial - just detach all output -nodes, followed by each of the attachments to each of it's input nodes, and then do any final clean +nodes, followed by each of the attachments to each of its input nodes, and then do any final clean up. With the above design, the worst-case scenario is `ma_node_detach_output_bus()` taking as long as it takes to process the output bus being detached. This will happen if it's called at just the wrong moment where the audio thread has just iterated it and has just started processing. The caller of `ma_node_detach_output_bus()` will stall until the audio thread is finished, which -includes the cost of recursively processing it's inputs. This is the biggest compromise made with -the approach taken by miniaudio for it's lock-free processing system. The cost of detaching nodes +includes the cost of recursively processing its inputs. This is the biggest compromise made with +the approach taken by miniaudio for its lock-free processing system. The cost of detaching nodes earlier in the pipeline (data sources, for example) will be cheaper than the cost of detaching higher level nodes, such as some kind of final post-processing endpoint. If you need to do mass detachments, detach starting from the lowest level nodes and work your way towards the final endpoint node (but don't try detaching the node graph's endpoint). If the audio thread is not running, detachment will be fast and detachment in any order will be the same. The reason nodes need to wait for their input attachments to complete is due to the potential for desyncs between -data sources. If the node was to terminate processing mid way through processing it's inputs, +data sources. If the node was to terminate processing mid way through processing its inputs, there's a chance that some of the underlying data sources will have been read, but then others not. That will then result in a potential desynchronization when detaching and reattaching higher-level nodes. A possible solution to this is to have an option when detaching to terminate processing @@ -2806,7 +2830,7 @@ weights. Custom weights can be passed in as the last parameter of `ma_channel_converter_config_init()`. Predefined channel maps can be retrieved with `ma_channel_map_init_standard()`. This takes a -`ma_standard_channel_map` enum as it's first parameter, which can be one of the following: +`ma_standard_channel_map` enum as its first parameter, which can be one of the following: +-----------------------------------+-----------------------------------------------------------+ | Name | Description | @@ -2892,7 +2916,7 @@ like the following: ma_resample_algorithm_linear); ma_resampler resampler; - ma_result result = ma_resampler_init(&config, &resampler); + ma_result result = ma_resampler_init(&config, NULL, &resampler); if (result != MA_SUCCESS) { // An error occurred... } @@ -3134,7 +3158,7 @@ Biquad filtering is achieved with the `ma_biquad` API. Example: ```c ma_biquad_config config = ma_biquad_config_init(ma_format_f32, channels, b0, b1, b2, a0, a1, a2); - ma_result result = ma_biquad_init(&config, &biquad); + ma_result result = ma_biquad_init(&config, NULL, &biquad); if (result != MA_SUCCESS) { // Error. } diff --git a/vendor/miniaudio/engine.odin b/vendor/miniaudio/engine.odin index a06e6c62c..e5364d782 100644 --- a/vendor/miniaudio/engine.odin +++ b/vendor/miniaudio/engine.odin @@ -18,7 +18,8 @@ sound_flag :: enum c.int { ASYNC = 2, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC */ WAIT_INIT = 3, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT */ UNKNOWN_LENGTH = 4, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH */ - + LOOPING = 5, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING */ + /* ma_sound specific flags. */ NO_DEFAULT_ATTACHMENT = 12, /* Do not attach to the endpoint by default. Useful for when setting up nodes in a complex graph system. */ NO_PITCH = 13, /* Disable pitch shifting with ma_sound_set_pitch() and ma_sound_group_set_pitch(). This is an optimization. */ @@ -51,7 +52,7 @@ engine_node_config :: struct { /* Base node object for both ma_sound and ma_sound_group. */ engine_node :: struct { - baseNode: node_base, /* Must be the first member for compatiblity with the ma_node API. */ + baseNode: node_base, /* Must be the first member for compatibility with the ma_node API. */ pEngine: ^engine, /* A pointer to the engine. Set based on the value from the config. */ sampleRate: u32, /* The sample rate of the input data. For sounds backed by a data source, this will be the data source's sample rate. Otherwise it'll be the engine's sample rate. */ volumeSmoothTimeInPCMFrames: u32, @@ -113,7 +114,6 @@ sound_config :: struct { rangeEndInPCMFrames: u64, loopPointBegInPCMFrames: u64, loopPointEndInPCMFrames: u64, - isLooping: b32, endCallback: sound_end_proc, /* Fired when the sound reaches the end. Will be fired from the audio thread. Do not restart, uninitialize or otherwise change the state of the sound from here. Instead fire an event or set a variable to indicate to a different thread to change the start of the sound. Will not be fired in response to a scheduled stop with ma_sound_set_stop_time_*(). */ pEndCallbackUserData: rawptr, @@ -121,6 +121,8 @@ sound_config :: struct { initNotifications: resource_manager_pipeline_notifications, pDoneFence: ^fence, /* Deprecated. Use initNotifications instead. Released when the resource manager has finished decoding the entire sound. Not used with streams. */ + + isLooping: b32, /* Deprecated. Use the MA_SOUND_FLAG_LOOPING in `flags` instead. */ } sound :: struct { @@ -226,6 +228,7 @@ foreign lib { sound_is_looping :: proc(pSound: ^sound) -> b32 --- sound_at_end :: proc(pSound: ^sound) -> b32 --- sound_seek_to_pcm_frame :: proc(pSound: ^sound, frameIndex: u64) -> result --- /* Just a wrapper around ma_data_source_seek_to_pcm_frame(). */ + sound_seek_to_second :: proc(pSound: ^sound, seekPointInSeconds: f32) -> result --- /* Abstraction to ma_sound_seek_to_pcm_frame() */ sound_get_data_format :: proc(pSound: ^sound, pFormat: ^format, pChannels, pSampleRate: ^u32, pChannelMap: ^channel, channelMapCap: c.size_t) -> result --- sound_get_cursor_in_pcm_frames :: proc(pSound: ^sound, pCursor: ^u64) -> result --- sound_get_length_in_pcm_frames :: proc(pSound: ^sound, pLength: ^u64) -> result --- @@ -323,6 +326,7 @@ engine_config :: struct { gainSmoothTimeInMilliseconds: u32, /* When set to 0, gainSmoothTimeInFrames will be used. If both are set to 0, a default value will be used. */ defaultVolumeSmoothTimeInPCMFrames: u32, /* Defaults to 0. Controls the default amount of smoothing to apply to volume changes to sounds. High values means more smoothing at the expense of high latency (will take longer to reach the new volume). */ + preMixStackSizeInBytes: u32, /* A stack is used for internal processing in the node graph. This allows you to configure the size of this stack. Smaller values will reduce the maximum depth of your node graph. You should rarely need to modify this. */ allocationCallbacks: allocation_callbacks, noAutoStart: b32, /* When set to true, requires an explicit call to ma_engine_start(). This is false by default, meaning the engine will be started automatically in ma_engine_init(). */ @@ -344,7 +348,7 @@ engine :: struct { allocationCallbacks: allocation_callbacks, ownsResourceManager: b8, ownsDevice: b8, - inlinedSoundLock: spinlock, /* For synchronizing access so the inlined sound list. */ + inlinedSoundLock: spinlock, /* For synchronizing access to the inlined sound list. */ pInlinedSoundHead: ^sound_inlined, /* The first inlined sound. Inlined sounds are tracked in a linked list. */ inlinedSoundCount: u32, /*atomic*/ /* The total number of allocated inlined sound objects. Used for debugging. */ gainSmoothTimeInFrames: u32, /* The number of frames to interpolate the gain of spatialized sounds across. */ diff --git a/vendor/miniaudio/lib/miniaudio.lib b/vendor/miniaudio/lib/miniaudio.lib index d339c746e..c9ee60979 100644 Binary files a/vendor/miniaudio/lib/miniaudio.lib and b/vendor/miniaudio/lib/miniaudio.lib differ diff --git a/vendor/miniaudio/node_graph.odin b/vendor/miniaudio/node_graph.odin index 610ada7a8..496d566f4 100644 --- a/vendor/miniaudio/node_graph.odin +++ b/vendor/miniaudio/node_graph.odin @@ -19,6 +19,13 @@ MAX_NODE_LOCAL_BUS_COUNT :: 2 /* Use this when the bus count is determined by the node instance rather than the vtable. */ NODE_BUS_COUNT_UNKNOWN :: 255 +/* For some internal memory management of ma_node_graph. */ +stack :: struct { + offset: uint, + sizeInBytes: uint, + _data: [1]byte, +} + node :: struct {} /* Node flags. */ @@ -53,7 +60,7 @@ node_vtable :: struct { onProcess: proc "c" (pNode: ^node, ppFramesIn: ^[^]f32, pFrameCountIn: ^u32, ppFramesOut: ^[^]f32, pFrameCountOut: ^u32), /* - A callback for retrieving the number of a input frames that are required to output the + A callback for retrieving the number of input frames that are required to output the specified number of output frames. You would only want to implement this when the node performs resampling. This is optional, even for nodes that perform resampling, but it does offer a small reduction in latency as it allows miniaudio to calculate the exact number of input frames @@ -134,8 +141,12 @@ node_input_bus :: struct { node_base :: struct { /* These variables are set once at startup. */ - pNodeGraph: ^node_graph, /* The graph this node belongs to. */ + pNodeGraph: ^node_graph, /* The graph this node belongs to. */ vtable: ^node_vtable, + inputBusCount: u32, + outputBusCount: u32, + pInputBuses: [^]node_input_bus `fmt:"v,inputBusCount"`, + pOutputBuses: [^]node_output_bus `fmt:"v,outputBusCount"`, pCachedData: [^]f32, /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ cachedDataCapInFramesPerBus: u16, /* The capacity of the input data cache in frames, per bus. */ @@ -148,10 +159,6 @@ node_base :: struct { state: node_state, /*atomic*/ /* When set to stopped, nothing will be read, regardless of the times in stateTimes. */ stateTimes: [2]u64, /*atomic*/ /* Indexed by ma_node_state. Specifies the time based on the global clock that a node should be considered to be in the relevant state. */ localTime: u64, /*atomic*/ /* The node's local clock. This is just a running sum of the number of output frames that have been processed. Can be modified by any thread with `ma_node_set_time()`. */ - inputBusCount: u32, - outputBusCount: u32, - pInputBuses: [^]node_input_bus, - pOutputBuses: [^]node_output_bus, /* Memory management. */ _inputBuses: [MAX_NODE_LOCAL_BUS_COUNT]node_input_bus, @@ -189,18 +196,25 @@ foreign lib { } node_graph_config :: struct { - channels: u32, - nodeCacheCapInFrames: u16, + channels: u32, + processingSizeInFrames: u32, /* This is the preferred processing size for node processing callbacks unless overridden by a node itself. Can be 0 in which case it will be based on the frame count passed into ma_node_graph_read_pcm_frames(), but will not be well defined. */ + preMixStackSizeInBytes: uint, /* Defaults to 512KB per channel. Reducing this will save memory, but the depth of your node graph will be more restricted. */ } node_graph :: struct { /* Immutable. */ base: node_base, /* The node graph itself is a node so it can be connected as an input to different node graph. This has zero inputs and calls ma_node_graph_read_pcm_frames() to generate it's output. */ endpoint: node_base, /* Special node that all nodes eventually connect to. Data is read from this node in ma_node_graph_read_pcm_frames(). */ - nodeCacheCapInFrames: u16, + + pProcessingCache: [^]f32, /* This will be allocated when processingSizeInFrames is non-zero. This is needed because ma_node_graph_read_pcm_frames() can be called with a variable number of frames, and we may need to do some buffering in situations where the caller requests a frame count that's not a multiple of processingSizeInFrames. */ + processingCacheFramesRemaining: u32, + processingSizeInFrames: u32, /* Read and written by multiple threads. */ isReading: b32, /*atomic*/ + + /* Modified only by the audio thread. */ + pPreMixStack: ^stack, } @(default_calling_convention="c", link_prefix="ma_") diff --git a/vendor/miniaudio/resource_manager.odin b/vendor/miniaudio/resource_manager.odin index 495a02c5d..4ef5778a9 100644 --- a/vendor/miniaudio/resource_manager.odin +++ b/vendor/miniaudio/resource_manager.odin @@ -16,6 +16,7 @@ resource_manager_data_source_flag :: enum c.int { ASYNC = 2, /* When set, the resource manager will load the data source asynchronously. */ WAIT_INIT = 3, /* When set, waits for initialization of the underlying data source before returning from ma_resource_manager_data_source_init(). */ UNKNOWN_LENGTH = 4, /* Gives the resource manager a hint that the length of the data source is unknown and calling `ma_data_source_get_length_in_pcm_frames()` should be avoided. */ + LOOPING = 5, /* When set, configures the data source to loop by default. */ } resource_manager_data_source_flags :: bit_set[resource_manager_data_source_flag; u32] @@ -79,8 +80,8 @@ resource_manager_data_source_config :: struct { rangeEndInPCMFrames: u64, loopPointBegInPCMFrames: u64, loopPointEndInPCMFrames: u64, - isLooping: b32, flags: u32, + isLooping: b32, /* Deprecated. Use the MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING flag in `flags` instead. */ } resource_manager_data_supply_type :: enum c.int { diff --git a/vendor/miniaudio/src/miniaudio.h b/vendor/miniaudio/src/miniaudio.h index 47332e11a..c74bebeb3 100644 --- a/vendor/miniaudio/src/miniaudio.h +++ b/vendor/miniaudio/src/miniaudio.h @@ -1,6 +1,6 @@ /* Audio playback and capture library. Choice of public domain or MIT-0. See license statements at the end of this file. -miniaudio - v0.11.21 - 2023-11-15 +miniaudio - v0.11.22 - 2025-02-24 David Reid - mackron@gmail.com @@ -12,15 +12,18 @@ GitHub: https://github.com/mackron/miniaudio /* 1. Introduction =============== -miniaudio is a single file library for audio playback and capture. To use it, do the following in -one .c file: +To use miniaudio, include "miniaudio.h": ```c - #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" ``` -You can do `#include "miniaudio.h"` in other parts of the program just like any other header. +The implementation is contained in "miniaudio.c". Just compile this like any other source file. You +can include miniaudio.c if you want to compile your project as a single translation unit: + + ```c + #include "miniaudio.c" + ``` miniaudio includes both low level and high level APIs. The low level API is good for those who want to do all of their mixing themselves and only require a light weight interface to the underlying @@ -293,7 +296,7 @@ avoids the same sound being loaded multiple times. The node graph is used for mixing and effect processing. The idea is that you connect a number of nodes into the graph by connecting each node's outputs to another node's inputs. Each node can -implement it's own effect. By chaining nodes together, advanced mixing and effect processing can +implement its own effect. By chaining nodes together, advanced mixing and effect processing can be achieved. The engine encapsulates both the resource manager and the node graph to create a simple, easy to @@ -398,7 +401,7 @@ the be started and/or stopped at a specific time. This can be done with the foll ``` The start/stop time needs to be specified based on the absolute timer which is controlled by the -engine. The current global time time in PCM frames can be retrieved with +engine. The current global time in PCM frames can be retrieved with `ma_engine_get_time_in_pcm_frames()`. The engine's global time can be changed with `ma_engine_set_time_in_pcm_frames()` for synchronization purposes if required. Note that scheduling a start time still requires an explicit call to `ma_sound_start()` before anything will play: @@ -430,11 +433,11 @@ Sounds and sound groups are nodes in the engine's node graph and can be plugged API. This makes it possible to connect sounds and sound groups to effect nodes to produce complex effect chains. -A sound can have it's volume changed with `ma_sound_set_volume()`. If you prefer decibel volume +A sound can have its volume changed with `ma_sound_set_volume()`. If you prefer decibel volume control you can use `ma_volume_db_to_linear()` to convert from decibel representation to linear. Panning and pitching is supported with `ma_sound_set_pan()` and `ma_sound_set_pitch()`. If you know -a sound will never have it's pitch changed with `ma_sound_set_pitch()` or via the doppler effect, +a sound will never have its pitch changed with `ma_sound_set_pitch()` or via the doppler effect, you can specify the `MA_SOUND_FLAG_NO_PITCH` flag when initializing the sound for an optimization. By default, sounds and sound groups have spatialization enabled. If you don't ever want to @@ -483,21 +486,12 @@ link the relevant frameworks but should compile cleanly out of the box with Xcod through the command line requires linking to `-lpthread` and `-lm`. Due to the way miniaudio links to frameworks at runtime, your application may not pass Apple's -notarization process. To fix this there are two options. The first is to use the -`MA_NO_RUNTIME_LINKING` option, like so: - - ```c - #ifdef __APPLE__ - #define MA_NO_RUNTIME_LINKING - #endif - #define MINIAUDIO_IMPLEMENTATION - #include "miniaudio.h" - ``` - -This will require linking with `-framework CoreFoundation -framework CoreAudio -framework AudioToolbox`. -If you get errors about AudioToolbox, try with `-framework AudioUnit` instead. You may get this when -using older versions of iOS. Alternatively, if you would rather keep using runtime linking you can -add the following to your entitlements.xcent file: +notarization process. To fix this there are two options. The first is to compile with +`-DMA_NO_RUNTIME_LINKING` which in turn will require linking with +`-framework CoreFoundation -framework CoreAudio -framework AudioToolbox`. If you get errors about +AudioToolbox, try with `-framework AudioUnit` instead. You may get this when using older versions +of iOS. Alternatively, if you would rather keep using runtime linking you can add the following to +your entitlements.xcent file: ``` com.apple.security.cs.allow-dyld-environment-variables @@ -555,7 +549,7 @@ To run locally, you'll need to use emrun: 2.7. Build Options ------------------ -`#define` these options before including miniaudio.h. +`#define` these options before including miniaudio.c, or pass them as compiler flags: +----------------------------------+--------------------------------------------------------------------+ | Option | Description | @@ -586,6 +580,8 @@ To run locally, you'll need to use emrun: +----------------------------------+--------------------------------------------------------------------+ | MA_NO_WEBAUDIO | Disables the Web Audio backend. | +----------------------------------+--------------------------------------------------------------------+ + | MA_NO_CUSTOM | Disables support for custom backends. | + +----------------------------------+--------------------------------------------------------------------+ | MA_NO_NULL | Disables the null backend. | +----------------------------------+--------------------------------------------------------------------+ | MA_ENABLE_ONLY_SPECIFIC_BACKENDS | Disables all backends by default and requires `MA_ENABLE_*` to | @@ -630,6 +626,9 @@ To run locally, you'll need to use emrun: | MA_ENABLE_WEBAUDIO | Used in conjunction with MA_ENABLE_ONLY_SPECIFIC_BACKENDS to | | | enable the Web Audio backend. | +----------------------------------+--------------------------------------------------------------------+ + | MA_ENABLE_CUSTOM | Used in conjunction with MA_ENABLE_ONLY_SPECIFIC_BACKENDS to | + | | enable custom backends. | + +----------------------------------+--------------------------------------------------------------------+ | MA_ENABLE_NULL | Used in conjunction with MA_ENABLE_ONLY_SPECIFIC_BACKENDS to | | | enable the null backend. | +----------------------------------+--------------------------------------------------------------------+ @@ -693,11 +692,30 @@ To run locally, you'll need to use emrun: | | You may need to enable this if your target platform does not allow | | | runtime linking via `dlopen()`. | +----------------------------------+--------------------------------------------------------------------+ + | MA_USE_STDINT | (Pass this in as a compiler flag. Do not `#define` this before | + | | miniaudio.c) Forces the use of stdint.h for sized types. | + +----------------------------------+--------------------------------------------------------------------+ | MA_DEBUG_OUTPUT | Enable `printf()` output of debug logs (`MA_LOG_LEVEL_DEBUG`). | +----------------------------------+--------------------------------------------------------------------+ | MA_COINIT_VALUE | Windows only. The value to pass to internal calls to | | | `CoInitializeEx()`. Defaults to `COINIT_MULTITHREADED`. | +----------------------------------+--------------------------------------------------------------------+ + | MA_FORCE_UWP | Windows only. Affects only the WASAPI backend. Will force the | + | | WASAPI backend to use the UWP code path instead of the regular | + | | desktop path. This is normally auto-detected and should rarely be | + | | needed to be used explicitly, but can be useful for debugging. | + +----------------------------------+--------------------------------------------------------------------+ + | MA_ON_THREAD_ENTRY | Defines some code that will be executed as soon as an internal | + | | miniaudio-managed thread is created. This will be the first thing | + | | to be executed by the thread entry point. | + +----------------------------------+--------------------------------------------------------------------+ + | MA_ON_THREAD_EXIT | Defines some code that will be executed from the entry point of an | + | | internal miniaudio-managed thread upon exit. This will be the last | + | | thing to be executed before the thread's entry point exits. | + +----------------------------------+--------------------------------------------------------------------+ + | MA_THREAD_DEFAULT_STACK_SIZE | If set, specifies the default stack size used by miniaudio-managed | + | | threads. | + +----------------------------------+--------------------------------------------------------------------+ | MA_API | Controls how public APIs should be decorated. Default is `extern`. | +----------------------------------+--------------------------------------------------------------------+ @@ -1309,7 +1327,7 @@ only works for sounds that were initialized with `ma_sound_init_from_file()` and When you initialize a sound, if you specify a sound group the sound will be attached to that group automatically. If you set it to NULL, it will be automatically attached to the engine's endpoint. -If you would instead rather leave the sound unattached by default, you can can specify the +If you would instead rather leave the sound unattached by default, you can specify the `MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT` flag. This is useful if you want to set up a complex node graph. @@ -1686,6 +1704,7 @@ combination of the following flags: MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING ``` When no flags are specified (set to 0), the sound will be fully loaded into memory, but not @@ -1706,6 +1725,14 @@ can instead stream audio data which you can do by specifying the second pages. When a new page needs to be decoded, a job will be posted to the job queue and then subsequently processed in a job thread. +The `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flag can be used so that the sound will loop +when it reaches the end by default. It's recommended you use this flag when you want to have a +looping streaming sound. If you try loading a very short sound as a stream, you will get a glitch. +This is because the resource manager needs to pre-fill the initial buffer at initialization time, +and if you don't specify the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flag, the resource +manager will assume the sound is not looping and will stop filling the buffer when it reaches the +end, therefore resulting in a discontinuous buffer. + For in-memory sounds, reference counting is used to ensure the data is loaded only once. This means multiple calls to `ma_resource_manager_data_source_init()` with the same file path will result in the file data only being loaded once. Each call to `ma_resource_manager_data_source_init()` must be @@ -1720,7 +1747,7 @@ actual file paths. When `ma_resource_manager_data_source_init()` is called (with `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM` flag), the resource manager will look for these explicitly registered data buffers and, if found, will use it as the backing data for the data source. Note that the resource manager does *not* make a copy of this data so it is up to the -caller to ensure the pointer stays valid for it's lifetime. Use +caller to ensure the pointer stays valid for its lifetime. Use `ma_resource_manager_unregister_data()` to unregister the self-managed data. You can also use `ma_resource_manager_register_file()` and `ma_resource_manager_unregister_file()` to register and unregister a file. It does not make sense to use the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM` @@ -2031,7 +2058,7 @@ In the above graph, it starts with two data sources whose outputs are attached t splitter node. It's at this point that the two data sources are mixed. After mixing, the splitter performs it's processing routine and produces two outputs which is simply a duplication of the input stream. One output is attached to a low pass filter, whereas the other output is attached to -a echo/delay. The outputs of the the low pass filter and the echo are attached to the endpoint, and +a echo/delay. The outputs of the low pass filter and the echo are attached to the endpoint, and since they're both connected to the same input bus, they'll be mixed. Each input bus must be configured to accept the same number of channels, but the number of channels @@ -2072,7 +2099,7 @@ data from the graph: ``` When you read audio data, miniaudio starts at the node graph's endpoint node which then pulls in -data from it's input attachments, which in turn recursively pull in data from their inputs, and so +data from its input attachments, which in turn recursively pull in data from their inputs, and so on. At the start of the graph there will be some kind of data source node which will have zero inputs and will instead read directly from a data source. The base nodes don't literally need to read from a `ma_data_source` object, but they will always have some kind of underlying object that @@ -2318,7 +2345,7 @@ You can start and stop a node with the following: By default the node is in a started state, but since it won't be connected to anything won't actually be invoked by the node graph until it's connected. When you stop a node, data will not be -read from any of it's input connections. You can use this property to stop a group of sounds +read from any of its input connections. You can use this property to stop a group of sounds atomically. You can configure the initial state of a node in it's config: @@ -2411,29 +2438,29 @@ audio thread is finished so that control is not handed back to the caller thereb chance to free the node's memory. When the audio thread is processing a node, it does so by reading from each of the output buses of -the node. In order for a node to process data for one of it's output buses, it needs to read from -each of it's input buses, and so on an so forth. It follows that once all output buses of a node +the node. In order for a node to process data for one of its output buses, it needs to read from +each of its input buses, and so on an so forth. It follows that once all output buses of a node are detached, the node as a whole will be disconnected and no further processing will occur unless it's output buses are reattached, which won't be happening when the node is being uninitialized. By having `ma_node_detach_output_bus()` wait until the audio thread is finished with it, we can simplify a few things, at the expense of making `ma_node_detach_output_bus()` a bit slower. By doing this, the implementation of `ma_node_uninit()` becomes trivial - just detach all output -nodes, followed by each of the attachments to each of it's input nodes, and then do any final clean +nodes, followed by each of the attachments to each of its input nodes, and then do any final clean up. With the above design, the worst-case scenario is `ma_node_detach_output_bus()` taking as long as it takes to process the output bus being detached. This will happen if it's called at just the wrong moment where the audio thread has just iterated it and has just started processing. The caller of `ma_node_detach_output_bus()` will stall until the audio thread is finished, which -includes the cost of recursively processing it's inputs. This is the biggest compromise made with -the approach taken by miniaudio for it's lock-free processing system. The cost of detaching nodes +includes the cost of recursively processing its inputs. This is the biggest compromise made with +the approach taken by miniaudio for its lock-free processing system. The cost of detaching nodes earlier in the pipeline (data sources, for example) will be cheaper than the cost of detaching higher level nodes, such as some kind of final post-processing endpoint. If you need to do mass detachments, detach starting from the lowest level nodes and work your way towards the final endpoint node (but don't try detaching the node graph's endpoint). If the audio thread is not running, detachment will be fast and detachment in any order will be the same. The reason nodes need to wait for their input attachments to complete is due to the potential for desyncs between -data sources. If the node was to terminate processing mid way through processing it's inputs, +data sources. If the node was to terminate processing mid way through processing its inputs, there's a chance that some of the underlying data sources will have been read, but then others not. That will then result in a potential desynchronization when detaching and reattaching higher-level nodes. A possible solution to this is to have an option when detaching to terminate processing @@ -2804,7 +2831,7 @@ weights. Custom weights can be passed in as the last parameter of `ma_channel_converter_config_init()`. Predefined channel maps can be retrieved with `ma_channel_map_init_standard()`. This takes a -`ma_standard_channel_map` enum as it's first parameter, which can be one of the following: +`ma_standard_channel_map` enum as its first parameter, which can be one of the following: +-----------------------------------+-----------------------------------------------------------+ | Name | Description | @@ -2890,7 +2917,7 @@ like the following: ma_resample_algorithm_linear); ma_resampler resampler; - ma_result result = ma_resampler_init(&config, &resampler); + ma_result result = ma_resampler_init(&config, NULL, &resampler); if (result != MA_SUCCESS) { // An error occurred... } @@ -3132,7 +3159,7 @@ Biquad filtering is achieved with the `ma_biquad` API. Example: ```c ma_biquad_config config = ma_biquad_config_init(ma_format_f32, channels, b0, b1, b2, a0, a1, a2); - ma_result result = ma_biquad_init(&config, &biquad); + ma_result result = ma_biquad_init(&config, NULL, &biquad); if (result != MA_SUCCESS) { // Error. } @@ -3723,7 +3750,7 @@ extern "C" { #define MA_VERSION_MAJOR 0 #define MA_VERSION_MINOR 11 -#define MA_VERSION_REVISION 21 +#define MA_VERSION_REVISION 22 #define MA_VERSION_STRING MA_XSTRINGIFY(MA_VERSION_MAJOR) "." MA_XSTRINGIFY(MA_VERSION_MINOR) "." MA_XSTRINGIFY(MA_VERSION_REVISION) #if defined(_MSC_VER) && !defined(__clang__) @@ -3740,8 +3767,7 @@ extern "C" { #endif - -#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__) +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__) || defined(__ppc64__) #define MA_SIZEOF_PTR 8 #else #define MA_SIZEOF_PTR 4 @@ -3805,7 +3831,7 @@ typedef void* ma_handle; typedef void* ma_ptr; /* -ma_proc is annoying because when compiling with GCC we get pendantic warnings about converting +ma_proc is annoying because when compiling with GCC we get pedantic warnings about converting between `void*` and `void (*)()`. We can't use `void (*)()` with MSVC however, because we'll get warning C4191 about "type cast between incompatible function types". To work around this I'm going to use a different data type depending on the compiler. @@ -3999,7 +4025,7 @@ Special wchar_t type to ensure any structures in the public sections that refere consistent size across all platforms. On Windows, wchar_t is 2 bytes, whereas everywhere else it's 4 bytes. Since Windows likes to use -wchar_t for it's IDs, we need a special explicitly sized wchar type that is always 2 bytes on all +wchar_t for its IDs, we need a special explicitly sized wchar type that is always 2 bytes on all platforms. */ #if !defined(MA_POSIX) && defined(MA_WIN32) @@ -4025,7 +4051,7 @@ MA_LOG_LEVEL_INFO callback. MA_LOG_LEVEL_WARNING - Warnings. You should enable this in you development builds and action them when encounted. These + Warnings. You should enable this in you development builds and action them when encountered. These logs usually indicate a potential problem or misconfiguration, but still allow you to keep running. This will never be called from within the data callback. @@ -5457,7 +5483,7 @@ input frames. MA_API ma_result ma_resampler_get_expected_output_frame_count(const ma_resampler* pResampler, ma_uint64 inputFrameCount, ma_uint64* pOutputFrameCount); /* -Resets the resampler's timer and clears it's internal cache. +Resets the resampler's timer and clears its internal cache. */ MA_API ma_result ma_resampler_reset(ma_resampler* pResampler); @@ -5678,7 +5704,7 @@ MA_API void ma_channel_map_init_standard(ma_standard_channel_map standardChannel /* Copies a channel map. -Both input and output channel map buffers must have a capacity of at at least `channels`. +Both input and output channel map buffers must have a capacity of at least `channels`. */ MA_API void ma_channel_map_copy(ma_channel* pOut, const ma_channel* pIn, ma_uint32 channels); @@ -5817,6 +5843,8 @@ MA_API void ma_data_source_uninit(ma_data_source* pDataSource); MA_API ma_result ma_data_source_read_pcm_frames(ma_data_source* pDataSource, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead); /* Must support pFramesOut = NULL in which case a forward seek should be performed. */ MA_API ma_result ma_data_source_seek_pcm_frames(ma_data_source* pDataSource, ma_uint64 frameCount, ma_uint64* pFramesSeeked); /* Can only seek forward. Equivalent to ma_data_source_read_pcm_frames(pDataSource, NULL, frameCount, &framesRead); */ MA_API ma_result ma_data_source_seek_to_pcm_frame(ma_data_source* pDataSource, ma_uint64 frameIndex); +MA_API ma_result ma_data_source_seek_seconds(ma_data_source* pDataSource, float secondCount, float* pSecondsSeeked); /* Can only seek forward. Abstraction to ma_data_source_seek_pcm_frames() */ +MA_API ma_result ma_data_source_seek_to_second(ma_data_source* pDataSource, float seekPointInSeconds); /* Abstraction to ma_data_source_seek_to_pcm_frame() */ MA_API ma_result ma_data_source_get_data_format(ma_data_source* pDataSource, ma_format* pFormat, ma_uint32* pChannels, ma_uint32* pSampleRate, ma_channel* pChannelMap, size_t channelMapCap); MA_API ma_result ma_data_source_get_cursor_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pCursor); MA_API ma_result ma_data_source_get_length_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pLength); /* Returns MA_NOT_IMPLEMENTED if the length is unknown or cannot be determined. Decoders can return this. */ @@ -6182,6 +6210,12 @@ MA_API ma_result ma_event_wait(ma_event* pEvent); Signals the specified auto-reset event. */ MA_API ma_result ma_event_signal(ma_event* pEvent); + + +MA_API ma_result ma_semaphore_init(int initialValue, ma_semaphore* pSemaphore); +MA_API void ma_semaphore_uninit(ma_semaphore* pSemaphore); +MA_API ma_result ma_semaphore_wait(ma_semaphore* pSemaphore); +MA_API ma_result ma_semaphore_release(ma_semaphore* pSemaphore); #endif /* MA_NO_THREADING */ @@ -6273,7 +6307,7 @@ Job Queue /* Slot Allocator -------------- -The idea of the slot allocator is for it to be used in conjunction with a fixed sized buffer. You use the slot allocator to allocator an index that can be used +The idea of the slot allocator is for it to be used in conjunction with a fixed sized buffer. You use the slot allocator to allocate an index that can be used as the insertion point for an object. Slots are reference counted to help mitigate the ABA problem in the lock-free queue we use for tracking jobs. @@ -7006,6 +7040,8 @@ typedef union int nullbackend; /* The null backend uses an integer for device IDs. */ } ma_device_id; +MA_API ma_bool32 ma_device_id_equal(const ma_device_id* pA, const ma_device_id* pB); + typedef struct ma_context_config ma_context_config; typedef struct ma_device_config ma_device_config; @@ -7093,6 +7129,7 @@ struct ma_device_config { const char* pStreamNamePlayback; const char* pStreamNameCapture; + int channelMap; } pulse; struct { @@ -7112,6 +7149,7 @@ struct ma_device_config ma_aaudio_allowed_capture_policy allowedCapturePolicy; ma_bool32 noAutoStartAfterReroute; ma_bool32 enableCompatibilityWorkarounds; + ma_bool32 allowSetBufferCapacity; } aaudio; }; @@ -7184,7 +7222,7 @@ and on output returns detailed information about the device in `ma_device_info`. case when the device ID is NULL, in which case information about the default device needs to be retrieved. Once the context has been created and the device ID retrieved (if using anything other than the default device), the device can be created. -This is a little bit more complicated than initialization of the context due to it's more complicated configuration. When initializing a +This is a little bit more complicated than initialization of the context due to its more complicated configuration. When initializing a device, a duplex device may be requested. This means a separate data format needs to be specified for both playback and capture. On input, the data format is set to what the application wants. On output it's set to the native format which should match as closely as possible to the requested format. The conversion between the format requested by the application and the device's native format will be handled @@ -7205,10 +7243,10 @@ asynchronous reading and writing, `onDeviceStart()` and `onDeviceStop()` should The handling of data delivery between the application and the device is the most complicated part of the process. To make this a bit easier, some helper callbacks are available. If the backend uses a blocking read/write style of API, the `onDeviceRead()` and `onDeviceWrite()` callbacks can optionally be implemented. These are blocking and work just like reading and writing from a file. If the -backend uses a callback for data delivery, that callback must call `ma_device_handle_backend_data_callback()` from within it's callback. +backend uses a callback for data delivery, that callback must call `ma_device_handle_backend_data_callback()` from within its callback. This allows miniaudio to then process any necessary data conversion and then pass it to the miniaudio data callback. -If the backend requires absolute flexibility with it's data delivery, it can optionally implement the `onDeviceDataLoop()` callback +If the backend requires absolute flexibility with its data delivery, it can optionally implement the `onDeviceDataLoop()` callback which will allow it to implement the logic that will run on the audio thread. This is much more advanced and is completely optional. The audio thread should run data delivery logic in a loop while `ma_device_get_state() == ma_device_state_started` and no errors have been @@ -7248,6 +7286,10 @@ struct ma_context_config void* pUserData; ma_allocation_callbacks allocationCallbacks; struct + { + ma_handle hWnd; /* HWND. Optional window handle to pass into SetCooperativeLevel(). Will default to the foreground window, and if that fails, the desktop window. */ + } dsound; + struct { ma_bool32 useVerboseDeviceEnumeration; } alsa; @@ -7336,6 +7378,7 @@ struct ma_context #ifdef MA_SUPPORT_DSOUND struct { + ma_handle hWnd; /* Can be null. */ ma_handle hDSoundDLL; ma_proc DirectSoundCreate; ma_proc DirectSoundEnumerateA; @@ -7942,6 +7985,7 @@ struct ma_device { /*AAudioStream**/ ma_ptr pStreamPlayback; /*AAudioStream**/ ma_ptr pStreamCapture; + ma_mutex rerouteLock; ma_aaudio_usage usage; ma_aaudio_content_type contentType; ma_aaudio_input_preset inputPreset; @@ -8365,6 +8409,10 @@ Retrieves basic information about every active playback and/or capture device. This function will allocate memory internally for the device lists and return a pointer to them through the `ppPlaybackDeviceInfos` and `ppCaptureDeviceInfos` parameters. If you do not want to incur the overhead of these allocations consider using `ma_context_enumerate_devices()` which will instead use a callback. +Note that this only retrieves the ID and name/description of the device. The reason for only retrieving basic information is that it would otherwise require +opening the backend device in order to probe it for more detailed information which can be inefficient. Consider using `ma_context_get_device_info()` for this, +but don't call it from within the enumeration callback. + Parameters ---------- @@ -8406,7 +8454,7 @@ The returned pointers will become invalid upon the next call this this function, See Also -------- -ma_context_get_devices() +ma_context_enumerate_devices() */ MA_API ma_result ma_context_get_devices(ma_context* pContext, ma_device_info** ppPlaybackDeviceInfos, ma_uint32* pPlaybackDeviceCount, ma_device_info** ppCaptureDeviceInfos, ma_uint32* pCaptureDeviceCount); @@ -8545,7 +8593,7 @@ from a microphone. Whether or not you should send or receive data from the devic playback, capture, full-duplex or loopback. (Note that loopback mode is only supported on select backends.) Sending and receiving audio data to and from the device is done via a callback which is fired by miniaudio at periodic time intervals. -The frequency at which data is delivered to and from a device depends on the size of it's period. The size of the period can be defined in terms of PCM frames +The frequency at which data is delivered to and from a device depends on the size of its period. The size of the period can be defined in terms of PCM frames or milliseconds, whichever is more convenient. Generally speaking, the smaller the period, the lower the latency at the expense of higher CPU usage and increased risk of glitching due to the more frequent and granular data deliver intervals. The size of a period will depend on your requirements, but miniaudio's defaults should work fine for most scenarios. If you're building a game you should leave this fairly small, whereas if you're building a simple @@ -8619,7 +8667,7 @@ then be set directly on the structure. Below are the members of the `ma_device_c performanceProfile A hint to miniaudio as to the performance requirements of your program. Can be either `ma_performance_profile_low_latency` (default) or - `ma_performance_profile_conservative`. This mainly affects the size of default buffers and can usually be left at it's default value. + `ma_performance_profile_conservative`. This mainly affects the size of default buffers and can usually be left at its default value. noPreSilencedOutputBuffer When set to true, the contents of the output buffer passed into the data callback will be left undefined. When set to false (default), the contents of @@ -8659,7 +8707,7 @@ then be set directly on the structure. Below are the members of the `ma_device_c A pointer that will passed to callbacks in pBackendVTable. resampling.linear.lpfOrder - The linear resampler applies a low-pass filter as part of it's processing for anti-aliasing. This setting controls the order of the filter. The higher + The linear resampler applies a low-pass filter as part of its processing for anti-aliasing. This setting controls the order of the filter. The higher the value, the better the quality, in general. Setting this to 0 will disable low-pass filtering altogether. The maximum value is `MA_MAX_FILTER_ORDER`. The default value is `min(4, MA_MAX_FILTER_ORDER)`. @@ -8736,6 +8784,9 @@ then be set directly on the structure. Below are the members of the `ma_device_c pulse.pStreamNameCapture PulseAudio only. Sets the stream name for capture. + pulse.channelMap + PulseAudio only. Sets the channel map that is requested from PulseAudio. See MA_PA_CHANNEL_MAP_* constants. Defaults to MA_PA_CHANNEL_MAP_AIFF. + coreaudio.allowNominalSampleRateChange Core Audio only. Desktop only. When enabled, allows the sample rate of the device to be changed at the operating system level. This is disabled by default in order to prevent intrusive changes to the user's system. This is useful if you want to use a sample rate @@ -8909,7 +8960,7 @@ Unsafe. It is not safe to call this inside any callback. Remarks ------- -You only need to use this function if you want to configure the context differently to it's defaults. You should never use this function if you want to manage +You only need to use this function if you want to configure the context differently to its defaults. You should never use this function if you want to manage your own context. See the documentation for `ma_context_init()` for information on the different context configuration options. @@ -9674,7 +9725,7 @@ Utilities ************************************************************************************************************************************************************/ /* -Calculates a buffer size in milliseconds from the specified number of frames and sample rate. +Calculates a buffer size in milliseconds (rounded up) from the specified number of frames and sample rate. */ MA_API ma_uint32 ma_calculate_buffer_size_in_milliseconds_from_frames(ma_uint32 bufferSizeInFrames, ma_uint32 sampleRate); @@ -9931,7 +9982,7 @@ struct ma_decoder void* pInputCache; /* In input format. Can be null if it's not needed. */ ma_uint64 inputCacheCap; /* The capacity of the input cache. */ ma_uint64 inputCacheConsumed; /* The number of frames that have been consumed in the cache. Used for determining the next valid frame. */ - ma_uint64 inputCacheRemaining; /* The number of valid frames remaining in the cahce. */ + ma_uint64 inputCacheRemaining; /* The number of valid frames remaining in the cache. */ ma_allocation_callbacks allocationCallbacks; union { @@ -9972,7 +10023,7 @@ This is not thread safe without your own synchronization. MA_API ma_result ma_decoder_read_pcm_frames(ma_decoder* pDecoder, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead); /* -Seeks to a PCM frame based on it's absolute index. +Seeks to a PCM frame based on its absolute index. This is not thread safe without your own synchronization. */ @@ -10235,7 +10286,8 @@ typedef enum MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE = 0x00000002, /* Decode data before storing in memory. When set, decoding is done at the resource manager level rather than the mixing thread. Results in faster mixing, but higher memory usage. */ MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC = 0x00000004, /* When set, the resource manager will load the data source asynchronously. */ MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT = 0x00000008, /* When set, waits for initialization of the underlying data source before returning from ma_resource_manager_data_source_init(). */ - MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH = 0x00000010 /* Gives the resource manager a hint that the length of the data source is unknown and calling `ma_data_source_get_length_in_pcm_frames()` should be avoided. */ + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH = 0x00000010, /* Gives the resource manager a hint that the length of the data source is unknown and calling `ma_data_source_get_length_in_pcm_frames()` should be avoided. */ + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING = 0x00000020 /* When set, configures the data source to loop by default. */ } ma_resource_manager_data_source_flags; @@ -10303,8 +10355,8 @@ typedef struct ma_uint64 rangeEndInPCMFrames; ma_uint64 loopPointBegInPCMFrames; ma_uint64 loopPointEndInPCMFrames; - ma_bool32 isLooping; ma_uint32 flags; + ma_bool32 isLooping; /* Deprecated. Use the MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING flag in `flags` instead. */ } ma_resource_manager_data_source_config; MA_API ma_resource_manager_data_source_config ma_resource_manager_data_source_config_init(void); @@ -10547,6 +10599,16 @@ Node Graph /* Use this when the bus count is determined by the node instance rather than the vtable. */ #define MA_NODE_BUS_COUNT_UNKNOWN 255 + +/* For some internal memory management of ma_node_graph. */ +typedef struct +{ + size_t offset; + size_t sizeInBytes; + unsigned char _data[1]; +} ma_stack; + + typedef struct ma_node_graph ma_node_graph; typedef void ma_node; @@ -10586,7 +10648,7 @@ typedef struct void (* onProcess)(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn, float** ppFramesOut, ma_uint32* pFrameCountOut); /* - A callback for retrieving the number of a input frames that are required to output the + A callback for retrieving the number of input frames that are required to output the specified number of output frames. You would only want to implement this when the node performs resampling. This is optional, even for nodes that perform resampling, but it does offer a small reduction in latency as it allows miniaudio to calculate the exact number of input frames @@ -10671,10 +10733,14 @@ typedef struct ma_node_base ma_node_base; struct ma_node_base { /* These variables are set once at startup. */ - ma_node_graph* pNodeGraph; /* The graph this node belongs to. */ + ma_node_graph* pNodeGraph; /* The graph this node belongs to. */ const ma_node_vtable* vtable; - float* pCachedData; /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ - ma_uint16 cachedDataCapInFramesPerBus; /* The capacity of the input data cache in frames, per bus. */ + ma_uint32 inputBusCount; + ma_uint32 outputBusCount; + ma_node_input_bus* pInputBuses; + ma_node_output_bus* pOutputBuses; + float* pCachedData; /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ + ma_uint16 cachedDataCapInFramesPerBus; /* The capacity of the input data cache in frames, per bus. */ /* These variables are read and written only from the audio thread. */ ma_uint16 cachedFrameCountOut; @@ -10682,13 +10748,9 @@ struct ma_node_base ma_uint16 consumedFrameCountIn; /* These variables are read and written between different threads. */ - MA_ATOMIC(4, ma_node_state) state; /* When set to stopped, nothing will be read, regardless of the times in stateTimes. */ - MA_ATOMIC(8, ma_uint64) stateTimes[2]; /* Indexed by ma_node_state. Specifies the time based on the global clock that a node should be considered to be in the relevant state. */ - MA_ATOMIC(8, ma_uint64) localTime; /* The node's local clock. This is just a running sum of the number of output frames that have been processed. Can be modified by any thread with `ma_node_set_time()`. */ - ma_uint32 inputBusCount; - ma_uint32 outputBusCount; - ma_node_input_bus* pInputBuses; - ma_node_output_bus* pOutputBuses; + MA_ATOMIC(4, ma_node_state) state; /* When set to stopped, nothing will be read, regardless of the times in stateTimes. */ + MA_ATOMIC(8, ma_uint64) stateTimes[2]; /* Indexed by ma_node_state. Specifies the time based on the global clock that a node should be considered to be in the relevant state. */ + MA_ATOMIC(8, ma_uint64) localTime; /* The node's local clock. This is just a running sum of the number of output frames that have been processed. Can be modified by any thread with `ma_node_set_time()`. */ /* Memory management. */ ma_node_input_bus _inputBuses[MA_MAX_NODE_LOCAL_BUS_COUNT]; @@ -10724,7 +10786,8 @@ MA_API ma_result ma_node_set_time(ma_node* pNode, ma_uint64 localTime); typedef struct { ma_uint32 channels; - ma_uint16 nodeCacheCapInFrames; + ma_uint32 processingSizeInFrames; /* This is the preferred processing size for node processing callbacks unless overridden by a node itself. Can be 0 in which case it will be based on the frame count passed into ma_node_graph_read_pcm_frames(), but will not be well defined. */ + size_t preMixStackSizeInBytes; /* Defaults to 512KB per channel. Reducing this will save memory, but the depth of your node graph will be more restricted. */ } ma_node_graph_config; MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels); @@ -10735,10 +10798,15 @@ struct ma_node_graph /* Immutable. */ ma_node_base base; /* The node graph itself is a node so it can be connected as an input to different node graph. This has zero inputs and calls ma_node_graph_read_pcm_frames() to generate it's output. */ ma_node_base endpoint; /* Special node that all nodes eventually connect to. Data is read from this node in ma_node_graph_read_pcm_frames(). */ - ma_uint16 nodeCacheCapInFrames; + float* pProcessingCache; /* This will be allocated when processingSizeInFrames is non-zero. This is needed because ma_node_graph_read_pcm_frames() can be called with a variable number of frames, and we may need to do some buffering in situations where the caller requests a frame count that's not a multiple of processingSizeInFrames. */ + ma_uint32 processingCacheFramesRemaining; + ma_uint32 processingSizeInFrames; /* Read and written by multiple threads. */ MA_ATOMIC(4, ma_bool32) isReading; + + /* Modified only by the audio thread. */ + ma_stack* pPreMixStack; }; MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_node_graph* pNodeGraph); @@ -11023,6 +11091,7 @@ typedef enum MA_SOUND_FLAG_ASYNC = 0x00000004, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC */ MA_SOUND_FLAG_WAIT_INIT = 0x00000008, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT */ MA_SOUND_FLAG_UNKNOWN_LENGTH = 0x00000010, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH */ + MA_SOUND_FLAG_LOOPING = 0x00000020, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING */ /* ma_sound specific flags. */ MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT = 0x00001000, /* Do not attach to the endpoint by default. Useful for when setting up nodes in a complex graph system. */ @@ -11062,7 +11131,7 @@ MA_API ma_engine_node_config ma_engine_node_config_init(ma_engine* pEngine, ma_e /* Base node object for both ma_sound and ma_sound_group. */ typedef struct { - ma_node_base baseNode; /* Must be the first member for compatiblity with the ma_node API. */ + ma_node_base baseNode; /* Must be the first member for compatibility with the ma_node API. */ ma_engine* pEngine; /* A pointer to the engine. Set based on the value from the config. */ ma_uint32 sampleRate; /* The sample rate of the input data. For sounds backed by a data source, this will be the data source's sample rate. Otherwise it'll be the engine's sample rate. */ ma_uint32 volumeSmoothTimeInPCMFrames; @@ -11122,13 +11191,13 @@ typedef struct ma_uint64 rangeEndInPCMFrames; ma_uint64 loopPointBegInPCMFrames; ma_uint64 loopPointEndInPCMFrames; - ma_bool32 isLooping; ma_sound_end_proc endCallback; /* Fired when the sound reaches the end. Will be fired from the audio thread. Do not restart, uninitialize or otherwise change the state of the sound from here. Instead fire an event or set a variable to indicate to a different thread to change the start of the sound. Will not be fired in response to a scheduled stop with ma_sound_set_stop_time_*(). */ void* pEndCallbackUserData; #ifndef MA_NO_RESOURCE_MANAGER ma_resource_manager_pipeline_notifications initNotifications; #endif ma_fence* pDoneFence; /* Deprecated. Use initNotifications instead. Released when the resource manager has finished decoding the entire sound. Not used with streams. */ + ma_bool32 isLooping; /* Deprecated. Use the MA_SOUND_FLAG_LOOPING flag in `flags` instead. */ } ma_sound_config; MA_API ma_sound_config ma_sound_config_init(void); /* Deprecated. Will be removed in version 0.12. Use ma_sound_config_2() instead. */ @@ -11192,6 +11261,7 @@ typedef struct ma_uint32 gainSmoothTimeInFrames; /* The number of frames to interpolate the gain of spatialized sounds across. If set to 0, will use gainSmoothTimeInMilliseconds. */ ma_uint32 gainSmoothTimeInMilliseconds; /* When set to 0, gainSmoothTimeInFrames will be used. If both are set to 0, a default value will be used. */ ma_uint32 defaultVolumeSmoothTimeInPCMFrames; /* Defaults to 0. Controls the default amount of smoothing to apply to volume changes to sounds. High values means more smoothing at the expense of high latency (will take longer to reach the new volume). */ + ma_uint32 preMixStackSizeInBytes; /* A stack is used for internal processing in the node graph. This allows you to configure the size of this stack. Smaller values will reduce the maximum depth of your node graph. You should rarely need to modify this. */ ma_allocation_callbacks allocationCallbacks; ma_bool32 noAutoStart; /* When set to true, requires an explicit call to ma_engine_start(). This is false by default, meaning the engine will be started automatically in ma_engine_init(). */ ma_bool32 noDevice; /* When set to true, don't create a default device. ma_engine_read_pcm_frames() can be called manually to read data. */ @@ -11206,12 +11276,12 @@ MA_API ma_engine_config ma_engine_config_init(void); struct ma_engine { - ma_node_graph nodeGraph; /* An engine is a node graph. It should be able to be plugged into any ma_node_graph API (with a cast) which means this must be the first member of this struct. */ + ma_node_graph nodeGraph; /* An engine is a node graph. It should be able to be plugged into any ma_node_graph API (with a cast) which means this must be the first member of this struct. */ #if !defined(MA_NO_RESOURCE_MANAGER) ma_resource_manager* pResourceManager; #endif #if !defined(MA_NO_DEVICE_IO) - ma_device* pDevice; /* Optionally set via the config, otherwise allocated by the engine in ma_engine_init(). */ + ma_device* pDevice; /* Optionally set via the config, otherwise allocated by the engine in ma_engine_init(). */ #endif ma_log* pLog; ma_uint32 sampleRate; @@ -11220,10 +11290,10 @@ struct ma_engine ma_allocation_callbacks allocationCallbacks; ma_bool8 ownsResourceManager; ma_bool8 ownsDevice; - ma_spinlock inlinedSoundLock; /* For synchronizing access so the inlined sound list. */ - ma_sound_inlined* pInlinedSoundHead; /* The first inlined sound. Inlined sounds are tracked in a linked list. */ - MA_ATOMIC(4, ma_uint32) inlinedSoundCount; /* The total number of allocated inlined sound objects. Used for debugging. */ - ma_uint32 gainSmoothTimeInFrames; /* The number of frames to interpolate the gain of spatialized sounds across. */ + ma_spinlock inlinedSoundLock; /* For synchronizing access to the inlined sound list. */ + ma_sound_inlined* pInlinedSoundHead; /* The first inlined sound. Inlined sounds are tracked in a linked list. */ + MA_ATOMIC(4, ma_uint32) inlinedSoundCount; /* The total number of allocated inlined sound objects. Used for debugging. */ + ma_uint32 gainSmoothTimeInFrames; /* The number of frames to interpolate the gain of spatialized sounds across. */ ma_uint32 defaultVolumeSmoothTimeInPCMFrames; ma_mono_expansion_mode monoExpansionMode; ma_engine_process_proc onProcess; @@ -11348,6 +11418,7 @@ MA_API void ma_sound_set_looping(ma_sound* pSound, ma_bool32 isLooping); MA_API ma_bool32 ma_sound_is_looping(const ma_sound* pSound); MA_API ma_bool32 ma_sound_at_end(const ma_sound* pSound); MA_API ma_result ma_sound_seek_to_pcm_frame(ma_sound* pSound, ma_uint64 frameIndex); /* Just a wrapper around ma_data_source_seek_to_pcm_frame(). */ +MA_API ma_result ma_sound_seek_to_second(ma_sound* pSound, float seekPointInSeconds); /* Abstraction to ma_sound_seek_to_pcm_frame() */ MA_API ma_result ma_sound_get_data_format(ma_sound* pSound, ma_format* pFormat, ma_uint32* pChannels, ma_uint32* pSampleRate, ma_channel* pChannelMap, size_t channelMapCap); MA_API ma_result ma_sound_get_cursor_in_pcm_frames(ma_sound* pSound, ma_uint64* pCursor); MA_API ma_result ma_sound_get_length_in_pcm_frames(ma_sound* pSound, ma_uint64* pLength); @@ -13861,7 +13932,7 @@ static ma_uint32 ma_ffs_32(ma_uint32 x) /* Just a naive implementation just to get things working for now. Will optimize this later. */ for (i = 0; i < 32; i += 1) { - if ((x & (1 << i)) != 0) { + if ((x & (1U << i)) != 0) { return i; } } @@ -14024,7 +14095,7 @@ static MA_INLINE ma_int32 ma_dither_s32(ma_dither_mode ditherMode, ma_int32 dith Atomics **************************************************************************************************************************************************************/ -/* ma_atomic.h begin */ +/* c89atomic.h begin */ #ifndef ma_atomic_h #if defined(__cplusplus) extern "C" { @@ -14750,12 +14821,12 @@ typedef int ma_atomic_memory_order; typedef ma_uint8 ma_atomic_flag; #define ma_atomic_flag_test_and_set_explicit(ptr, order) (ma_bool32)ma_atomic_test_and_set_explicit_8(ptr, order) #define ma_atomic_flag_clear_explicit(ptr, order) ma_atomic_clear_explicit_8(ptr, order) - #define c89atoimc_flag_load_explicit(ptr, order) ma_atomic_load_explicit_8(ptr, order) + #define ma_atomic_flag_load_explicit(ptr, order) ma_atomic_load_explicit_8(ptr, order) #else typedef ma_uint32 ma_atomic_flag; #define ma_atomic_flag_test_and_set_explicit(ptr, order) (ma_bool32)ma_atomic_test_and_set_explicit_32(ptr, order) #define ma_atomic_flag_clear_explicit(ptr, order) ma_atomic_clear_explicit_32(ptr, order) - #define c89atoimc_flag_load_explicit(ptr, order) ma_atomic_load_explicit_32(ptr, order) + #define ma_atomic_flag_load_explicit(ptr, order) ma_atomic_load_explicit_32(ptr, order) #endif #elif defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) #define MA_ATOMIC_HAS_NATIVE_COMPARE_EXCHANGE @@ -14836,15 +14907,24 @@ typedef int ma_atomic_memory_order; __atomic_compare_exchange_n(dst, &expected, desired, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); return expected; } + #if defined(__clang__) + #pragma clang diagnostic push + #if __clang_major__ >= 8 + #pragma clang diagnostic ignored "-Watomic-alignment" + #endif + #endif static MA_INLINE ma_uint64 ma_atomic_compare_and_swap_64(volatile ma_uint64* dst, ma_uint64 expected, ma_uint64 desired) { __atomic_compare_exchange_n(dst, &expected, desired, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); return expected; } + #if defined(__clang__) + #pragma clang diagnostic pop + #endif typedef ma_uint8 ma_atomic_flag; #define ma_atomic_flag_test_and_set_explicit(dst, order) (ma_bool32)__atomic_test_and_set(dst, order) #define ma_atomic_flag_clear_explicit(dst, order) __atomic_clear(dst, order) - #define c89atoimc_flag_load_explicit(ptr, order) ma_atomic_load_explicit_8(ptr, order) + #define ma_atomic_flag_load_explicit(ptr, order) ma_atomic_load_explicit_8(ptr, order) #else #define ma_atomic_memory_order_relaxed 1 #define ma_atomic_memory_order_consume 2 @@ -15358,7 +15438,7 @@ typedef int ma_atomic_memory_order; typedef ma_uint8 ma_atomic_flag; #define ma_atomic_flag_test_and_set_explicit(ptr, order) (ma_bool32)ma_atomic_test_and_set_explicit_8(ptr, order) #define ma_atomic_flag_clear_explicit(ptr, order) ma_atomic_clear_explicit_8(ptr, order) - #define c89atoimc_flag_load_explicit(ptr, order) ma_atomic_load_explicit_8(ptr, order) + #define ma_atomic_flag_load_explicit(ptr, order) ma_atomic_load_explicit_8(ptr, order) #endif #if !defined(MA_ATOMIC_HAS_NATIVE_COMPARE_EXCHANGE) #if defined(MA_ATOMIC_HAS_8) @@ -15883,7 +15963,7 @@ static MA_INLINE void ma_atomic_spinlock_lock(volatile ma_atomic_spinlock* pSpin if (ma_atomic_flag_test_and_set_explicit(pSpinlock, ma_atomic_memory_order_acquire) == 0) { break; } - while (c89atoimc_flag_load_explicit(pSpinlock, ma_atomic_memory_order_relaxed) == 1) { + while (ma_atomic_flag_load_explicit(pSpinlock, ma_atomic_memory_order_relaxed) == 1) { } } } @@ -15898,7 +15978,7 @@ static MA_INLINE void ma_atomic_spinlock_unlock(volatile ma_atomic_spinlock* pSp } #endif #endif -/* ma_atomic.h end */ +/* c89atomic.h end */ #define MA_ATOMIC_SAFE_TYPE_IMPL(c89TypeExtension, type) \ static MA_INLINE ma_##type ma_atomic_##type##_get(ma_atomic_##type* x) \ @@ -16096,7 +16176,7 @@ static ma_result ma_thread_create__posix(ma_thread* pThread, ma_thread_priority int result; pthread_attr_t* pAttr = NULL; -#if !defined(__EMSCRIPTEN__) +#if !defined(__EMSCRIPTEN__) && !defined(__3DS__) /* Try setting the thread priority. It's not critical if anything fails here. */ pthread_attr_t attr; if (pthread_attr_init(&attr) == 0) { @@ -16142,19 +16222,34 @@ static ma_result ma_thread_create__posix(ma_thread* pThread, ma_thread_priority if (priority == ma_thread_priority_idle) { sched.sched_priority = priorityMin; } else if (priority == ma_thread_priority_realtime) { - sched.sched_priority = priorityMax; - } else { - sched.sched_priority += ((int)priority + 5) * priorityStep; /* +5 because the lowest priority is -5. */ - if (sched.sched_priority < priorityMin) { - sched.sched_priority = priorityMin; + #if defined(MA_PTHREAD_REALTIME_THREAD_PRIORITY) + { + sched.sched_priority = MA_PTHREAD_REALTIME_THREAD_PRIORITY; } - if (sched.sched_priority > priorityMax) { + #else + { sched.sched_priority = priorityMax; } + #endif + } else { + sched.sched_priority += ((int)priority + 5) * priorityStep; /* +5 because the lowest priority is -5. */ } - /* I'm not treating a failure of setting the priority as a critical error so not checking the return value here. */ - pthread_attr_setschedparam(&attr, &sched); + if (sched.sched_priority < priorityMin) { + sched.sched_priority = priorityMin; + } + if (sched.sched_priority > priorityMax) { + sched.sched_priority = priorityMax; + } + + /* I'm not treating a failure of setting the priority as a critical error so not aborting on failure here. */ + if (pthread_attr_setschedparam(&attr, &sched) == 0) { + #if !defined(MA_ANDROID) || (defined(__ANDROID_API__) && __ANDROID_API__ >= 28) + { + pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); + } + #endif + } } } } @@ -16187,7 +16282,7 @@ static void ma_thread_wait__posix(ma_thread* pThread) static ma_result ma_mutex_init__posix(ma_mutex* pMutex) { int result; - + if (pMutex == NULL) { return MA_INVALID_ARGS; } @@ -17406,7 +17501,7 @@ static ma_job_proc g_jobVTable[MA_JOB_TYPE_COUNT] = /* Device. */ #if !defined(MA_NO_DEVICE_IO) - ma_job_process__device__aaudio_reroute /*MA_JOB_TYPE_DEVICE_AAUDIO_REROUTE*/ + ma_job_process__device__aaudio_reroute /* MA_JOB_TYPE_DEVICE_AAUDIO_REROUTE */ #endif }; @@ -17751,7 +17846,7 @@ MA_API ma_result ma_job_queue_next(ma_job_queue* pQueue, ma_job* pJob) is stored. One thread can fall through to the freeing of this item while another is still using "head" for the retrieval of the "next" variable. - The slot allocator might need to make use of some reference counting to ensure it's only truely freed when + The slot allocator might need to make use of some reference counting to ensure it's only truly freed when there are no more references to the item. This must be fixed before removing these locks. */ @@ -17859,7 +17954,16 @@ MA_API void ma_dlclose(ma_log* pLog, ma_handle handle) #ifdef MA_WIN32 FreeLibrary((HMODULE)handle); #else - dlclose((void*)handle); + /* Hack for Android bug (see https://github.com/android/ndk/issues/360). Calling dlclose() pre-API 28 may segfault. */ + #if !defined(MA_ANDROID) || (defined(__ANDROID_API__) && __ANDROID_API__ >= 28) + { + dlclose((void*)handle); + } + #else + { + (void)handle; + } + #endif #endif (void)pLog; @@ -17880,12 +17984,12 @@ MA_API ma_proc ma_dlsym(ma_log* pLog, ma_handle handle, const char* symbol) #ifdef _WIN32 proc = (ma_proc)GetProcAddress((HMODULE)handle, symbol); #else -#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) +#if (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #endif proc = (ma_proc)dlsym((void*)handle, symbol); -#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) +#if (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))) || defined(__clang__) #pragma GCC diagnostic pop #endif #endif @@ -17923,9 +18027,13 @@ DEVICE I/O #endif #endif +#ifdef MA_APPLE + #include +#endif + #ifndef MA_NO_DEVICE_IO -#if defined(MA_APPLE) && (__MAC_OS_X_VERSION_MIN_REQUIRED < 101200) +#if defined(MA_APPLE) && (MAC_OS_X_VERSION_MIN_REQUIRED < 101200) #include /* For mach_absolute_time() */ #endif @@ -17939,6 +18047,10 @@ DEVICE I/O #endif #endif +/* This must be set to at least 26. */ +#ifndef MA_AAUDIO_MIN_ANDROID_SDK_VERSION +#define MA_AAUDIO_MIN_ANDROID_SDK_VERSION 27 +#endif MA_API void ma_device_info_add_native_data_format(ma_device_info* pDeviceInfo, ma_format format, ma_uint32 channels, ma_uint32 sampleRate, ma_uint32 flags) @@ -18085,7 +18197,7 @@ MA_API ma_bool32 ma_is_backend_enabled(ma_backend backend) #if defined(MA_HAS_AAUDIO) #if defined(MA_ANDROID) { - return ma_android_sdk_version() >= 26; + return ma_android_sdk_version() >= MA_AAUDIO_MIN_ANDROID_SDK_VERSION; } #else return MA_FALSE; @@ -18402,7 +18514,6 @@ typedef LONG (WINAPI * MA_PFN_RegCloseKey)(HKEY hKey); typedef LONG (WINAPI * MA_PFN_RegQueryValueExA)(HKEY hKey, const char* lpValueName, DWORD* lpReserved, DWORD* lpType, BYTE* lpData, DWORD* lpcbData); #endif /* MA_WIN32_DESKTOP */ - MA_API size_t ma_strlen_WCHAR(const WCHAR* str) { size_t len = 0; @@ -18487,7 +18598,7 @@ Timing return (double)(counter.QuadPart - pTimer->counter) / g_ma_TimerFrequency.QuadPart; } -#elif defined(MA_APPLE) && (__MAC_OS_X_VERSION_MIN_REQUIRED < 101200) +#elif defined(MA_APPLE) && (MAC_OS_X_VERSION_MIN_REQUIRED < 101200) static ma_uint64 g_ma_TimerFrequency = 0; static void ma_timer_init(ma_timer* pTimer) { @@ -18670,11 +18781,16 @@ static void ma_device__on_notification_rerouted(ma_device* pDevice) #endif #if defined(MA_EMSCRIPTEN) -EMSCRIPTEN_KEEPALIVE -void ma_device__on_notification_unlocked(ma_device* pDevice) +#ifdef __cplusplus +extern "C" { +#endif +void EMSCRIPTEN_KEEPALIVE ma_device__on_notification_unlocked(ma_device* pDevice) { ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_unlocked)); } +#ifdef __cplusplus +} +#endif #endif @@ -18802,7 +18918,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut unsigned int prevDenormalState = ma_device_disable_denormals(pDevice); { /* Volume control of input makes things a bit awkward because the input buffer is read-only. We'll need to use a temp buffer and loop in this case. */ - if (pFramesIn != NULL && masterVolumeFactor < 1) { + if (pFramesIn != NULL && masterVolumeFactor != 1) { ma_uint8 tempFramesIn[MA_DATA_CONVERTER_STACK_BUFFER_SIZE]; ma_uint32 bpfCapture = ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels); ma_uint32 bpfPlayback = ma_get_bytes_per_frame(pDevice->playback.format, pDevice->playback.channels); @@ -18825,7 +18941,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut /* Volume control and clipping for playback devices. */ if (pFramesOut != NULL) { - if (masterVolumeFactor < 1) { + if (masterVolumeFactor != 1) { if (pFramesIn == NULL) { /* <-- In full-duplex situations, the volume will have been applied to the input samples before the data callback. Applying it again post-callback will incorrectly compound it. */ ma_apply_volume_factor_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels, masterVolumeFactor); } @@ -18837,6 +18953,11 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut } } ma_device_restore_denormals(pDevice, prevDenormalState); + } else { + /* No data callback. Just silence the output. */ + if (pFramesOut != NULL) { + ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels); + } } } @@ -18922,9 +19043,7 @@ static void ma_device__read_frames_from_client(ma_device* pDevice, ma_uint32 fra framesToReadThisIterationIn = requiredInputFrameCount; } - if (framesToReadThisIterationIn > 0) { - ma_device__handle_data_callback(pDevice, pIntermediaryBuffer, NULL, (ma_uint32)framesToReadThisIterationIn); - } + ma_device__handle_data_callback(pDevice, pIntermediaryBuffer, NULL, (ma_uint32)framesToReadThisIterationIn); /* At this point we have our decoded data in input format and now we need to convert to output format. Note that even if we didn't read any @@ -18965,7 +19084,7 @@ static void ma_device__send_frames_to_client(ma_device* pDevice, ma_uint32 frame ma_uint64 totalClientFramesProcessed = 0; const void* pRunningFramesInDeviceFormat = pFramesInDeviceFormat; - /* We just keep going until we've exhaused all of our input frames and cannot generate any more output frames. */ + /* We just keep going until we've exhausted all of our input frames and cannot generate any more output frames. */ for (;;) { ma_uint64 deviceFramesProcessedThisIteration; ma_uint64 clientFramesProcessedThisIteration; @@ -19248,7 +19367,7 @@ static ma_result ma_device_audio_thread__default_read_write(ma_device* pDevice) } /* - If we weren't able to generate any output frames it must mean we've exhaused all of our input. The only time this would not be the case is if capturedClientData was too small + If we weren't able to generate any output frames it must mean we've exhausted all of our input. The only time this would not be the case is if capturedClientData was too small which should never be the case when it's of the size MA_DATA_CONVERTER_STACK_BUFFER_SIZE. */ if (capturedClientFramesToProcessThisIteration == 0) { @@ -19451,7 +19570,7 @@ static ma_result ma_device_do_operation__null(ma_device* pDevice, ma_uint32 oper /* The first thing to do is wait for an operation slot to become available. We only have a single slot for this, but we could extend this later - to support queing of operations. + to support queuing of operations. */ result = ma_semaphore_wait(&pDevice->null_device.operationSemaphore); if (result != MA_SUCCESS) { @@ -21268,7 +21387,7 @@ static ma_result ma_context_get_device_info_from_IAudioClient__wasapi(ma_context } /* - Exlcusive Mode. We repeatedly call IsFormatSupported() here. This is not currently supported on + Exclusive Mode. We repeatedly call IsFormatSupported() here. This is not currently supported on UWP. Failure to retrieve the exclusive mode format is not considered an error, so from here on out, MA_SUCCESS is guaranteed to be returned. */ @@ -21473,8 +21592,23 @@ static ma_result ma_context_get_MMDevice__wasapi(ma_context* pContext, ma_device MA_ASSERT(pContext != NULL); MA_ASSERT(ppMMDevice != NULL); - hr = ma_CoCreateInstance(pContext, &MA_CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &MA_IID_IMMDeviceEnumerator, (void**)&pDeviceEnumerator); - if (FAILED(hr)) { + /* + This weird COM init/uninit here is a hack to work around a crash when changing devices. What is happening is + WASAPI fires a callback from another thread when the device is changed. It's from that thread where this + function is getting called. What I'm suspecting is that the other thread is not initializing COM which in turn + results in CoCreateInstance() failing. + + The community has reported that this seems to fix the crash. There are future plans to move all WASAPI operation + over to a single thread to make everything safer, but in the meantime while we wait for that to come online I'm + happy enough to use this hack instead. + */ + ma_CoInitializeEx(pContext, NULL, MA_COINIT_VALUE); + { + hr = ma_CoCreateInstance(pContext, &MA_CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &MA_IID_IMMDeviceEnumerator, (void**)&pDeviceEnumerator); + } + ma_CoUninitialize(pContext); + + if (FAILED(hr)) { /* <-- This is checking the call above to ma_CoCreateInstance(). */ ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[WASAPI] Failed to create IMMDeviceEnumerator.\n"); return ma_result_from_HRESULT(hr); } @@ -21506,7 +21640,7 @@ static ma_result ma_context_get_device_id_from_MMDevice__wasapi(ma_context* pCon size_t idlen = ma_strlen_WCHAR(pDeviceIDString); if (idlen+1 > ma_countof(pDeviceID->wasapi)) { ma_CoTaskMemFree(pContext, pDeviceIDString); - MA_ASSERT(MA_FALSE); /* NOTE: If this is triggered, please report it. It means the format of the ID must haved change and is too long to fit in our fixed sized buffer. */ + MA_ASSERT(MA_FALSE); /* NOTE: If this is triggered, please report it. It means the format of the ID must have changed and is too long to fit in our fixed sized buffer. */ return MA_ERROR; } @@ -21950,12 +22084,16 @@ static ma_result ma_device_uninit__wasapi(ma_device* pDevice) { MA_ASSERT(pDevice != NULL); -#if defined(MA_WIN32_DESKTOP) || defined(MA_WIN32_GDK) - if (pDevice->wasapi.pDeviceEnumerator) { - ((ma_IMMDeviceEnumerator*)pDevice->wasapi.pDeviceEnumerator)->lpVtbl->UnregisterEndpointNotificationCallback((ma_IMMDeviceEnumerator*)pDevice->wasapi.pDeviceEnumerator, &pDevice->wasapi.notificationClient); - ma_IMMDeviceEnumerator_Release((ma_IMMDeviceEnumerator*)pDevice->wasapi.pDeviceEnumerator); + #if defined(MA_WIN32_DESKTOP) || defined(MA_WIN32_GDK) + { + if (pDevice->wasapi.pDeviceEnumerator) { + ((ma_IMMDeviceEnumerator*)pDevice->wasapi.pDeviceEnumerator)->lpVtbl->UnregisterEndpointNotificationCallback((ma_IMMDeviceEnumerator*)pDevice->wasapi.pDeviceEnumerator, &pDevice->wasapi.notificationClient); + ma_IMMDeviceEnumerator_Release((ma_IMMDeviceEnumerator*)pDevice->wasapi.pDeviceEnumerator); + } + + ma_mutex_uninit(&pDevice->wasapi.rerouteLock); } -#endif + #endif if (pDevice->wasapi.pRenderClient) { if (pDevice->wasapi.pMappedBufferPlayback != NULL) { @@ -22256,7 +22394,7 @@ static ma_result ma_device_init_internal__wasapi(ma_context* pContext, ma_device MA_REFERENCE_TIME bufferDuration = periodDurationInMicroseconds * pData->periodsOut * 10; /* - If the periodicy is too small, Initialize() will fail with AUDCLNT_E_INVALID_DEVICE_PERIOD. In this case we should just keep increasing + If the periodicity is too small, Initialize() will fail with AUDCLNT_E_INVALID_DEVICE_PERIOD. In this case we should just keep increasing it and trying it again. */ hr = E_FAIL; @@ -22266,7 +22404,7 @@ static ma_result ma_device_init_internal__wasapi(ma_context* pContext, ma_device if (bufferDuration > 500*10000) { break; } else { - if (bufferDuration == 0) { /* <-- Just a sanity check to prevent an infinit loop. Should never happen, but it makes me feel better. */ + if (bufferDuration == 0) { /* <-- Just a sanity check to prevent an infinite loop. Should never happen, but it makes me feel better. */ break; } @@ -23005,6 +23143,14 @@ static ma_result ma_device_stop__wasapi_nolock(ma_device* pDevice) } if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_duplex || pDevice->type == ma_device_type_loopback) { + /* If we have a mapped buffer we need to release it. */ + if (pDevice->wasapi.pMappedBufferCapture != NULL) { + ma_IAudioCaptureClient_ReleaseBuffer((ma_IAudioCaptureClient*)pDevice->wasapi.pCaptureClient, pDevice->wasapi.mappedBufferCaptureCap); + pDevice->wasapi.pMappedBufferCapture = NULL; + pDevice->wasapi.mappedBufferCaptureCap = 0; + pDevice->wasapi.mappedBufferCaptureLen = 0; + } + hr = ma_IAudioClient_Stop((ma_IAudioClient*)pDevice->wasapi.pAudioClientCapture); if (FAILED(hr)) { ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "[WASAPI] Failed to stop internal capture device."); @@ -23018,31 +23164,34 @@ static ma_result ma_device_stop__wasapi_nolock(ma_device* pDevice) return ma_result_from_HRESULT(hr); } - /* If we have a mapped buffer we need to release it. */ - if (pDevice->wasapi.pMappedBufferCapture != NULL) { - ma_IAudioCaptureClient_ReleaseBuffer((ma_IAudioCaptureClient*)pDevice->wasapi.pCaptureClient, pDevice->wasapi.mappedBufferCaptureCap); - pDevice->wasapi.pMappedBufferCapture = NULL; - pDevice->wasapi.mappedBufferCaptureCap = 0; - pDevice->wasapi.mappedBufferCaptureLen = 0; - } - ma_atomic_bool32_set(&pDevice->wasapi.isStartedCapture, MA_FALSE); } if (pDevice->type == ma_device_type_playback || pDevice->type == ma_device_type_duplex) { + if (pDevice->wasapi.pMappedBufferPlayback != NULL) { + ma_silence_pcm_frames( + ma_offset_pcm_frames_ptr(pDevice->wasapi.pMappedBufferPlayback, pDevice->wasapi.mappedBufferPlaybackLen, pDevice->playback.internalFormat, pDevice->playback.internalChannels), + pDevice->wasapi.mappedBufferPlaybackCap - pDevice->wasapi.mappedBufferPlaybackLen, + pDevice->playback.internalFormat, pDevice->playback.internalChannels + ); + ma_IAudioRenderClient_ReleaseBuffer((ma_IAudioRenderClient*)pDevice->wasapi.pRenderClient, pDevice->wasapi.mappedBufferPlaybackCap, 0); + pDevice->wasapi.pMappedBufferPlayback = NULL; + pDevice->wasapi.mappedBufferPlaybackCap = 0; + pDevice->wasapi.mappedBufferPlaybackLen = 0; + } + /* The buffer needs to be drained before stopping the device. Not doing this will result in the last few frames not getting output to the speakers. This is a problem for very short sounds because it'll result in a significant portion of it not getting played. */ if (ma_atomic_bool32_get(&pDevice->wasapi.isStartedPlayback)) { /* We need to make sure we put a timeout here or else we'll risk getting stuck in a deadlock in some cases. */ - DWORD waitTime = pDevice->wasapi.actualBufferSizeInFramesPlayback / pDevice->playback.internalSampleRate; + DWORD waitTime = (pDevice->wasapi.actualBufferSizeInFramesPlayback * 1000) / pDevice->playback.internalSampleRate; if (pDevice->playback.shareMode == ma_share_mode_exclusive) { WaitForSingleObject((HANDLE)pDevice->wasapi.hEventPlayback, waitTime); - } - else { - ma_uint32 prevFramesAvaialablePlayback = (ma_uint32)-1; + } else { + ma_uint32 prevFramesAvailablePlayback = (ma_uint32)-1; ma_uint32 framesAvailablePlayback; for (;;) { result = ma_device__get_available_frames__wasapi(pDevice, (ma_IAudioClient*)pDevice->wasapi.pAudioClientPlayback, &framesAvailablePlayback); @@ -23058,13 +23207,13 @@ static ma_result ma_device_stop__wasapi_nolock(ma_device* pDevice) Just a safety check to avoid an infinite loop. If this iteration results in a situation where the number of available frames has not changed, get out of the loop. I don't think this should ever happen, but I think it's nice to have just in case. */ - if (framesAvailablePlayback == prevFramesAvaialablePlayback) { + if (framesAvailablePlayback == prevFramesAvailablePlayback) { break; } - prevFramesAvaialablePlayback = framesAvailablePlayback; + prevFramesAvailablePlayback = framesAvailablePlayback; - WaitForSingleObject((HANDLE)pDevice->wasapi.hEventPlayback, waitTime * 1000); ResetEvent((HANDLE)pDevice->wasapi.hEventPlayback); /* Manual reset. */ + WaitForSingleObject((HANDLE)pDevice->wasapi.hEventPlayback, waitTime); } } } @@ -23076,19 +23225,20 @@ static ma_result ma_device_stop__wasapi_nolock(ma_device* pDevice) } /* The audio client needs to be reset otherwise restarting will fail. */ - hr = ma_IAudioClient_Reset((ma_IAudioClient*)pDevice->wasapi.pAudioClientPlayback); + { + ma_int32 retries = 5; + + while ((hr = ma_IAudioClient_Reset((ma_IAudioClient*)pDevice->wasapi.pAudioClientPlayback)) == MA_AUDCLNT_E_BUFFER_OPERATION_PENDING && retries > 0) { + ma_sleep(10); + retries -= 1; + } + } + if (FAILED(hr)) { ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "[WASAPI] Failed to reset internal playback device."); return ma_result_from_HRESULT(hr); } - if (pDevice->wasapi.pMappedBufferPlayback != NULL) { - ma_IAudioRenderClient_ReleaseBuffer((ma_IAudioRenderClient*)pDevice->wasapi.pRenderClient, pDevice->wasapi.mappedBufferPlaybackCap, 0); - pDevice->wasapi.pMappedBufferPlayback = NULL; - pDevice->wasapi.mappedBufferPlaybackCap = 0; - pDevice->wasapi.mappedBufferPlaybackLen = 0; - } - ma_atomic_bool32_set(&pDevice->wasapi.isStartedPlayback, MA_FALSE); } @@ -23655,6 +23805,13 @@ DirectSound Backend #define MA_DSBPLAY_TERMINATEBY_DISTANCE 0x00000010 #define MA_DSBPLAY_TERMINATEBY_PRIORITY 0x00000020 +#define MA_DSBSTATUS_PLAYING 0x00000001 +#define MA_DSBSTATUS_BUFFERLOST 0x00000002 +#define MA_DSBSTATUS_LOOPING 0x00000004 +#define MA_DSBSTATUS_LOCHARDWARE 0x00000008 +#define MA_DSBSTATUS_LOCSOFTWARE 0x00000010 +#define MA_DSBSTATUS_TERMINATED 0x00000020 + #define MA_DSCBSTART_LOOPING 0x00000001 typedef struct @@ -24024,9 +24181,12 @@ static ma_result ma_context_create_IDirectSound__dsound(ma_context* pContext, ma } /* The cooperative level must be set before doing anything else. */ - hWnd = ((MA_PFN_GetForegroundWindow)pContext->win32.GetForegroundWindow)(); + hWnd = (HWND)pContext->dsound.hWnd; if (hWnd == 0) { - hWnd = ((MA_PFN_GetDesktopWindow)pContext->win32.GetDesktopWindow)(); + hWnd = ((MA_PFN_GetForegroundWindow)pContext->win32.GetForegroundWindow)(); + if (hWnd == 0) { + hWnd = ((MA_PFN_GetDesktopWindow)pContext->win32.GetDesktopWindow)(); + } } hr = ma_IDirectSound_SetCooperativeLevel(pDirectSound, hWnd, (shareMode == ma_share_mode_exclusive) ? MA_DSSCL_EXCLUSIVE : MA_DSSCL_PRIORITY); @@ -24530,8 +24690,8 @@ static ma_result ma_device_init__dsound(ma_device* pDevice, const ma_device_conf } /* - Unfortunately DirectSound uses different APIs and data structures for playback and catpure devices. We need to initialize - the capture device first because we'll want to match it's buffer size and period count on the playback side if we're using + Unfortunately DirectSound uses different APIs and data structures for playback and capture devices. We need to initialize + the capture device first because we'll want to match its buffer size and period count on the playback side if we're using full-duplex mode. */ if (pConfig->deviceType == ma_device_type_capture || pConfig->deviceType == ma_device_type_duplex) { @@ -24814,6 +24974,7 @@ static ma_result ma_device_data_loop__dsound(ma_device* pDevice) ma_bool32 isPlaybackDeviceStarted = MA_FALSE; ma_uint32 framesWrittenToPlaybackDevice = 0; /* For knowing whether or not the playback device needs to be started. */ ma_uint32 waitTimeInMilliseconds = 1; + DWORD playbackBufferStatus = 0; MA_ASSERT(pDevice != NULL); @@ -25142,6 +25303,20 @@ static ma_result ma_device_data_loop__dsound(ma_device* pDevice) break; } + hr = ma_IDirectSoundBuffer_GetStatus((ma_IDirectSoundBuffer*)pDevice->dsound.pPlaybackBuffer, &playbackBufferStatus); + if (SUCCEEDED(hr) && (playbackBufferStatus & MA_DSBSTATUS_PLAYING) == 0 && isPlaybackDeviceStarted) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, "[DirectSound] Attempting to resume audio due to state: %d.", (int)playbackBufferStatus); + hr = ma_IDirectSoundBuffer_Play((ma_IDirectSoundBuffer*)pDevice->dsound.pPlaybackBuffer, 0, 0, MA_DSBPLAY_LOOPING); + if (FAILED(hr)) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "[DirectSound] IDirectSoundBuffer_Play() failed after attempting to resume from state %d.", (int)playbackBufferStatus); + return ma_result_from_HRESULT(hr); + } + + isPlaybackDeviceStarted = MA_TRUE; + ma_sleep(waitTimeInMilliseconds); + continue; + } + if (physicalPlayCursorInBytes < prevPlayCursorInBytesPlayback) { physicalPlayCursorLoopFlagPlayback = !physicalPlayCursorLoopFlagPlayback; } @@ -25343,6 +25518,8 @@ static ma_result ma_context_init__dsound(ma_context* pContext, const ma_context_ return MA_API_NOT_FOUND; } + pContext->dsound.hWnd = pConfig->dsound.hWnd; + pCallbacks->onContextInit = ma_context_init__dsound; pCallbacks->onContextUninit = ma_context_uninit__dsound; pCallbacks->onContextEnumerateDevices = ma_context_enumerate_devices__dsound; @@ -25665,7 +25842,7 @@ static ma_result ma_context_get_device_info_from_WAVECAPS(ma_context* pContext, - If the name GUID is not present in the registry we'll also need to stick to the original 31 characters. - I like consistency, so I want the returned device names to be consistent with those returned by WASAPI and DirectSound. The problem, however is that WASAPI and DirectSound use " ()" format (such as "Speakers (High Definition Audio)"), - but WinMM does not specificy the component name. From my admittedly limited testing, I've notice the component name seems to + but WinMM does not specify the component name. From my admittedly limited testing, I've notice the component name seems to usually fit within the 31 characters of the fixed sized buffer, so what I'm going to do is parse that string for the component name, and then concatenate the name from the registry. */ @@ -25933,7 +26110,7 @@ static ma_result ma_device_init__winmm(ma_device* pDevice, const ma_device_confi return MA_DEVICE_TYPE_NOT_SUPPORTED; } - /* No exlusive mode with WinMM. */ + /* No exclusive mode with WinMM. */ if (((pConfig->deviceType == ma_device_type_playback || pConfig->deviceType == ma_device_type_duplex) && pDescriptorPlayback->shareMode == ma_share_mode_exclusive) || ((pConfig->deviceType == ma_device_type_capture || pConfig->deviceType == ma_device_type_duplex) && pDescriptorCapture->shareMode == ma_share_mode_exclusive)) { return MA_SHARE_MODE_NOT_SUPPORTED; @@ -25955,7 +26132,7 @@ static ma_result ma_device_init__winmm(ma_device* pDevice, const ma_device_confi /* We use an event to know when a new fragment needs to be enqueued. */ pDevice->winmm.hEventCapture = (ma_handle)CreateEventA(NULL, TRUE, TRUE, NULL); if (pDevice->winmm.hEventCapture == NULL) { - errorMsg = "[WinMM] Failed to create event for fragment enqueing for the capture device.", errorCode = ma_result_from_GetLastError(GetLastError()); + errorMsg = "[WinMM] Failed to create event for fragment enqueuing for the capture device.", errorCode = ma_result_from_GetLastError(GetLastError()); goto on_error; } @@ -25993,7 +26170,7 @@ static ma_result ma_device_init__winmm(ma_device* pDevice, const ma_device_confi /* We use an event to know when a new fragment needs to be enqueued. */ pDevice->winmm.hEventPlayback = (ma_handle)CreateEventA(NULL, TRUE, TRUE, NULL); if (pDevice->winmm.hEventPlayback == NULL) { - errorMsg = "[WinMM] Failed to create event for fragment enqueing for the playback device.", errorCode = ma_result_from_GetLastError(GetLastError()); + errorMsg = "[WinMM] Failed to create event for fragment enqueuing for the playback device.", errorCode = ma_result_from_GetLastError(GetLastError()); goto on_error; } @@ -27115,7 +27292,7 @@ static ma_result ma_context_open_pcm__alsa(ma_context* pContext, ma_share_mode s /* We're trying to open a specific device. There's a few things to consider here: - miniaudio recongnizes a special format of device id that excludes the "hw", "dmix", etc. prefix. It looks like this: ":0,0", ":0,1", etc. When + miniaudio recognizes a special format of device id that excludes the "hw", "dmix", etc. prefix. It looks like this: ":0,0", ":0,1", etc. When an ID of this format is specified, it indicates to miniaudio that it can try different combinations of plugins ("hw", "dmix", etc.) until it finds an appropriate one that works. This comes in very handy when trying to open a device in shared mode ("dmix"), vs exclusive mode ("hw"). */ @@ -27214,7 +27391,7 @@ static ma_result ma_context_enumerate_devices__alsa(ma_context* pContext, ma_enu /* At this point, hwid looks like "hw:0,0". In simplified enumeration mode, we actually want to strip off the plugin name so it looks like ":0,0". The reason for this is that this special format is detected at device - initialization time and is used as an indicator to try and use the most appropriate plugin depending on the + initialization time and is used as an indicator to try to use the most appropriate plugin depending on the device type and sharing mode. */ char* dst = hwid; @@ -27393,7 +27570,7 @@ static void ma_context_iterate_rates_and_add_native_data_format__alsa(ma_context ((ma_snd_pcm_hw_params_get_rate_min_proc)pContext->alsa.snd_pcm_hw_params_get_rate_min)(pHWParams, &minSampleRate, &sampleRateDir); ((ma_snd_pcm_hw_params_get_rate_max_proc)pContext->alsa.snd_pcm_hw_params_get_rate_max)(pHWParams, &maxSampleRate, &sampleRateDir); - /* Make sure our sample rates are clamped to sane values. Stupid devices like "pulse" will reports rates like "1" which is ridiculus. */ + /* Make sure our sample rates are clamped to sane values. Stupid devices like "pulse" will reports rates like "1" which is ridiculous. */ minSampleRate = ma_clamp(minSampleRate, (unsigned int)ma_standard_sample_rate_min, (unsigned int)ma_standard_sample_rate_max); maxSampleRate = ma_clamp(maxSampleRate, (unsigned int)ma_standard_sample_rate_min, (unsigned int)ma_standard_sample_rate_max); @@ -27469,10 +27646,10 @@ static ma_result ma_context_get_device_info__alsa(ma_context* pContext, ma_devic /* Some ALSA devices can support many permutations of formats, channels and rates. We only support a fixed number of permutations which means we need to employ some strategies to ensure the best - combinations are returned. An example is the "pulse" device which can do it's own data conversion + combinations are returned. An example is the "pulse" device which can do its own data conversion in software and as a result can support any combination of format, channels and rate. - We want to ensure the the first data formats are the best. We have a list of favored sample + We want to ensure that the first data formats are the best. We have a list of favored sample formats and sample rates, so these will be the basis of our iteration. */ @@ -28050,7 +28227,21 @@ static ma_result ma_device_start__alsa(ma_device* pDevice) } if (pDevice->type == ma_device_type_playback || pDevice->type == ma_device_type_duplex) { - /* Don't need to do anything for playback because it'll be started automatically when enough data has been written. */ + /* + When data is written to the device we wait for the device to get ready to receive data with poll(). In my testing + I have observed that poll() can sometimes block forever unless the device is started explicitly with snd_pcm_start() + or some data is written with snd_pcm_writei(). + + To resolve this I've decided to do an explicit start with snd_pcm_start(). The problem with this is that the device + is started without any data in the internal buffer which will result in an immediate underrun. If instead we were + to call into snd_pcm_writei() in an attempt to prevent the underrun, we would run the risk of a weird deadlock + issue as documented inside ma_device_write__alsa(). + */ + resultALSA = ((ma_snd_pcm_start_proc)pDevice->pContext->alsa.snd_pcm_start)((ma_snd_pcm_t*)pDevice->alsa.pPCMPlayback); + if (resultALSA < 0) { + ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "[ALSA] Failed to start playback device."); + return ma_result_from_errno(-resultALSA); + } } return MA_SUCCESS; @@ -28063,6 +28254,7 @@ static ma_result ma_device_stop__alsa(ma_device* pDevice) a small chance that our wakeupfd has not been cleared. We'll clear that out now if applicable. */ int resultPoll; + int resultRead; if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_duplex) { ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[ALSA] Dropping capture device...\n"); @@ -28077,12 +28269,15 @@ static ma_result ma_device_stop__alsa(ma_device* pDevice) ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[ALSA] Preparing capture device successful.\n"); } - /* Clear the wakeupfd. */ - resultPoll = poll((struct pollfd*)pDevice->alsa.pPollDescriptorsCapture, 1, 0); - if (resultPoll > 0) { - ma_uint64 t; - read(((struct pollfd*)pDevice->alsa.pPollDescriptorsCapture)[0].fd, &t, sizeof(t)); - } + /* Clear the wakeupfd. */ + resultPoll = poll((struct pollfd*)pDevice->alsa.pPollDescriptorsCapture, 1, 0); + if (resultPoll > 0) { + ma_uint64 t; + resultRead = read(((struct pollfd*)pDevice->alsa.pPollDescriptorsCapture)[0].fd, &t, sizeof(t)); + if (resultRead != sizeof(t)) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[ALSA] Failed to read from capture wakeupfd. read() = %d\n", resultRead); + } + } } if (pDevice->type == ma_device_type_playback || pDevice->type == ma_device_type_duplex) { @@ -28099,12 +28294,14 @@ static ma_result ma_device_stop__alsa(ma_device* pDevice) } /* Clear the wakeupfd. */ - resultPoll = poll((struct pollfd*)pDevice->alsa.pPollDescriptorsPlayback, 1, 0); - if (resultPoll > 0) { - ma_uint64 t; - read(((struct pollfd*)pDevice->alsa.pPollDescriptorsPlayback)[0].fd, &t, sizeof(t)); - } - + resultPoll = poll((struct pollfd*)pDevice->alsa.pPollDescriptorsPlayback, 1, 0); + if (resultPoll > 0) { + ma_uint64 t; + resultRead = read(((struct pollfd*)pDevice->alsa.pPollDescriptorsPlayback)[0].fd, &t, sizeof(t)); + if (resultRead != sizeof(t)) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[ALSA] Failed to read from playback wakeupfd. read() = %d\n", resultRead); + } + } } return MA_SUCCESS; @@ -28117,13 +28314,20 @@ static ma_result ma_device_wait__alsa(ma_device* pDevice, ma_snd_pcm_t* pPCM, st int resultALSA; int resultPoll = poll(pPollDescriptors, pollDescriptorCount, -1); if (resultPoll < 0) { - ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "[ALSA] poll() failed.\n"); - return ma_result_from_errno(errno); + ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_WARNING, "[ALSA] poll() failed.\n"); + + /* + There have been reports that poll() is returning an error randomly and that instead of + returning an error, simply trying again will work. I'm experimenting with adopting this + advice. + */ + continue; + /*return ma_result_from_errno(errno);*/ } /* Before checking the ALSA poll descriptor flag we need to check if the wakeup descriptor - has had it's POLLIN flag set. If so, we need to actually read the data and then exit + has had it's POLLIN flag set. If so, we need to actually read the data and then exit the function. The wakeup descriptor will be the first item in the descriptors buffer. */ if ((pPollDescriptors[0].revents & POLLIN) != 0) { @@ -28152,7 +28356,7 @@ static ma_result ma_device_wait__alsa(ma_device* pDevice, ma_snd_pcm_t* pPCM, st ma_snd_pcm_state_t state = ((ma_snd_pcm_state_proc)pDevice->pContext->alsa.snd_pcm_state)(pPCM); if (state == MA_SND_PCM_STATE_XRUN) { /* The PCM is in a xrun state. This will be recovered from at a higher level. We can disregard this. */ - } else { + } else { ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_WARNING, "[ALSA] POLLERR detected. status = %d\n", ((ma_snd_pcm_state_proc)pDevice->pContext->alsa.snd_pcm_state)(pPCM)); } } @@ -28585,7 +28789,7 @@ static ma_result ma_context_init__alsa(ma_context* pContext, const ma_context_co return MA_SUCCESS; } -#endif /* ALSA */ +#endif /* MA_HAS_ALSA */ @@ -28596,7 +28800,7 @@ PulseAudio Backend ******************************************************************************/ #ifdef MA_HAS_PULSEAUDIO /* -The PulseAudio API, along with Apple's Core Audio, is the worst of the maintream audio APIs. This is a brief description of what's going on +The PulseAudio API, along with Apple's Core Audio, is the worst of the mainstream audio APIs. This is a brief description of what's going on in the PulseAudio backend. I apologize if this gets a bit ranty for your liking - you might want to skip this discussion. PulseAudio has something they call the "Simple API", which unfortunately isn't suitable for miniaudio. I've not seen anywhere where it @@ -28611,7 +28815,7 @@ get fun, and I don't mean that in a good way... The problems start with the very name of the API - "asynchronous". Yes, this is an asynchronous oriented API which means your commands don't immediately take effect. You instead need to issue your commands, and then wait for them to complete. The waiting mechanism is -enabled through the use of a "main loop". In the asychronous API you cannot get away from the main loop, and the main loop is where almost +enabled through the use of a "main loop". In the asynchronous API you cannot get away from the main loop, and the main loop is where almost all of PulseAudio's problems stem from. When you first initialize PulseAudio you need an object referred to as "main loop". You can implement this yourself by defining your own @@ -28661,7 +28865,7 @@ because PulseAudio takes it literally, specifically the "can be". You would thin writing and reading data to and from the stream, and that would be right, except when it's not. When you initialize the stream, you can set a flag that tells PulseAudio to not start the stream automatically. This is required because miniaudio does not auto-start devices straight after initialization - you need to call `ma_device_start()` manually. The problem is that even when this flag is specified, -PulseAudio will immediately fire it's write or read callback. This is *technically* correct (based on the wording in the documentation) +PulseAudio will immediately fire its write or read callback. This is *technically* correct (based on the wording in the documentation) because indeed, data *can* be written at this point. The problem is that it's not *practical*. It makes sense that the write/read callback would be where a program will want to write or read data to or from the stream, but when it's called before the application has even requested that the stream be started, it's just not practical because the program probably isn't ready for any kind of data delivery at @@ -30039,16 +30243,18 @@ static ma_pa_buffer_attr ma_device__pa_buffer_attr_new(ma_uint32 periodSizeInFra static ma_pa_stream* ma_device__pa_stream_new__pulse(ma_device* pDevice, const char* pStreamName, const ma_pa_sample_spec* ss, const ma_pa_channel_map* cmap) { - static int g_StreamCounter = 0; + static ma_atomic_uint32 g_StreamCounter = { 0 }; char actualStreamName[256]; if (pStreamName != NULL) { ma_strncpy_s(actualStreamName, sizeof(actualStreamName), pStreamName, (size_t)-1); } else { - ma_strcpy_s(actualStreamName, sizeof(actualStreamName), "miniaudio:"); - ma_itoa_s(g_StreamCounter, actualStreamName + 8, sizeof(actualStreamName)-8, 10); /* 8 = strlen("miniaudio:") */ + const char* pBaseName = "miniaudio:"; + size_t baseNameLen = strlen(pBaseName); + ma_strcpy_s(actualStreamName, sizeof(actualStreamName), pBaseName); + ma_itoa_s((int)ma_atomic_uint32_get(&g_StreamCounter), actualStreamName + baseNameLen, sizeof(actualStreamName)-baseNameLen, 10); } - g_StreamCounter += 1; + ma_atomic_uint32_fetch_add(&g_StreamCounter, 1); return ((ma_pa_stream_new_proc)pDevice->pContext->pulse.pa_stream_new)((ma_pa_context*)pDevice->pulse.pPulseContext, actualStreamName, ss, cmap); } @@ -30302,6 +30508,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi ma_pa_buffer_attr attr; const ma_pa_sample_spec* pActualSS = NULL; const ma_pa_buffer_attr* pActualAttr = NULL; + const ma_pa_channel_map* pActualChannelMap = NULL; ma_uint32 iChannel; ma_pa_stream_flags_t streamFlags; @@ -30362,7 +30569,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi } /* Use a default channel map. */ - ((ma_pa_channel_map_init_extend_proc)pDevice->pContext->pulse.pa_channel_map_init_extend)(&cmap, ss.channels, MA_PA_CHANNEL_MAP_DEFAULT); + ((ma_pa_channel_map_init_extend_proc)pDevice->pContext->pulse.pa_channel_map_init_extend)(&cmap, ss.channels, pConfig->pulse.channelMap); /* Use the requested sample rate if one was specified. */ if (pDescriptorCapture->sampleRate != 0) { @@ -30451,7 +30658,12 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi goto on_error4; } + /* Internal channel map. */ + pActualChannelMap = ((ma_pa_stream_get_channel_map_proc)pDevice->pContext->pulse.pa_stream_get_channel_map)((ma_pa_stream*)pDevice->pulse.pStreamCapture); + if (pActualChannelMap == NULL) { + pActualChannelMap = &cmap; /* Fallback just in case. */ + } /* Bug in PipeWire. There have been reports that PipeWire is returning AUX channels when reporting @@ -30461,8 +30673,8 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi fixed sooner than later. I might remove this hack later. */ if (pDescriptorCapture->channels > 2) { - for (iChannel = 0; iChannel < pDescriptorCapture->channels; ++iChannel) { - pDescriptorCapture->channelMap[iChannel] = ma_channel_position_from_pulse(cmap.map[iChannel]); + for (iChannel = 0; iChannel < pDescriptorCapture->channels; iChannel += 1) { + pDescriptorCapture->channelMap[iChannel] = ma_channel_position_from_pulse(pActualChannelMap->map[iChannel]); } } else { /* Hack for mono and stereo. */ @@ -30509,7 +30721,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi } /* Use a default channel map. */ - ((ma_pa_channel_map_init_extend_proc)pDevice->pContext->pulse.pa_channel_map_init_extend)(&cmap, ss.channels, MA_PA_CHANNEL_MAP_DEFAULT); + ((ma_pa_channel_map_init_extend_proc)pDevice->pContext->pulse.pa_channel_map_init_extend)(&cmap, ss.channels, pConfig->pulse.channelMap); /* Use the requested sample rate if one was specified. */ @@ -30603,7 +30815,12 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi goto on_error4; } + /* Internal channel map. */ + pActualChannelMap = ((ma_pa_stream_get_channel_map_proc)pDevice->pContext->pulse.pa_stream_get_channel_map)((ma_pa_stream*)pDevice->pulse.pStreamPlayback); + if (pActualChannelMap == NULL) { + pActualChannelMap = &cmap; /* Fallback just in case. */ + } /* Bug in PipeWire. There have been reports that PipeWire is returning AUX channels when reporting @@ -30613,8 +30830,8 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi fixed sooner than later. I might remove this hack later. */ if (pDescriptorPlayback->channels > 2) { - for (iChannel = 0; iChannel < pDescriptorPlayback->channels; ++iChannel) { - pDescriptorPlayback->channelMap[iChannel] = ma_channel_position_from_pulse(cmap.map[iChannel]); + for (iChannel = 0; iChannel < pDescriptorPlayback->channels; iChannel += 1) { + pDescriptorPlayback->channelMap[iChannel] = ma_channel_position_from_pulse(pActualChannelMap->map[iChannel]); } } else { /* Hack for mono and stereo. */ @@ -31767,7 +31984,7 @@ static ma_result ma_context_init__jack(ma_context* pContext, const ma_context_co return MA_SUCCESS; } -#endif /* JACK */ +#endif /* MA_HAS_JACK */ @@ -31858,7 +32075,7 @@ that supports this level of detail. There was some public domain sample code I s and AudioUnit APIs, but I couldn't see anything that gave low-level control over device selection and capabilities (the distinction between playback and capture in particular). Therefore, miniaudio is using the AudioObject API. -Most (all?) functions in the AudioObject API take a AudioObjectID as it's input. This is the device identifier. When +Most (all?) functions in the AudioObject API take a AudioObjectID as its input. This is the device identifier. When retrieving global information, such as the device list, you use kAudioObjectSystemObject. When retrieving device-specific data, you pass in the ID for that device. In order to retrieve device-specific IDs you need to enumerate over each of the devices. This is done using the AudioObjectGetPropertyDataSize() and AudioObjectGetPropertyData() APIs which seem to be @@ -32193,6 +32410,12 @@ static ma_result ma_get_channel_map_from_AudioChannelLayout(AudioChannelLayout* #define AUDIO_OBJECT_PROPERTY_ELEMENT kAudioObjectPropertyElementMaster #endif +/* kAudioDevicePropertyScope* were renamed to kAudioObjectPropertyScope* in 10.8. */ +#if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8) +#define kAudioObjectPropertyScopeInput kAudioDevicePropertyScopeInput +#define kAudioObjectPropertyScopeOutput kAudioDevicePropertyScopeOutput +#endif + static ma_result ma_get_device_object_ids__coreaudio(ma_context* pContext, UInt32* pDeviceCount, AudioObjectID** ppDeviceObjectIDs) /* NOTE: Free the returned buffer with ma_free(). */ { AudioObjectPropertyAddress propAddressDevices; @@ -32782,7 +33005,7 @@ static ma_result ma_find_best_format__coreaudio(ma_context* pContext, AudioObjec desiredSampleRate = sampleRate; if (desiredSampleRate == 0) { - desiredSampleRate = pOrigFormat->mSampleRate; + desiredSampleRate = (ma_uint32)pOrigFormat->mSampleRate; } desiredChannelCount = channels; @@ -33425,7 +33648,7 @@ static OSStatus ma_on_output__coreaudio(void* pUserData, AudioUnitRenderActionFl } } else { /* This is the deinterleaved case. We need to update each buffer in groups of internalChannels. This assumes each buffer is the same size. */ - MA_ASSERT(pDevice->playback.internalChannels <= MA_MAX_CHANNELS); /* This should heve been validated at initialization time. */ + MA_ASSERT(pDevice->playback.internalChannels <= MA_MAX_CHANNELS); /* This should have been validated at initialization time. */ /* For safety we'll check that the internal channels is a multiple of the buffer count. If it's not it means something @@ -33516,11 +33739,12 @@ static OSStatus ma_on_input__coreaudio(void* pUserData, AudioUnitRenderActionFla */ for (iBuffer = 0; iBuffer < pRenderedBufferList->mNumberBuffers; ++iBuffer) { pRenderedBufferList->mBuffers[iBuffer].mDataByteSize = pDevice->coreaudio.audioBufferCapInFrames * ma_get_bytes_per_sample(pDevice->capture.internalFormat) * pRenderedBufferList->mBuffers[iBuffer].mNumberChannels; + /*printf("DEBUG: nDataByteSize = %d\n", (int)pRenderedBufferList->mBuffers[iBuffer].mDataByteSize);*/ } status = ((ma_AudioUnitRender_proc)pDevice->pContext->coreaudio.AudioUnitRender)((AudioUnit)pDevice->coreaudio.audioUnitCapture, pActionFlags, pTimeStamp, busNumber, frameCount, pRenderedBufferList); if (status != noErr) { - ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, " ERROR: AudioUnitRender() failed with %d.\n", (int)status); + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "ERROR: AudioUnitRender() failed with %d.\n", (int)status); return status; } @@ -33756,7 +33980,7 @@ static ma_result ma_context__init_device_tracking__coreaudio(ma_context* pContex ma_spinlock_lock(&g_DeviceTrackingInitLock_CoreAudio); { - /* Don't do anything if we've already initializd device tracking. */ + /* Don't do anything if we've already initialized device tracking. */ if (g_DeviceTrackingInitCounter_CoreAudio == 0) { AudioObjectPropertyAddress propAddress; propAddress.mScope = kAudioObjectPropertyScopeGlobal; @@ -34068,11 +34292,11 @@ typedef struct static ma_result ma_device_init_internal__coreaudio(ma_context* pContext, ma_device_type deviceType, const ma_device_id* pDeviceID, ma_device_init_internal_data__coreaudio* pData, void* pDevice_DoNotReference) /* <-- pDevice is typed as void* intentionally so as to avoid accidentally referencing it. */ { - ma_result result; + ma_result result = MA_SUCCESS; OSStatus status; UInt32 enableIOFlag; AudioStreamBasicDescription bestFormat; - UInt32 actualPeriodSizeInFrames; + ma_uint32 actualPeriodSizeInFrames; AURenderCallbackStruct callbackInfo; #if defined(MA_APPLE_DESKTOP) AudioObjectID deviceObjectID; @@ -34224,7 +34448,7 @@ static ma_result ma_device_init_internal__coreaudio(ma_context* pContext, ma_dev returning a result code of -10863. I have also tried changing the format directly on the input scope on the input bus, but this just results in `ca_require: IsStreamFormatWritable(inScope, inElement) NotWritable` when trying to set the format. - Something that does seem to work, however, has been setting the nominal sample rate on the deivce object. The problem with + Something that does seem to work, however, has been setting the nominal sample rate on the device object. The problem with this, however, is that it actually changes the sample rate at the operating system level and not just the application. This could be intrusive to the user, however, so I don't think it's wise to make this the default. Instead I'm making this a configuration option. When the `coreaudio.allowNominalSampleRateChange` config option is set to true, changing the sample @@ -34275,15 +34499,28 @@ static ma_result ma_device_init_internal__coreaudio(ma_context* pContext, ma_dev /* I've had a report that the channel count returned by AudioUnitGetProperty above is inconsistent with AVAudioSession outputNumberOfChannels. I'm going to try using the AVAudioSession values instead. + + UPDATE 20/02/2025: + When testing on the simulator with an iPhone 15 and iOS 17 I get an error when initializing the audio + unit if set the input channels to pAudioSession.inputNumberOfChannels. What is happening is the channel + count returned from AudioUnitGetProperty() above is set to 2, but pAudioSession is reporting a channel + count of 1. When this happens, the call to AudioUnitSetProprty() below just down below will succeed, but + AudioUnitInitialize() further down will fail. The only solution I have come up with is to not set the + channel count to pAudioSession.inputNumberOfChannels. */ if (deviceType == ma_device_type_playback) { bestFormat.mChannelsPerFrame = (UInt32)pAudioSession.outputNumberOfChannels; } + + #if 0 if (deviceType == ma_device_type_capture) { + /*printf("DEBUG: bestFormat.mChannelsPerFrame = %d; pAudioSession.inputNumberOfChannels = %d\n", (int)bestFormat.mChannelsPerFrame, (int)pAudioSession.inputNumberOfChannels);*/ bestFormat.mChannelsPerFrame = (UInt32)pAudioSession.inputNumberOfChannels; } + #endif } + status = ((ma_AudioUnitSetProperty_proc)pContext->coreaudio.AudioUnitSetProperty)(pData->audioUnit, kAudioUnitProperty_StreamFormat, formatScope, formatElement, &bestFormat, sizeof(bestFormat)); if (status != noErr) { ((ma_AudioComponentInstanceDispose_proc)pContext->coreaudio.AudioComponentInstanceDispose)(pData->audioUnit); @@ -34303,7 +34540,7 @@ static ma_result ma_device_init_internal__coreaudio(ma_context* pContext, ma_dev } pData->channelsOut = bestFormat.mChannelsPerFrame; - pData->sampleRateOut = bestFormat.mSampleRate; + pData->sampleRateOut = (ma_uint32)bestFormat.mSampleRate; } /* Clamp the channel count for safety. */ @@ -34610,7 +34847,7 @@ static ma_result ma_device_init__coreaudio(ma_device* pDevice, const ma_device_c ma_get_AudioObject_uid(pDevice->pContext, pDevice->coreaudio.deviceObjectIDCapture, sizeof(pDevice->capture.id.coreaudio), pDevice->capture.id.coreaudio); /* - If we are using the default device we'll need to listen for changes to the system's default device so we can seemlessly + If we are using the default device we'll need to listen for changes to the system's default device so we can seamlessly switch the device in the background. */ if (pConfig->capture.pDeviceID == NULL) { @@ -34674,7 +34911,7 @@ static ma_result ma_device_init__coreaudio(ma_device* pDevice, const ma_device_c ma_get_AudioObject_uid(pDevice->pContext, pDevice->coreaudio.deviceObjectIDPlayback, sizeof(pDevice->playback.id.coreaudio), pDevice->playback.id.coreaudio); /* - If we are using the default device we'll need to listen for changes to the system's default device so we can seemlessly + If we are using the default device we'll need to listen for changes to the system's default device so we can seamlessly switch the device in the background. */ if (pDescriptorPlayback->pDeviceID == NULL && (pConfig->deviceType != ma_device_type_duplex || pDescriptorCapture->pDeviceID != NULL)) { @@ -34992,7 +35229,7 @@ static ma_result ma_context_init__coreaudio(ma_context* pContext, const ma_conte return MA_SUCCESS; } -#endif /* Core Audio */ +#endif /* MA_HAS_COREAUDIO */ @@ -35484,7 +35721,7 @@ static ma_result ma_device_uninit__sndio(ma_device* pDevice) ((ma_sio_close_proc)pDevice->pContext->sndio.sio_close)((struct ma_sio_hdl*)pDevice->sndio.handleCapture); } - if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_duplex) { + if (pDevice->type == ma_device_type_playback || pDevice->type == ma_device_type_duplex) { ((ma_sio_close_proc)pDevice->pContext->sndio.sio_close)((struct ma_sio_hdl*)pDevice->sndio.handlePlayback); } @@ -35839,7 +36076,7 @@ static ma_result ma_context_init__sndio(ma_context* pContext, const ma_context_c (void)pConfig; return MA_SUCCESS; } -#endif /* sndio */ +#endif /* MA_HAS_SNDIO */ @@ -35857,6 +36094,10 @@ audio(4) Backend #include #include +#ifdef __NetBSD__ +#include +#endif + #if defined(__OpenBSD__) #include #if defined(OpenBSD) && OpenBSD >= 201709 @@ -36076,9 +36317,15 @@ static ma_result ma_context_get_device_info_from_fd__audio4(ma_context* pContext ma_uint32 channels; ma_uint32 sampleRate; +#if defined(__NetBSD__) && (__NetBSD_Version__ >= 900000000) + if (ioctl(fd, AUDIO_GETFORMAT, &fdInfo) < 0) { + return MA_ERROR; + } +#else if (ioctl(fd, AUDIO_GETINFO, &fdInfo) < 0) { return MA_ERROR; } +#endif if (deviceType == ma_device_type_playback) { channels = fdInfo.play.channels; @@ -36356,7 +36603,11 @@ static ma_result ma_device_init_fd__audio4(ma_device* pDevice, const ma_device_c /* We're using a default device. Get the info from the /dev/audioctl file instead of /dev/audio. */ int fdctl = open(pDefaultDeviceCtlNames[iDefaultDevice], fdFlags, 0); if (fdctl != -1) { +#if defined(__NetBSD__) && (__NetBSD_Version__ >= 900000000) + fdInfoResult = ioctl(fdctl, AUDIO_GETFORMAT, &fdInfo); +#else fdInfoResult = ioctl(fdctl, AUDIO_GETINFO, &fdInfo); +#endif close(fdctl); } } @@ -36723,7 +36974,7 @@ static ma_result ma_context_init__audio4(ma_context* pContext, const ma_context_ return MA_SUCCESS; } -#endif /* audio4 */ +#endif /* MA_HAS_AUDIO4 */ /****************************************************************************** @@ -37086,7 +37337,7 @@ static ma_result ma_device_init_fd__oss(ma_device* pDevice, const ma_device_conf } /* - The OSS documantation is very clear about the order we should be initializing the device's properties: + The OSS documentation is very clear about the order we should be initializing the device's properties: 1) Format 2) Channels 3) Sample rate. @@ -37354,7 +37605,7 @@ static ma_result ma_context_init__oss(ma_context* pContext, const ma_context_con return MA_SUCCESS; } -#endif /* OSS */ +#endif /* MA_HAS_OSS */ @@ -37367,7 +37618,9 @@ AAudio Backend ******************************************************************************/ #ifdef MA_HAS_AAUDIO -/*#include */ +#ifdef MA_NO_RUNTIME_LINKING + #include +#endif typedef int32_t ma_aaudio_result_t; typedef int32_t ma_aaudio_direction_t; @@ -37580,9 +37833,7 @@ static void ma_stream_error_callback__aaudio(ma_AAudioStream* pStream, void* pUs MA_ASSERT(pDevice != NULL); (void)error; - ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, "[AAudio] ERROR CALLBACK: error=%d, AAudioStream_getState()=%d\n", error, ((MA_PFN_AAudioStream_getState)pDevice->pContext->aaudio.AAudioStream_getState)(pStream)); - /* When we get an error, we'll assume that the stream is in an erroneous state and needs to be restarted. From the documentation, we cannot do this from the error callback. Therefore we are going to use an event thread for the AAudio backend to do this @@ -37610,7 +37861,9 @@ static ma_aaudio_data_callback_result_t ma_stream_data_callback_capture__aaudio( ma_device* pDevice = (ma_device*)pUserData; MA_ASSERT(pDevice != NULL); - ma_device_handle_backend_data_callback(pDevice, NULL, pAudioData, frameCount); + if (frameCount > 0) { + ma_device_handle_backend_data_callback(pDevice, NULL, pAudioData, (ma_uint32)frameCount); + } (void)pStream; return MA_AAUDIO_CALLBACK_RESULT_CONTINUE; @@ -37621,7 +37874,14 @@ static ma_aaudio_data_callback_result_t ma_stream_data_callback_playback__aaudio ma_device* pDevice = (ma_device*)pUserData; MA_ASSERT(pDevice != NULL); - ma_device_handle_backend_data_callback(pDevice, pAudioData, NULL, frameCount); + /* + I've had a report that AAudio can sometimes post a frame count of 0. We need to check for that here + so we don't get any errors at a deeper level. I'm doing the same with the capture side for safety, + though I've not yet had any reports about that one. + */ + if (frameCount > 0) { + ma_device_handle_backend_data_callback(pDevice, pAudioData, NULL, (ma_uint32)frameCount); + } (void)pStream; return MA_AAUDIO_CALLBACK_RESULT_CONTINUE; @@ -37656,32 +37916,25 @@ static ma_result ma_create_and_configure_AAudioStreamBuilder__aaudio(ma_context* ((MA_PFN_AAudioStreamBuilder_setSampleRate)pContext->aaudio.AAudioStreamBuilder_setSampleRate)(pBuilder, pDescriptor->sampleRate); } - if (deviceType == ma_device_type_capture) { - if (pDescriptor->channels != 0) { - ((MA_PFN_AAudioStreamBuilder_setChannelCount)pContext->aaudio.AAudioStreamBuilder_setChannelCount)(pBuilder, pDescriptor->channels); - } - if (pDescriptor->format != ma_format_unknown) { - ((MA_PFN_AAudioStreamBuilder_setFormat)pContext->aaudio.AAudioStreamBuilder_setFormat)(pBuilder, (pDescriptor->format == ma_format_s16) ? MA_AAUDIO_FORMAT_PCM_I16 : MA_AAUDIO_FORMAT_PCM_FLOAT); - } - } else { - if (pDescriptor->channels != 0) { - ((MA_PFN_AAudioStreamBuilder_setChannelCount)pContext->aaudio.AAudioStreamBuilder_setChannelCount)(pBuilder, pDescriptor->channels); - } - if (pDescriptor->format != ma_format_unknown) { - ((MA_PFN_AAudioStreamBuilder_setFormat)pContext->aaudio.AAudioStreamBuilder_setFormat)(pBuilder, (pDescriptor->format == ma_format_s16) ? MA_AAUDIO_FORMAT_PCM_I16 : MA_AAUDIO_FORMAT_PCM_FLOAT); - } + if (pDescriptor->channels != 0) { + ((MA_PFN_AAudioStreamBuilder_setChannelCount)pContext->aaudio.AAudioStreamBuilder_setChannelCount)(pBuilder, pDescriptor->channels); + } + + if (pDescriptor->format != ma_format_unknown) { + ((MA_PFN_AAudioStreamBuilder_setFormat)pContext->aaudio.AAudioStreamBuilder_setFormat)(pBuilder, (pDescriptor->format == ma_format_s16) ? MA_AAUDIO_FORMAT_PCM_I16 : MA_AAUDIO_FORMAT_PCM_FLOAT); } /* - There have been reports where setting the frames per data callback results in an error - later on from Android. To address this, I'm experimenting with simply not setting it on - anything from Android 11 and earlier. Suggestions welcome on how we might be able to make - this more targetted. + There have been reports where setting the frames per data callback results in an error. + In particular, re-routing may inadvertently switch from low-latency mode, resulting in a less stable + stream from the legacy path (AudioStreamLegacy). To address this, we simply don't set the value. It + can still be set if it's explicitly requested via the aaudio.allowSetBufferCapacity variable in the + device config. */ - if (!pConfig->aaudio.enableCompatibilityWorkarounds || ma_android_sdk_version() > 30) { + if ((!pConfig->aaudio.enableCompatibilityWorkarounds || ma_android_sdk_version() > 30) && pConfig->aaudio.allowSetBufferCapacity) { /* - AAudio is annoying when it comes to it's buffer calculation stuff because it doesn't let you + AAudio is annoying when it comes to its buffer calculation stuff because it doesn't let you retrieve the actual sample rate until after you've opened the stream. But you need to configure the buffer capacity before you open the stream... :/ @@ -37715,7 +37968,11 @@ static ma_result ma_create_and_configure_AAudioStreamBuilder__aaudio(ma_context* ((MA_PFN_AAudioStreamBuilder_setDataCallback)pContext->aaudio.AAudioStreamBuilder_setDataCallback)(pBuilder, ma_stream_data_callback_playback__aaudio, (void*)pDevice); } - /* Not sure how this affects things, but since there's a mapping between miniaudio's performance profiles and AAudio's performance modes, let go ahead and set it. */ + /* + If we set AAUDIO_PERFORMANCE_MODE_LOW_LATENCY, we allow for MMAP (non-legacy path). + Since there's a mapping between miniaudio's performance profiles and AAudio's performance modes, let's use it. + Beware though, with a conservative performance profile, AAudio will indeed take the legacy path. + */ ((MA_PFN_AAudioStreamBuilder_setPerformanceMode)pContext->aaudio.AAudioStreamBuilder_setPerformanceMode)(pBuilder, (pConfig->performanceProfile == ma_performance_profile_low_latency) ? MA_AAUDIO_PERFORMANCE_MODE_LOW_LATENCY : MA_AAUDIO_PERFORMANCE_MODE_NONE); /* We need to set an error callback to detect device changes. */ @@ -37751,6 +38008,9 @@ static ma_result ma_open_stream_basic__aaudio(ma_context* pContext, const ma_dev return result; } + /* Let's give AAudio a hint to avoid the legacy path (AudioStreamLegacy). */ + ((MA_PFN_AAudioStreamBuilder_setPerformanceMode)pContext->aaudio.AAudioStreamBuilder_setPerformanceMode)(pBuilder, MA_AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); + return ma_open_stream_and_close_builder__aaudio(pContext, pBuilder, ppStream); } @@ -37775,6 +38035,10 @@ static ma_result ma_open_stream__aaudio(ma_device* pDevice, const ma_device_conf static ma_result ma_close_stream__aaudio(ma_context* pContext, ma_AAudioStream* pStream) { + if (pStream == NULL) { + return MA_INVALID_ARGS; + } + return ma_result_from_aaudio(((MA_PFN_AAudioStream_close)pContext->aaudio.AAudioStream_close)(pStream)); } @@ -37901,20 +38165,36 @@ static ma_result ma_context_get_device_info__aaudio(ma_context* pContext, ma_dev return MA_SUCCESS; } +static ma_result ma_close_streams__aaudio(ma_device* pDevice) +{ + MA_ASSERT(pDevice != NULL); + + /* When re-routing, streams may have been closed and never re-opened. Hence the extra checks below. */ + if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_duplex) { + ma_close_stream__aaudio(pDevice->pContext, (ma_AAudioStream*)pDevice->aaudio.pStreamCapture); + pDevice->aaudio.pStreamCapture = NULL; + } + if (pDevice->type == ma_device_type_playback || pDevice->type == ma_device_type_duplex) { + ma_close_stream__aaudio(pDevice->pContext, (ma_AAudioStream*)pDevice->aaudio.pStreamPlayback); + pDevice->aaudio.pStreamPlayback = NULL; + } + + return MA_SUCCESS; +} static ma_result ma_device_uninit__aaudio(ma_device* pDevice) { MA_ASSERT(pDevice != NULL); - if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_duplex) { - ma_close_stream__aaudio(pDevice->pContext, (ma_AAudioStream*)pDevice->aaudio.pStreamCapture); - pDevice->aaudio.pStreamCapture = NULL; + /* Wait for any rerouting to finish before attempting to close the streams. */ + ma_mutex_lock(&pDevice->aaudio.rerouteLock); + { + ma_close_streams__aaudio(pDevice); } + ma_mutex_unlock(&pDevice->aaudio.rerouteLock); - if (pDevice->type == ma_device_type_playback || pDevice->type == ma_device_type_duplex) { - ma_close_stream__aaudio(pDevice->pContext, (ma_AAudioStream*)pDevice->aaudio.pStreamPlayback); - pDevice->aaudio.pStreamPlayback = NULL; - } + /* Destroy re-routing lock. */ + ma_mutex_uninit(&pDevice->aaudio.rerouteLock); return MA_SUCCESS; } @@ -37966,7 +38246,7 @@ static ma_result ma_device_init_by_type__aaudio(ma_device* pDevice, const ma_dev return MA_SUCCESS; } -static ma_result ma_device_init__aaudio(ma_device* pDevice, const ma_device_config* pConfig, ma_device_descriptor* pDescriptorPlayback, ma_device_descriptor* pDescriptorCapture) +static ma_result ma_device_init_streams__aaudio(ma_device* pDevice, const ma_device_config* pConfig, ma_device_descriptor* pDescriptorPlayback, ma_device_descriptor* pDescriptorCapture) { ma_result result; @@ -37999,6 +38279,25 @@ static ma_result ma_device_init__aaudio(ma_device* pDevice, const ma_device_conf return MA_SUCCESS; } +static ma_result ma_device_init__aaudio(ma_device* pDevice, const ma_device_config* pConfig, ma_device_descriptor* pDescriptorPlayback, ma_device_descriptor* pDescriptorCapture) +{ + ma_result result; + + MA_ASSERT(pDevice != NULL); + + result = ma_device_init_streams__aaudio(pDevice, pConfig, pDescriptorPlayback, pDescriptorCapture); + if (result != MA_SUCCESS) { + return result; + } + + result = ma_mutex_init(&pDevice->aaudio.rerouteLock); + if (result != MA_SUCCESS) { + return result; + } + + return MA_SUCCESS; +} + static ma_result ma_device_start_stream__aaudio(ma_device* pDevice, ma_AAudioStream* pStream) { ma_aaudio_result_t resultAA; @@ -38006,12 +38305,16 @@ static ma_result ma_device_start_stream__aaudio(ma_device* pDevice, ma_AAudioStr MA_ASSERT(pDevice != NULL); + if (pStream == NULL) { + return MA_INVALID_ARGS; + } + resultAA = ((MA_PFN_AAudioStream_requestStart)pDevice->pContext->aaudio.AAudioStream_requestStart)(pStream); if (resultAA != MA_AAUDIO_OK) { return ma_result_from_aaudio(resultAA); } - /* Do we actually need to wait for the device to transition into it's started state? */ + /* Do we actually need to wait for the device to transition into its started state? */ /* The device should be in either a starting or started state. If it's not set to started we need to wait for it to transition. It should go from starting to started. */ currentState = ((MA_PFN_AAudioStream_getState)pDevice->pContext->aaudio.AAudioStream_getState)(pStream); @@ -38038,6 +38341,10 @@ static ma_result ma_device_stop_stream__aaudio(ma_device* pDevice, ma_AAudioStre MA_ASSERT(pDevice != NULL); + if (pStream == NULL) { + return MA_INVALID_ARGS; + } + /* From the AAudio documentation: @@ -38123,22 +38430,20 @@ static ma_result ma_device_stop__aaudio(ma_device* pDevice) static ma_result ma_device_reinit__aaudio(ma_device* pDevice, ma_device_type deviceType) { ma_result result; + int32_t retries = 0; MA_ASSERT(pDevice != NULL); - /* The first thing to do is close the streams. */ - if (deviceType == ma_device_type_capture || deviceType == ma_device_type_duplex) { - ma_close_stream__aaudio(pDevice->pContext, (ma_AAudioStream*)pDevice->aaudio.pStreamCapture); - pDevice->aaudio.pStreamCapture = NULL; - } - - if (deviceType == ma_device_type_playback || deviceType == ma_device_type_duplex) { - ma_close_stream__aaudio(pDevice->pContext, (ma_AAudioStream*)pDevice->aaudio.pStreamPlayback); - pDevice->aaudio.pStreamPlayback = NULL; - } - - /* Now we need to reinitialize each streams. The hardest part with this is just filling output the config and descriptors. */ + /* + TODO: Stop retrying if main thread is about to uninit device. + */ + ma_mutex_lock(&pDevice->aaudio.rerouteLock); { +error_disconnected: + /* The first thing to do is close the streams. */ + ma_close_streams__aaudio(pDevice); + + /* Now we need to reinitialize each streams. The hardest part with this is just filling output the config and descriptors. */ ma_device_config deviceConfig; ma_device_descriptor descriptorPlayback; ma_device_descriptor descriptorCapture; @@ -38187,15 +38492,17 @@ static ma_result ma_device_reinit__aaudio(ma_device* pDevice, ma_device_type dev descriptorPlayback.periodCount = deviceConfig.periods; } - result = ma_device_init__aaudio(pDevice, &deviceConfig, &descriptorPlayback, &descriptorCapture); + result = ma_device_init_streams__aaudio(pDevice, &deviceConfig, &descriptorPlayback, &descriptorCapture); if (result != MA_SUCCESS) { - return result; + ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_WARNING, "[AAudio] Failed to create stream after route change."); + goto done; } result = ma_device_post_init(pDevice, deviceType, &descriptorPlayback, &descriptorCapture); if (result != MA_SUCCESS) { - ma_device_uninit__aaudio(pDevice); - return result; + ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_WARNING, "[AAudio] Failed to initialize device after route change."); + ma_close_streams__aaudio(pDevice); + goto done; } /* We'll only ever do this in response to a reroute. */ @@ -38204,14 +38511,29 @@ static ma_result ma_device_reinit__aaudio(ma_device* pDevice, ma_device_type dev /* If the device is started, start the streams. Maybe make this configurable? */ if (ma_device_get_state(pDevice) == ma_device_state_started) { if (pDevice->aaudio.noAutoStartAfterReroute == MA_FALSE) { - ma_device_start__aaudio(pDevice); + result = ma_device_start__aaudio(pDevice); + if (result != MA_SUCCESS) { + /* We got disconnected! Retry a few times, until we find a connected device! */ + retries += 1; + if (retries <= 3) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, "[AAudio] Failed to start stream after route change, retrying(%d)", retries); + goto error_disconnected; + } + ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, "[AAudio] Failed to start stream after route change."); + goto done; + } } else { ma_device_stop(pDevice); /* Do a full device stop so we set internal state correctly. */ } } - - return MA_SUCCESS; + + result = MA_SUCCESS; } +done: + /* Re-routing done */ + ma_mutex_unlock(&pDevice->aaudio.rerouteLock); + + return result; } static ma_result ma_device_get_info__aaudio(ma_device* pDevice, ma_device_type type, ma_device_info* pDeviceInfo) @@ -38222,12 +38544,12 @@ static ma_result ma_device_get_info__aaudio(ma_device* pDevice, ma_device_type t MA_ASSERT(type != ma_device_type_duplex); MA_ASSERT(pDeviceInfo != NULL); - if (type == ma_device_type_playback) { + if (type == ma_device_type_capture) { pStream = (ma_AAudioStream*)pDevice->aaudio.pStreamCapture; pDeviceInfo->id.aaudio = pDevice->capture.id.aaudio; ma_strncpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), MA_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); /* Only supporting default devices. */ } - if (type == ma_device_type_capture) { + if (type == ma_device_type_playback) { pStream = (ma_AAudioStream*)pDevice->aaudio.pStreamPlayback; pDeviceInfo->id.aaudio = pDevice->playback.id.aaudio; ma_strncpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), MA_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); /* Only supporting default devices. */ @@ -38260,6 +38582,7 @@ static ma_result ma_context_uninit__aaudio(ma_context* pContext) static ma_result ma_context_init__aaudio(ma_context* pContext, const ma_context_config* pConfig, ma_backend_callbacks* pCallbacks) { +#if !defined(MA_NO_RUNTIME_LINKING) size_t i; const char* libNames[] = { "libaaudio.so" @@ -38305,7 +38628,39 @@ static ma_result ma_context_init__aaudio(ma_context* pContext, const ma_context_ pContext->aaudio.AAudioStream_getFramesPerBurst = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->aaudio.hAAudio, "AAudioStream_getFramesPerBurst"); pContext->aaudio.AAudioStream_requestStart = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->aaudio.hAAudio, "AAudioStream_requestStart"); pContext->aaudio.AAudioStream_requestStop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->aaudio.hAAudio, "AAudioStream_requestStop"); - +#else + pContext->aaudio.AAudio_createStreamBuilder = (ma_proc)AAudio_createStreamBuilder; + pContext->aaudio.AAudioStreamBuilder_delete = (ma_proc)AAudioStreamBuilder_delete; + pContext->aaudio.AAudioStreamBuilder_setDeviceId = (ma_proc)AAudioStreamBuilder_setDeviceId; + pContext->aaudio.AAudioStreamBuilder_setDirection = (ma_proc)AAudioStreamBuilder_setDirection; + pContext->aaudio.AAudioStreamBuilder_setSharingMode = (ma_proc)AAudioStreamBuilder_setSharingMode; + pContext->aaudio.AAudioStreamBuilder_setFormat = (ma_proc)AAudioStreamBuilder_setFormat; + pContext->aaudio.AAudioStreamBuilder_setChannelCount = (ma_proc)AAudioStreamBuilder_setChannelCount; + pContext->aaudio.AAudioStreamBuilder_setSampleRate = (ma_proc)AAudioStreamBuilder_setSampleRate; + pContext->aaudio.AAudioStreamBuilder_setBufferCapacityInFrames = (ma_proc)AAudioStreamBuilder_setBufferCapacityInFrames; + pContext->aaudio.AAudioStreamBuilder_setFramesPerDataCallback = (ma_proc)AAudioStreamBuilder_setFramesPerDataCallback; + pContext->aaudio.AAudioStreamBuilder_setDataCallback = (ma_proc)AAudioStreamBuilder_setDataCallback; + pContext->aaudio.AAudioStreamBuilder_setErrorCallback = (ma_proc)AAudioStreamBuilder_setErrorCallback; + pContext->aaudio.AAudioStreamBuilder_setPerformanceMode = (ma_proc)AAudioStreamBuilder_setPerformanceMode; + pContext->aaudio.AAudioStreamBuilder_setUsage = (ma_proc)AAudioStreamBuilder_setUsage; + pContext->aaudio.AAudioStreamBuilder_setContentType = (ma_proc)AAudioStreamBuilder_setContentType; + pContext->aaudio.AAudioStreamBuilder_setInputPreset = (ma_proc)AAudioStreamBuilder_setInputPreset; + #if defined(__ANDROID_API__) && __ANDROID_API__ >= 29 + pContext->aaudio.AAudioStreamBuilder_setAllowedCapturePolicy = (ma_proc)AAudioStreamBuilder_setAllowedCapturePolicy; + #endif + pContext->aaudio.AAudioStreamBuilder_openStream = (ma_proc)AAudioStreamBuilder_openStream; + pContext->aaudio.AAudioStream_close = (ma_proc)AAudioStream_close; + pContext->aaudio.AAudioStream_getState = (ma_proc)AAudioStream_getState; + pContext->aaudio.AAudioStream_waitForStateChange = (ma_proc)AAudioStream_waitForStateChange; + pContext->aaudio.AAudioStream_getFormat = (ma_proc)AAudioStream_getFormat; + pContext->aaudio.AAudioStream_getChannelCount = (ma_proc)AAudioStream_getChannelCount; + pContext->aaudio.AAudioStream_getSampleRate = (ma_proc)AAudioStream_getSampleRate; + pContext->aaudio.AAudioStream_getBufferCapacityInFrames = (ma_proc)AAudioStream_getBufferCapacityInFrames; + pContext->aaudio.AAudioStream_getFramesPerDataCallback = (ma_proc)AAudioStream_getFramesPerDataCallback; + pContext->aaudio.AAudioStream_getFramesPerBurst = (ma_proc)AAudioStream_getFramesPerBurst; + pContext->aaudio.AAudioStream_requestStart = (ma_proc)AAudioStream_requestStart; + pContext->aaudio.AAudioStream_requestStop = (ma_proc)AAudioStream_requestStop; +#endif pCallbacks->onContextInit = ma_context_init__aaudio; pCallbacks->onContextUninit = ma_context_uninit__aaudio; @@ -38343,6 +38698,7 @@ static ma_result ma_context_init__aaudio(ma_context* pContext, const ma_context_ static ma_result ma_job_process__device__aaudio_reroute(ma_job* pJob) { + ma_result result; ma_device* pDevice; MA_ASSERT(pJob != NULL); @@ -38351,7 +38707,18 @@ static ma_result ma_job_process__device__aaudio_reroute(ma_job* pJob) MA_ASSERT(pDevice != NULL); /* Here is where we need to reroute the device. To do this we need to uninitialize the stream and reinitialize it. */ - return ma_device_reinit__aaudio(pDevice, (ma_device_type)pJob->data.device.aaudio.reroute.deviceType); + result = ma_device_reinit__aaudio(pDevice, (ma_device_type)pJob->data.device.aaudio.reroute.deviceType); + if (result != MA_SUCCESS) { + /* + Getting here means we failed to reroute the device. The best thing I can think of here is to + just stop the device. + */ + ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "[AAudio] Stopping device due to reroute failure."); + ma_device_stop(pDevice); + return result; + } + + return MA_SUCCESS; } #else /* Getting here means there is no AAudio backend so we need a no-op job implementation. */ @@ -39637,6 +40004,10 @@ Web Audio Backend #if (__EMSCRIPTEN_major__ > 3) || (__EMSCRIPTEN_major__ == 3 && (__EMSCRIPTEN_minor__ > 1 || (__EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 32))) #include #define MA_SUPPORT_AUDIO_WORKLETS + + #if (__EMSCRIPTEN_major__ > 3) || (__EMSCRIPTEN_major__ == 3 && (__EMSCRIPTEN_minor__ > 1 || (__EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 70))) + #define MA_SUPPORT_AUDIO_WORKLETS_VARIABLE_BUFFER_SIZE + #endif #endif /* @@ -39648,7 +40019,7 @@ TODO: Version 0.12: Swap this logic around so that AudioWorklets are used by def /* The thread stack size must be a multiple of 16. */ #ifndef MA_AUDIO_WORKLETS_THREAD_STACK_SIZE -#define MA_AUDIO_WORKLETS_THREAD_STACK_SIZE 16384 +#define MA_AUDIO_WORKLETS_THREAD_STACK_SIZE 131072 #endif #if defined(MA_USE_AUDIO_WORKLETS) @@ -39774,12 +40145,14 @@ static ma_result ma_device_uninit__webaudio(ma_device* pDevice) #if defined(MA_USE_AUDIO_WORKLETS) { EM_ASM({ - var device = miniaudio.get_device_by_index($0); + var device = window.miniaudio.get_device_by_index($0); if (device.streamNode !== undefined) { device.streamNode.disconnect(); device.streamNode = undefined; } + + device.pDevice = undefined; }, pDevice->webaudio.deviceIndex); emscripten_destroy_web_audio_node(pDevice->webaudio.audioWorklet); @@ -39789,7 +40162,7 @@ static ma_result ma_device_uninit__webaudio(ma_device* pDevice) #else { EM_ASM({ - var device = miniaudio.get_device_by_index($0); + var device = window.miniaudio.get_device_by_index($0); /* Make sure all nodes are disconnected and marked for collection. */ if (device.scriptNode !== undefined) { @@ -39816,7 +40189,7 @@ static ma_result ma_device_uninit__webaudio(ma_device* pDevice) /* Clean up the device on the JS side. */ EM_ASM({ - miniaudio.untrack_device_by_index($0); + window.miniaudio.untrack_device_by_index($0); }, pDevice->webaudio.deviceIndex); ma_free(pDevice->webaudio.pIntermediaryBuffer, &pDevice->pContext->allocationCallbacks); @@ -39882,10 +40255,6 @@ static EM_BOOL ma_audio_worklet_process_callback__webaudio(int inputCount, const (void)paramCount; (void)pParams; - if (ma_device_get_state(pDevice) != ma_device_state_started) { - return EM_TRUE; - } - /* The Emscripten documentation says that it'll always be 128 frames being passed in. Hard coding it like that feels like a very bad idea to me. Even if it's hard coded in the backend, the API and documentation should always refer @@ -39894,7 +40263,20 @@ static EM_BOOL ma_audio_worklet_process_callback__webaudio(int inputCount, const Unfortunately the audio data is not interleaved so we'll need to convert it before we give the data to miniaudio for further processing. */ - frameCount = 128; + if (pDevice->type == ma_device_type_playback) { + frameCount = pDevice->playback.internalPeriodSizeInFrames; + } else { + frameCount = pDevice->capture.internalPeriodSizeInFrames; + } + + if (ma_device_get_state(pDevice) != ma_device_state_started) { + /* Fill the output buffer with zero to avoid a noise sound */ + for (int i = 0; i < outputCount; i += 1) { + MA_ZERO_MEMORY(pOutputs[i].data, pOutputs[i].numberOfChannels * frameCount * sizeof(float)); + } + + return EM_TRUE; + } if (inputCount > 0) { /* Input data needs to be interleaved before we hand it to the client. */ @@ -39949,7 +40331,7 @@ static void ma_audio_worklet_processor_created__webaudio(EMSCRIPTEN_WEBAUDIO_T a count from MediaStreamAudioSourceNode (what we use for capture)? The only way to have control is to configure an output channel count on the capture side. This is slightly confusing for capture mode because intuitively you wouldn't actually connect an output to an input-only node, but this is what we'll have to do in order to have - proper control over the channel count. In the capture case, we'll have to output silence to it's output node. + proper control over the channel count. In the capture case, we'll have to output silence to its output node. */ if (pParameters->pConfig->deviceType == ma_device_type_capture) { channels = (int)((pParameters->pDescriptorCapture->channels > 0) ? pParameters->pDescriptorCapture->channels : MA_DEFAULT_CHANNELS); @@ -39972,7 +40354,15 @@ static void ma_audio_worklet_processor_created__webaudio(EMSCRIPTEN_WEBAUDIO_T a Now that we know the channel count to use we can allocate the intermediary buffer. The intermediary buffer is used for interleaving and deinterleaving. */ - intermediaryBufferSizeInFrames = 128; + #if defined(MA_SUPPORT_AUDIO_WORKLETS_VARIABLE_BUFFER_SIZE) + { + intermediaryBufferSizeInFrames = (size_t)emscripten_audio_context_quantum_size(audioContext); + } + #else + { + intermediaryBufferSizeInFrames = 128; + } + #endif pParameters->pDevice->webaudio.pIntermediaryBuffer = (float*)ma_malloc(intermediaryBufferSizeInFrames * (ma_uint32)channels * sizeof(float), &pParameters->pDevice->pContext->allocationCallbacks); if (pParameters->pDevice->webaudio.pIntermediaryBuffer == NULL) { @@ -39981,7 +40371,6 @@ static void ma_audio_worklet_processor_created__webaudio(EMSCRIPTEN_WEBAUDIO_T a return; } - pParameters->pDevice->webaudio.audioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "miniaudio", &audioWorkletOptions, &ma_audio_worklet_process_callback__webaudio, pParameters->pDevice); /* With the audio worklet initialized we can now attach it to the graph. */ @@ -40121,7 +40510,6 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co /* It's not clear if this can return an error. None of the tests in the Emscripten repository check for this, so neither am I for now. */ pDevice->webaudio.audioContext = emscripten_create_audio_context(&audioContextAttributes); - /* With the context created we can now create the worklet. We can only have a single worklet per audio context which means we'll need to craft this appropriately to handle duplex devices correctly. @@ -40170,11 +40558,12 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co /* We need to add an entry to the miniaudio.devices list on the JS side so we can do some JS/C interop. */ pDevice->webaudio.deviceIndex = EM_ASM_INT({ - return miniaudio.track_device({ + return window.miniaudio.track_device({ webaudio: emscriptenGetAudioObject($0), - state: 1 /* 1 = ma_device_state_stopped */ + state: 1, /* 1 = ma_device_state_stopped */ + pDevice: $1 }); - }, pDevice->webaudio.audioContext); + }, pDevice->webaudio.audioContext, pDevice); return MA_SUCCESS; } @@ -40186,7 +40575,7 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co ma_uint32 sampleRate; ma_uint32 periodSizeInFrames; - /* The channel count will depend on the device type. If it's a capture, use it's, otherwise use the playback side. */ + /* The channel count will depend on the device type. If it's a capture, use its, otherwise use the playback side. */ if (pConfig->deviceType == ma_device_type_capture) { channels = (pDescriptorCapture->channels > 0) ? pDescriptorCapture->channels : MA_DEFAULT_CHANNELS; } else { @@ -40255,11 +40644,11 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co /* The node processing callback. */ device.scriptNode.onaudioprocess = function(e) { if (device.intermediaryBufferView == null || device.intermediaryBufferView.length == 0) { - device.intermediaryBufferView = new Float32Array(Module.HEAPF32.buffer, pIntermediaryBuffer, bufferSize * channels); + device.intermediaryBufferView = new Float32Array(HEAPF32.buffer, pIntermediaryBuffer, bufferSize * channels); } /* Do the capture side first. */ - if (deviceType == miniaudio.device_type.capture || deviceType == miniaudio.device_type.duplex) { + if (deviceType == window.miniaudio.device_type.capture || deviceType == window.miniaudio.device_type.duplex) { /* The data must be interleaved before being processed miniaudio. */ for (var iChannel = 0; iChannel < channels; iChannel += 1) { var inputBuffer = e.inputBuffer.getChannelData(iChannel); @@ -40273,7 +40662,7 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co _ma_device_process_pcm_frames_capture__webaudio(pDevice, bufferSize, pIntermediaryBuffer); } - if (deviceType == miniaudio.device_type.playback || deviceType == miniaudio.device_type.duplex) { + if (deviceType == window.miniaudio.device_type.playback || deviceType == window.miniaudio.device_type.duplex) { _ma_device_process_pcm_frames_playback__webaudio(pDevice, bufferSize, pIntermediaryBuffer); for (var iChannel = 0; iChannel < e.outputBuffer.numberOfChannels; ++iChannel) { @@ -40293,7 +40682,7 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co }; /* Now we need to connect our node to the graph. */ - if (deviceType == miniaudio.device_type.capture || deviceType == miniaudio.device_type.duplex) { + if (deviceType == window.miniaudio.device_type.capture || deviceType == window.miniaudio.device_type.duplex) { navigator.mediaDevices.getUserMedia({audio:true, video:false}) .then(function(stream) { device.streamNode = device.webaudio.createMediaStreamSource(stream); @@ -40305,13 +40694,13 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co }); } - if (deviceType == miniaudio.device_type.playback) { + if (deviceType == window.miniaudio.device_type.playback) { device.scriptNode.connect(device.webaudio.destination); } device.pDevice = pDevice; - return miniaudio.track_device(device); + return window.miniaudio.track_device(device); }, pConfig->deviceType, channels, sampleRate, periodSizeInFrames, pDevice->webaudio.pIntermediaryBuffer, pDevice); if (deviceIndex < 0) { @@ -40321,7 +40710,7 @@ static ma_result ma_device_init__webaudio(ma_device* pDevice, const ma_device_co pDevice->webaudio.deviceIndex = deviceIndex; /* Grab the sample rate from the audio context directly. */ - sampleRate = (ma_uint32)EM_ASM_INT({ return miniaudio.get_device_by_index($0).webaudio.sampleRate; }, deviceIndex); + sampleRate = (ma_uint32)EM_ASM_INT({ return window.miniaudio.get_device_by_index($0).webaudio.sampleRate; }, deviceIndex); if (pDescriptorCapture != NULL) { pDescriptorCapture->format = ma_format_f32; @@ -40351,9 +40740,9 @@ static ma_result ma_device_start__webaudio(ma_device* pDevice) MA_ASSERT(pDevice != NULL); EM_ASM({ - var device = miniaudio.get_device_by_index($0); + var device = window.miniaudio.get_device_by_index($0); device.webaudio.resume(); - device.state = miniaudio.device_state.started; + device.state = window.miniaudio.device_state.started; }, pDevice->webaudio.deviceIndex); return MA_SUCCESS; @@ -40373,9 +40762,9 @@ static ma_result ma_device_stop__webaudio(ma_device* pDevice) do any kind of explicit draining. */ EM_ASM({ - var device = miniaudio.get_device_by_index($0); + var device = window.miniaudio.get_device_by_index($0); device.webaudio.suspend(); - device.state = miniaudio.device_state.stopped; + device.state = window.miniaudio.device_state.stopped; }, pDevice->webaudio.deviceIndex); ma_device__on_notification_stopped(pDevice); @@ -40393,6 +40782,10 @@ static ma_result ma_context_uninit__webaudio(ma_context* pContext) /* Remove the global miniaudio object from window if there are no more references to it. */ EM_ASM({ if (typeof(window.miniaudio) !== 'undefined') { + miniaudio.unlock_event_types.map(function(event_type) { + document.removeEventListener(event_type, miniaudio.unlock, true); + }); + window.miniaudio.referenceCount -= 1; if (window.miniaudio.referenceCount === 0) { delete window.miniaudio; @@ -40434,6 +40827,7 @@ static ma_result ma_context_init__webaudio(ma_context* pContext, const ma_contex window.miniaudio.device_state.started = $4; /* Device cache for mapping devices to indexes for JavaScript/C interop. */ + let miniaudio = window.miniaudio; miniaudio.devices = []; miniaudio.track_device = function(device) { @@ -40485,13 +40879,13 @@ static ma_result ma_context_init__webaudio(ma_context* pContext, const ma_contex var device = miniaudio.devices[i]; if (device != null && device.webaudio != null && - device.state === window.miniaudio.device_state.started) { + device.state === miniaudio.device_state.started) { device.webaudio.resume().then(() => { - Module._ma_device__on_notification_unlocked(device.pDevice); - }, - (error) => {console.error("Failed to resume audiocontext", error); - }); + _ma_device__on_notification_unlocked(device.pDevice); + }, + (error) => {console.error("Failed to resume audiocontext", error); + }); } } miniaudio.unlock_event_types.map(function(event_type) { @@ -40527,7 +40921,7 @@ static ma_result ma_context_init__webaudio(ma_context* pContext, const ma_contex return MA_SUCCESS; } -#endif /* Web Audio */ +#endif /* MA_HAS_WEBAUDIO */ @@ -40806,7 +41200,7 @@ MA_API ma_result ma_device_post_init(ma_device* pDevice, ma_device_type deviceTy ma_device_info deviceInfo; if (deviceType == ma_device_type_capture || deviceType == ma_device_type_duplex || deviceType == ma_device_type_loopback) { - result = ma_device_get_info(pDevice, (deviceType == ma_device_type_loopback) ? ma_device_type_playback : ma_device_type_capture, &deviceInfo); + result = ma_device_get_info(pDevice, ma_device_type_capture, &deviceInfo); if (result == MA_SUCCESS) { ma_strncpy_s(pDevice->capture.name, sizeof(pDevice->capture.name), deviceInfo.name, (size_t)-1); } else { @@ -40853,7 +41247,7 @@ static ma_thread_result MA_THREADCALL ma_worker_thread(void* pData) #endif /* - When the device is being initialized it's initial state is set to ma_device_state_uninitialized. Before returning from + When the device is being initialized its initial state is set to ma_device_state_uninitialized. Before returning from ma_device_init(), the state needs to be set to something valid. In miniaudio the device's default state immediately after initialization is stopped, so therefore we need to mark the device as such. miniaudio will wait on the worker thread to signal an event to know when the worker thread is ready for action. @@ -41198,6 +41592,24 @@ MA_API ma_result ma_device_job_thread_next(ma_device_job_thread* pJobThread, ma_ } +MA_API ma_bool32 ma_device_id_equal(const ma_device_id* pA, const ma_device_id* pB) +{ + size_t i; + + if (pA == NULL || pB == NULL) { + return MA_FALSE; + } + + for (i = 0; i < sizeof(ma_device_id); i += 1) { + if (((const char*)pA)[i] != ((const char*)pB)[i]) { + return MA_FALSE; + } + } + + return MA_TRUE; +} + + MA_API ma_context_config ma_context_config_init(void) { @@ -41971,7 +42383,7 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC return result; } - /* Wait for the worker thread to put the device into it's stopped state for real. */ + /* Wait for the worker thread to put the device into its stopped state for real. */ ma_event_wait(&pDevice->stopEvent); MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped); } else { @@ -41997,7 +42409,7 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, "[%s]\n", ma_get_backend_name(pDevice->pContext->backend)); if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_duplex || pDevice->type == ma_device_type_loopback) { char name[MA_MAX_DEVICE_NAME_LENGTH + 1]; - ma_device_get_name(pDevice, (pDevice->type == ma_device_type_loopback) ? ma_device_type_playback : ma_device_type_capture, name, sizeof(name), NULL); + ma_device_get_name(pDevice, ma_device_type_capture, name, sizeof(name), NULL); ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, " %s (%s)\n", name, "Capture"); ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_INFO, " Format: %s -> %s\n", ma_get_format_name(pDevice->capture.internalFormat), ma_get_format_name(pDevice->capture.format)); @@ -42250,6 +42662,17 @@ MA_API ma_result ma_device_get_info(ma_device* pDevice, ma_device_type type, ma_ if (type == ma_device_type_playback) { return ma_context_get_device_info(pDevice->pContext, type, pDevice->playback.pID, pDeviceInfo); } else { + /* + Here we're getting the capture side, which is the branch we'll be entering for a loopback + device, since loopback is capturing. However, if the device is using the default device ID, + it won't get the correct information because it'll think we're asking for the default + capture device, where in fact for loopback we want the default *playback* device. We'll do + a bit of a hack here to make sure we get the correct info. + */ + if (pDevice->type == ma_device_type_loopback && pDevice->capture.pID == NULL) { + type = ma_device_type_playback; + } + return ma_context_get_device_info(pDevice->pContext, type, pDevice->capture.pID, pDeviceInfo); } } @@ -42311,6 +42734,15 @@ MA_API ma_result ma_device_start(ma_device* pDevice) ma_mutex_lock(&pDevice->startStopLock); { + /* + We need to check again if the device is in a started state because it's possible for one thread to have started the device + while another was waiting on the mutex. + */ + if (ma_device_get_state(pDevice) == ma_device_state_started) { + ma_mutex_unlock(&pDevice->startStopLock); + return MA_SUCCESS; /* Already started. */ + } + /* Starting and stopping are wrapped in a mutex which means we can assert that the device is in a stopped or paused state. */ MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped); @@ -42371,6 +42803,15 @@ MA_API ma_result ma_device_stop(ma_device* pDevice) ma_mutex_lock(&pDevice->startStopLock); { + /* + We need to check again if the device is in a stopped state because it's possible for one thread to have stopped the device + while another was waiting on the mutex. + */ + if (ma_device_get_state(pDevice) == ma_device_state_stopped) { + ma_mutex_unlock(&pDevice->startStopLock); + return MA_SUCCESS; /* Already stopped. */ + } + /* Starting and stopping are wrapped in a mutex which means we can assert that the device is in a started or paused state. */ MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_started); @@ -42389,7 +42830,7 @@ MA_API ma_result ma_device_stop(ma_device* pDevice) } else { /* Synchronous backends. The stop callback is always called from the worker thread. Do not call the stop callback here. If - the backend is implementing it's own audio thread loop we'll need to wake it up if required. Note that we need to make + the backend is implementing its own audio thread loop we'll need to wake it up if required. Note that we need to make sure the state of the device is *not* playing right now, which it shouldn't be since we set it above. This is super important though, so I'm asserting it here as well for extra safety in case we accidentally change something later. */ @@ -42506,6 +42947,15 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void return MA_INVALID_ARGS; } + /* + There is an assert deeper in the code that checks that frameCount > 0. Since this is a public facing + API we'll need to check for that here. I've had reports that AAudio can sometimes post a frame count + of 0. + */ + if (frameCount == 0) { + return MA_INVALID_ARGS; + } + if (pDevice->type == ma_device_type_duplex) { if (pInput != NULL) { ma_device__handle_duplex_callback_capture(pDevice, frameCount, pInput, &pDevice->duplexRB.rb); @@ -42580,7 +43030,7 @@ MA_API ma_uint32 ma_calculate_buffer_size_in_milliseconds_from_frames(ma_uint32 return 0; } - return bufferSizeInFrames*1000 / sampleRate; + return (bufferSizeInFrames*1000 + (sampleRate - 1)) / sampleRate; } MA_API ma_uint32 ma_calculate_buffer_size_in_frames_from_milliseconds(ma_uint32 bufferSizeInMilliseconds, ma_uint32 sampleRate) @@ -47408,7 +47858,7 @@ static ma_result ma_bpf_get_heap_layout(const ma_bpf_config* pConfig, ma_bpf_hea return MA_INVALID_ARGS; } - bpf2Count = pConfig->channels / 2; + bpf2Count = pConfig->order / 2; pHeapLayout->sizeInBytes = 0; @@ -49466,7 +49916,7 @@ MA_API float ma_fader_get_current_volume(const ma_fader* pFader) } else if ((ma_uint64)pFader->cursorInFrames >= pFader->lengthInFrames) { /* Safe case because the < 0 case was checked above. */ return pFader->volumeEnd; } else { - /* The cursor is somewhere inside the fading period. We can figure this out with a simple linear interpoluation between volumeBeg and volumeEnd based on our cursor position. */ + /* The cursor is somewhere inside the fading period. We can figure this out with a simple linear interpolation between volumeBeg and volumeEnd based on our cursor position. */ return ma_mix_f32_fast(pFader->volumeBeg, pFader->volumeEnd, (ma_uint32)pFader->cursorInFrames / (float)((ma_uint32)pFader->lengthInFrames)); /* Safe cast to uint32 because we clamp it in ma_fader_process_pcm_frames(). */ } } @@ -49689,9 +50139,9 @@ static float ma_attenuation_exponential(float distance, float minDistance, float /* -Dopper Effect calculation taken from the OpenAL spec, with two main differences: +Doppler Effect calculation taken from the OpenAL spec, with two main differences: - 1) The source to listener vector will have already been calcualted at an earlier step so we can + 1) The source to listener vector will have already been calculated at an earlier step so we can just use that directly. We need only the position of the source relative to the origin. 2) We don't scale by a frequency because we actually just want the ratio which we'll plug straight @@ -49730,7 +50180,7 @@ static void ma_get_default_channel_map_for_spatializer(ma_channel* pChannelMap, Special case for stereo. Want to default the left and right speakers to side left and side right so that they're facing directly down the X axis rather than slightly forward. Not doing this will result in sounds being quieter when behind the listener. This might - actually be good for some scenerios, but I don't think it's an appropriate default because + actually be good for some scenarios, but I don't think it's an appropriate default because it can be a bit unexpected. */ if (channelCount == 2) { @@ -50064,7 +50514,7 @@ MA_API ma_spatializer_config ma_spatializer_config_init(ma_uint32 channelsIn, ma config.maxDistance = MA_FLT_MAX; config.rolloff = 1; config.coneInnerAngleInRadians = 6.283185f; /* 360 degrees. */ - config.coneOuterAngleInRadians = 6.283185f; /* 360 degress. */ + config.coneOuterAngleInRadians = 6.283185f; /* 360 degrees. */ config.coneOuterGain = 0.0f; config.dopplerFactor = 1; config.directionalAttenuationFactor = 1; @@ -50298,7 +50748,7 @@ static float ma_calculate_angular_gain(ma_vec3f dirA, ma_vec3f dirB, float coneI To do cone attenuation, I'm just using the same math that we'd use to implement a basic spotlight in OpenGL. We just need to get the direction from the source to the listener and then do a dot product against that and the direction of the spotlight. Then we just compare that dot product against the cosine of the inner and outer - angles. If the dot product is greater than the the outer angle, we just use coneOuterGain. If it's less than + angles. If the dot product is greater than the outer angle, we just use coneOuterGain. If it's less than the inner angle, we just use a gain of 1. Otherwise we linearly interpolate between 1 and coneOuterGain. */ if (coneInnerAngleInRadians < 6.283185f) { @@ -50368,7 +50818,7 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer, ma_vec3f relativePosNormalized; ma_vec3f relativePos; /* The position relative to the listener. */ ma_vec3f relativeDir; /* The direction of the sound, relative to the listener. */ - ma_vec3f listenerVel; /* The volocity of the listener. For doppler pitch calculation. */ + ma_vec3f listenerVel; /* The velocity of the listener. For doppler pitch calculation. */ float speedOfSound; float distance = 0; float gain = 1; @@ -50449,11 +50899,11 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer, To do cone attenuation, I'm just using the same math that we'd use to implement a basic spotlight in OpenGL. We just need to get the direction from the source to the listener and then do a dot product against that and the direction of the spotlight. Then we just compare that dot product against the cosine of the inner and outer - angles. If the dot product is greater than the the outer angle, we just use coneOuterGain. If it's less than + angles. If the dot product is greater than the outer angle, we just use coneOuterGain. If it's less than the inner angle, we just use a gain of 1. Otherwise we linearly interpolate between 1 and coneOuterGain. */ if (distance > 0) { - /* Source anglular gain. */ + /* Source angular gain. */ float spatializerConeInnerAngle; float spatializerConeOuterAngle; float spatializerConeOuterGain; @@ -50965,7 +51415,7 @@ MA_API void ma_spatializer_get_relative_position_and_direction(const ma_spatiali listenerDirection = ma_spatializer_listener_get_direction(pListener); /* - We need to calcualte the right vector from our forward and up vectors. This is done with + We need to calculate the right vector from our forward and up vectors. This is done with a cross product. */ axisZ = ma_vec3f_normalize(listenerDirection); /* Normalization required here because we can't trust the caller. */ @@ -51111,7 +51561,7 @@ static ma_result ma_linear_resampler_set_rate_internal(ma_linear_resampler* pRes lpfConfig = ma_lpf_config_init(pResampler->config.format, pResampler->config.channels, lpfSampleRate, lpfCutoffFrequency, pResampler->config.lpfOrder); /* - If the resampler is alreay initialized we don't want to do a fresh initialization of the low-pass filter because it will result in the cached frames + If the resampler is already initialized we don't want to do a fresh initialization of the low-pass filter because it will result in the cached frames getting cleared. Instead we re-initialize the filter which will maintain any cached frames. */ if (isResamplerAlreadyInitialized) { @@ -51806,7 +52256,7 @@ MA_API ma_result ma_linear_resampler_get_expected_output_frame_count(const ma_li preliminaryInputFrameCount = (pResampler->inTimeInt + outputFrameCount*pResampler->inAdvanceInt ) + preliminaryInputFrameCountFromFrac; /* - If the total number of *whole* input frames that would be required to generate our preliminary output frame count is greather than + If the total number of *whole* input frames that would be required to generate our preliminary output frame count is greater than the amount of whole input frames we have available as input we need to *not* add an extra output frame as there won't be enough data to actually process. Otherwise we need to add the extra output frame. */ @@ -51844,7 +52294,7 @@ MA_API ma_result ma_linear_resampler_reset(ma_linear_resampler* pResampler) } } - /* The low pass filter needs to have it's cache reset. */ + /* The low pass filter needs to have its cache reset. */ ma_lpf_clear_cache(&pResampler->lpf); return MA_SUCCESS; @@ -52361,19 +52811,19 @@ static float ma_calculate_channel_position_rectangular_weight(ma_channel channel of contribution to apply to the side/left and back/left speakers, however, is a bit more complicated. Imagine the front/left speaker as emitting audio from two planes - the front plane and the left plane. You can think of the front/left - speaker emitting half of it's total volume from the front, and the other half from the left. Since part of it's volume is being emitted + speaker emitting half of its total volume from the front, and the other half from the left. Since part of its volume is being emitted from the left side, and the side/left and back/left channels also emit audio from the left plane, one would expect that they would receive some amount of contribution from front/left speaker. The amount of contribution depends on how many planes are shared between the two speakers. Note that in the examples below I've added a top/front/left speaker as an example just to show how the math works across 3 spatial dimensions. The first thing to do is figure out how each speaker's volume is spread over each of plane: - - front/left: 2 planes (front and left) = 1/2 = half it's total volume on each plane + - front/left: 2 planes (front and left) = 1/2 = half its total volume on each plane - side/left: 1 plane (left only) = 1/1 = entire volume from left plane - - back/left: 2 planes (back and left) = 1/2 = half it's total volume on each plane - - top/front/left: 3 planes (top, front and left) = 1/3 = one third it's total volume on each plane + - back/left: 2 planes (back and left) = 1/2 = half its total volume on each plane + - top/front/left: 3 planes (top, front and left) = 1/3 = one third its total volume on each plane - The amount of volume each channel contributes to each of it's planes is what controls how much it is willing to given and take to other + The amount of volume each channel contributes to each of its planes is what controls how much it is willing to given and take to other channels on the same plane. The volume that is willing to the given by one channel is multiplied by the volume that is willing to be taken by the other to produce the final contribution. */ @@ -52484,12 +52934,7 @@ static ma_channel_conversion_path ma_channel_map_get_conversion_path(const ma_ch ma_uint32 iChannelIn; ma_bool32 areAllChannelPositionsPresent = MA_TRUE; for (iChannelIn = 0; iChannelIn < channelsIn; ++iChannelIn) { - ma_bool32 isInputChannelPositionInOutput = MA_FALSE; - if (ma_channel_map_contains_channel_position(channelsOut, pChannelMapOut, ma_channel_map_get_channel(pChannelMapIn, channelsIn, iChannelIn))) { - isInputChannelPositionInOutput = MA_TRUE; - break; - } - + ma_bool32 isInputChannelPositionInOutput = ma_channel_map_contains_channel_position(channelsOut, pChannelMapOut, ma_channel_map_get_channel(pChannelMapIn, channelsIn, iChannelIn)); if (!isInputChannelPositionInOutput) { areAllChannelPositionsPresent = MA_FALSE; break; @@ -52516,8 +52961,8 @@ static ma_result ma_channel_map_build_shuffle_table(const ma_channel* pChannelMa } /* - When building the shuffle table we just do a 1:1 mapping based on the first occurance of a channel. If the - input channel has more than one occurance of a channel position, the second one will be ignored. + When building the shuffle table we just do a 1:1 mapping based on the first occurrence of a channel. If the + input channel has more than one occurrence of a channel position, the second one will be ignored. */ for (iChannelOut = 0; iChannelOut < channelCountOut; iChannelOut += 1) { ma_channel channelOut; @@ -54812,7 +55257,7 @@ static ma_result ma_data_converter_process_pcm_frames__channels_first(ma_data_co Before doing any processing we need to determine how many frames we should try processing this iteration, for both input and output. The resampler requires us to perform format and channel conversion before passing any data into it. If we get our input count wrong, we'll - end up peforming redundant pre-processing. This isn't the end of the world, but it does + end up performing redundant pre-processing. This isn't the end of the world, but it does result in some inefficiencies proportionate to how far our estimates are off. If the resampler has a means to calculate exactly how much we'll need, we'll use that. @@ -55982,7 +56427,7 @@ MA_API const char* ma_channel_position_to_string(ma_channel channel) case MA_CHANNEL_LFE : return "CHANNEL_LFE"; case MA_CHANNEL_BACK_LEFT : return "CHANNEL_BACK_LEFT"; case MA_CHANNEL_BACK_RIGHT : return "CHANNEL_BACK_RIGHT"; - case MA_CHANNEL_FRONT_LEFT_CENTER : return "CHANNEL_FRONT_LEFT_CENTER "; + case MA_CHANNEL_FRONT_LEFT_CENTER : return "CHANNEL_FRONT_LEFT_CENTER"; case MA_CHANNEL_FRONT_RIGHT_CENTER: return "CHANNEL_FRONT_RIGHT_CENTER"; case MA_CHANNEL_BACK_CENTER : return "CHANNEL_BACK_CENTER"; case MA_CHANNEL_SIDE_LEFT : return "CHANNEL_SIDE_LEFT"; @@ -56287,13 +56732,9 @@ MA_API ma_result ma_rb_commit_read(ma_rb* pRB, size_t sizeInBytes) newReadOffsetLoopFlag ^= 0x80000000; } - ma_atomic_exchange_32(&pRB->encodedReadOffset, ma_rb__construct_offset(newReadOffsetLoopFlag, newReadOffsetInBytes)); + ma_atomic_exchange_32(&pRB->encodedReadOffset, ma_rb__construct_offset(newReadOffsetInBytes, newReadOffsetLoopFlag)); - if (ma_rb_pointer_distance(pRB) == 0) { - return MA_AT_END; - } else { - return MA_SUCCESS; - } + return MA_SUCCESS; } MA_API ma_result ma_rb_acquire_write(ma_rb* pRB, size_t* pSizeInBytes, void** ppBufferOut) @@ -56373,13 +56814,9 @@ MA_API ma_result ma_rb_commit_write(ma_rb* pRB, size_t sizeInBytes) newWriteOffsetLoopFlag ^= 0x80000000; } - ma_atomic_exchange_32(&pRB->encodedWriteOffset, ma_rb__construct_offset(newWriteOffsetLoopFlag, newWriteOffsetInBytes)); + ma_atomic_exchange_32(&pRB->encodedWriteOffset, ma_rb__construct_offset(newWriteOffsetInBytes, newWriteOffsetLoopFlag)); - if (ma_rb_pointer_distance(pRB) == 0) { - return MA_AT_END; - } else { - return MA_SUCCESS; - } + return MA_SUCCESS; } MA_API ma_result ma_rb_seek_read(ma_rb* pRB, size_t offsetInBytes) @@ -56602,6 +57039,16 @@ static ma_result ma_pcm_rb_data_source__on_read(ma_data_source* pDataSource, voi totalFramesRead += mappedFrameCount; } + /* + There is no notion of an "end" in a ring buffer. If we didn't have enough data to fill the requested frame + count we'll need to pad with silence. If we don't do this, totalFramesRead might equal 0 which will result + in the data source layer at a higher level translating this to MA_AT_END which is incorrect for a ring buffer. + */ + if (totalFramesRead < frameCount) { + ma_silence_pcm_frames(ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, pRB->format, pRB->channels), (frameCount - totalFramesRead), pRB->format, pRB->channels); + totalFramesRead = frameCount; + } + *pFramesRead = totalFramesRead; return MA_SUCCESS; } @@ -57150,6 +57597,10 @@ MA_API ma_result ma_data_source_init(const ma_data_source_config* pConfig, ma_da return MA_INVALID_ARGS; } + if (pConfig->vtable == NULL) { + return MA_INVALID_ARGS; + } + pDataSourceBase->vtable = pConfig->vtable; pDataSourceBase->rangeBegInFrames = MA_DATA_SOURCE_DEFAULT_RANGE_BEG; pDataSourceBase->rangeEndInFrames = MA_DATA_SOURCE_DEFAULT_RANGE_END; @@ -57200,6 +57651,58 @@ static ma_result ma_data_source_resolve_current(ma_data_source* pDataSource, ma_ return MA_SUCCESS; } +static ma_result ma_data_source_read_pcm_frames_from_backend(ma_data_source* pDataSource, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead) +{ + ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource; + + MA_ASSERT(pDataSourceBase != NULL); + MA_ASSERT(pDataSourceBase->vtable != NULL); + MA_ASSERT(pDataSourceBase->vtable->onRead != NULL); + MA_ASSERT(pFramesRead != NULL); + + if (pFramesOut != NULL) { + return pDataSourceBase->vtable->onRead(pDataSourceBase, pFramesOut, frameCount, pFramesRead); + } else { + /* + No output buffer. Probably seeking forward. Read and discard. Can probably optimize this in terms of + onSeek and onGetCursor, but need to keep in mind that the data source may not implement these functions. + */ + ma_result result; + ma_uint64 framesRead; + ma_format format; + ma_uint32 channels; + ma_uint64 discardBufferCapInFrames; + ma_uint8 pDiscardBuffer[4096]; + + result = ma_data_source_get_data_format(pDataSource, &format, &channels, NULL, NULL, 0); + if (result != MA_SUCCESS) { + return result; + } + + discardBufferCapInFrames = sizeof(pDiscardBuffer) / ma_get_bytes_per_frame(format, channels); + + framesRead = 0; + while (framesRead < frameCount) { + ma_uint64 framesReadThisIteration = 0; + ma_uint64 framesToRead = frameCount - framesRead; + if (framesToRead > discardBufferCapInFrames) { + framesToRead = discardBufferCapInFrames; + } + + result = pDataSourceBase->vtable->onRead(pDataSourceBase, pDiscardBuffer, framesToRead, &framesReadThisIteration); + if (result != MA_SUCCESS) { + return result; + } + + framesRead += framesReadThisIteration; + } + + *pFramesRead = framesRead; + + return MA_SUCCESS; + } +} + static ma_result ma_data_source_read_pcm_frames_within_range(ma_data_source* pDataSource, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead) { ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource; @@ -57215,9 +57718,11 @@ static ma_result ma_data_source_read_pcm_frames_within_range(ma_data_source* pDa return MA_INVALID_ARGS; } + MA_ASSERT(pDataSourceBase->vtable != NULL); + if ((pDataSourceBase->vtable->flags & MA_DATA_SOURCE_SELF_MANAGED_RANGE_AND_LOOP_POINT) != 0 || (pDataSourceBase->rangeEndInFrames == ~((ma_uint64)0) && (pDataSourceBase->loopEndInFrames == ~((ma_uint64)0) || loop == MA_FALSE))) { /* Either the data source is self-managing the range, or no range is set - just read like normal. The data source itself will tell us when the end is reached. */ - result = pDataSourceBase->vtable->onRead(pDataSourceBase, pFramesOut, frameCount, &framesRead); + result = ma_data_source_read_pcm_frames_from_backend(pDataSource, pFramesOut, frameCount, &framesRead); } else { /* Need to clamp to within the range. */ ma_uint64 relativeCursor; @@ -57226,7 +57731,7 @@ static ma_result ma_data_source_read_pcm_frames_within_range(ma_data_source* pDa result = ma_data_source_get_cursor_in_pcm_frames(pDataSourceBase, &relativeCursor); if (result != MA_SUCCESS) { /* Failed to retrieve the cursor. Cannot read within a range or loop points. Just read like normal - this may happen for things like noise data sources where it doesn't really matter. */ - result = pDataSourceBase->vtable->onRead(pDataSourceBase, pFramesOut, frameCount, &framesRead); + result = ma_data_source_read_pcm_frames_from_backend(pDataSource, pFramesOut, frameCount, &framesRead); } else { ma_uint64 rangeBeg; ma_uint64 rangeEnd; @@ -57254,7 +57759,7 @@ static ma_result ma_data_source_read_pcm_frames_within_range(ma_data_source* pDa MA_AT_END so the higher level function can know about it. */ if (frameCount > 0) { - result = pDataSourceBase->vtable->onRead(pDataSourceBase, pFramesOut, frameCount, &framesRead); + result = ma_data_source_read_pcm_frames_from_backend(pDataSource, pFramesOut, frameCount, &framesRead); } else { result = MA_AT_END; /* The cursor is sitting on the end of the range which means we're at the end. */ } @@ -57336,7 +57841,7 @@ MA_API ma_result ma_data_source_read_pcm_frames(ma_data_source* pDataSource, voi totalFramesProcessed += framesProcessed; /* - If we encounted an error from the read callback, make sure it's propagated to the caller. The caller may need to know whether or not MA_BUSY is returned which is + If we encountered an error from the read callback, make sure it's propagated to the caller. The caller may need to know whether or not MA_BUSY is returned which is not necessarily considered an error. */ if (result != MA_SUCCESS && result != MA_AT_END) { @@ -57427,7 +57932,7 @@ MA_API ma_result ma_data_source_seek_to_pcm_frame(ma_data_source* pDataSource, m ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource; if (pDataSourceBase == NULL) { - return MA_SUCCESS; + return MA_INVALID_ARGS; } if (pDataSourceBase->vtable->onSeek == NULL) { @@ -57435,12 +57940,61 @@ MA_API ma_result ma_data_source_seek_to_pcm_frame(ma_data_source* pDataSource, m } if (frameIndex > pDataSourceBase->rangeEndInFrames) { - return MA_INVALID_OPERATION; /* Trying to seek to far forward. */ + return MA_INVALID_OPERATION; /* Trying to seek too far forward. */ } + MA_ASSERT(pDataSourceBase->vtable != NULL); + return pDataSourceBase->vtable->onSeek(pDataSource, pDataSourceBase->rangeBegInFrames + frameIndex); } +MA_API ma_result ma_data_source_seek_seconds(ma_data_source* pDataSource, float secondCount, float* pSecondsSeeked) +{ + ma_uint64 frameCount; + ma_uint64 framesSeeked = 0; + ma_uint32 sampleRate; + ma_result result; + + if (pDataSource == NULL) { + return MA_INVALID_ARGS; + } + + result = ma_data_source_get_data_format(pDataSource, NULL, NULL, &sampleRate, NULL, 0); + if (result != MA_SUCCESS) { + return result; + } + + /* We need PCM frames instead of seconds */ + frameCount = (ma_uint64)(secondCount * sampleRate); + + result = ma_data_source_seek_pcm_frames(pDataSource, frameCount, &framesSeeked); + + /* VC6 doesn't support division between unsigned 64-bit integer and floating point number. Signed integer needed. This shouldn't affect anything in practice */ + *pSecondsSeeked = (ma_int64)framesSeeked / (float)sampleRate; + return result; +} + +MA_API ma_result ma_data_source_seek_to_second(ma_data_source* pDataSource, float seekPointInSeconds) +{ + ma_uint64 frameIndex; + ma_uint32 sampleRate; + ma_result result; + + if (pDataSource == NULL) { + return MA_INVALID_ARGS; + } + + result = ma_data_source_get_data_format(pDataSource, NULL, NULL, &sampleRate, NULL, 0); + if (result != MA_SUCCESS) { + return result; + } + + /* We need PCM frames instead of seconds */ + frameIndex = (ma_uint64)(seekPointInSeconds * sampleRate); + + return ma_data_source_seek_to_pcm_frame(pDataSource, frameIndex); +} + MA_API ma_result ma_data_source_get_data_format(ma_data_source* pDataSource, ma_format* pFormat, ma_uint32* pChannels, ma_uint32* pSampleRate, ma_channel* pChannelMap, size_t channelMapCap) { ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource; @@ -57467,6 +58021,8 @@ MA_API ma_result ma_data_source_get_data_format(ma_data_source* pDataSource, ma_ return MA_INVALID_ARGS; } + MA_ASSERT(pDataSourceBase->vtable != NULL); + if (pDataSourceBase->vtable->onGetDataFormat == NULL) { return MA_NOT_IMPLEMENTED; } @@ -57507,6 +58063,8 @@ MA_API ma_result ma_data_source_get_cursor_in_pcm_frames(ma_data_source* pDataSo return MA_SUCCESS; } + MA_ASSERT(pDataSourceBase->vtable != NULL); + if (pDataSourceBase->vtable->onGetCursor == NULL) { return MA_NOT_IMPLEMENTED; } @@ -57540,6 +58098,8 @@ MA_API ma_result ma_data_source_get_length_in_pcm_frames(ma_data_source* pDataSo return MA_INVALID_ARGS; } + MA_ASSERT(pDataSourceBase->vtable != NULL); + /* If we have a range defined we'll use that to determine the length. This is one of rare times where we'll actually trust the caller. If they've set the range, I think it's mostly safe to @@ -57627,6 +58187,8 @@ MA_API ma_result ma_data_source_set_looping(ma_data_source* pDataSource, ma_bool ma_atomic_exchange_32(&pDataSourceBase->isLooping, isLooping); + MA_ASSERT(pDataSourceBase->vtable != NULL); + /* If there's no callback for this just treat it as a successful no-op. */ if (pDataSourceBase->vtable->onSetLooping == NULL) { return MA_SUCCESS; @@ -57664,7 +58226,7 @@ MA_API ma_result ma_data_source_set_range_in_pcm_frames(ma_data_source* pDataSou /* We may need to adjust the position of the cursor to ensure it's clamped to the range. Grab it now - so we can calculate it's absolute position before we change the range. + so we can calculate its absolute position before we change the range. */ result = ma_data_source_get_cursor_in_pcm_frames(pDataSource, &relativeCursor); if (result == MA_SUCCESS) { @@ -57698,7 +58260,7 @@ MA_API ma_result ma_data_source_set_range_in_pcm_frames(ma_data_source* pDataSou /* Seek to within range. Note that our seek positions here are relative to the new range. We don't want - do do this if we failed to retrieve the cursor earlier on because it probably means the data source + to do this if we failed to retrieve the cursor earlier on because it probably means the data source has no notion of a cursor. In practice the seek would probably fail (which we silently ignore), but I'm just not even going to attempt it. */ @@ -57717,6 +58279,13 @@ MA_API void ma_data_source_get_range_in_pcm_frames(const ma_data_source* pDataSo { const ma_data_source_base* pDataSourceBase = (const ma_data_source_base*)pDataSource; + if (pRangeBegInFrames != NULL) { + *pRangeBegInFrames = 0; + } + if (pRangeEndInFrames != NULL) { + *pRangeEndInFrames = 0; + } + if (pDataSource == NULL) { return; } @@ -57761,6 +58330,13 @@ MA_API void ma_data_source_get_loop_point_in_pcm_frames(const ma_data_source* pD { const ma_data_source_base* pDataSourceBase = (const ma_data_source_base*)pDataSource; + if (pLoopBegInFrames != NULL) { + *pLoopBegInFrames = 0; + } + if (pLoopEndInFrames != NULL) { + *pLoopEndInFrames = 0; + } + if (pDataSource == NULL) { return; } @@ -59155,7 +59731,7 @@ static ma_result ma_default_vfs_seek__win32(ma_vfs* pVFS, ma_vfs_file file, ma_i result = ma_SetFilePointerEx((HANDLE)file, liDistanceToMove, NULL, dwMoveMethod); } else if (ma_SetFilePointer != NULL) { /* No SetFilePointerEx() so restrict to 31 bits. */ - if (origin > 0x7FFFFFFF) { + if (offset > 0x7FFFFFFF) { return MA_OUT_OF_RANGE; } @@ -59365,7 +59941,7 @@ static ma_result ma_default_vfs_seek__stdio(ma_vfs* pVFS, ma_vfs_file file, ma_i result = _fseeki64((FILE*)file, offset, whence); #else /* No _fseeki64() so restrict to 31 bits. */ - if (origin > 0x7FFFFFFF) { + if (offset > 0x7FFFFFFF) { return MA_OUT_OF_RANGE; } @@ -59758,7 +60334,7 @@ extern "C" { #define MA_DR_WAV_XSTRINGIFY(x) MA_DR_WAV_STRINGIFY(x) #define MA_DR_WAV_VERSION_MAJOR 0 #define MA_DR_WAV_VERSION_MINOR 13 -#define MA_DR_WAV_VERSION_REVISION 13 +#define MA_DR_WAV_VERSION_REVISION 18 #define MA_DR_WAV_VERSION_STRING MA_DR_WAV_XSTRINGIFY(MA_DR_WAV_VERSION_MAJOR) "." MA_DR_WAV_XSTRINGIFY(MA_DR_WAV_VERSION_MINOR) "." MA_DR_WAV_XSTRINGIFY(MA_DR_WAV_VERSION_REVISION) #include #define MA_DR_WAVE_FORMAT_PCM 0x1 @@ -60178,7 +60754,7 @@ extern "C" { #define MA_DR_FLAC_XSTRINGIFY(x) MA_DR_FLAC_STRINGIFY(x) #define MA_DR_FLAC_VERSION_MAJOR 0 #define MA_DR_FLAC_VERSION_MINOR 12 -#define MA_DR_FLAC_VERSION_REVISION 42 +#define MA_DR_FLAC_VERSION_REVISION 43 #define MA_DR_FLAC_VERSION_STRING MA_DR_FLAC_XSTRINGIFY(MA_DR_FLAC_VERSION_MAJOR) "." MA_DR_FLAC_XSTRINGIFY(MA_DR_FLAC_VERSION_MINOR) "." MA_DR_FLAC_XSTRINGIFY(MA_DR_FLAC_VERSION_REVISION) #include #if defined(_MSC_VER) && _MSC_VER >= 1700 @@ -60465,7 +61041,7 @@ extern "C" { #define MA_DR_MP3_XSTRINGIFY(x) MA_DR_MP3_STRINGIFY(x) #define MA_DR_MP3_VERSION_MAJOR 0 #define MA_DR_MP3_VERSION_MINOR 6 -#define MA_DR_MP3_VERSION_REVISION 38 +#define MA_DR_MP3_VERSION_REVISION 40 #define MA_DR_MP3_VERSION_STRING MA_DR_MP3_XSTRINGIFY(MA_DR_MP3_VERSION_MAJOR) "." MA_DR_MP3_XSTRINGIFY(MA_DR_MP3_VERSION_MINOR) "." MA_DR_MP3_XSTRINGIFY(MA_DR_MP3_VERSION_REVISION) #include #define MA_DR_MP3_MAX_PCM_FRAMES_PER_MP3_FRAME 1152 @@ -60627,7 +61203,7 @@ MA_API ma_decoder_config ma_decoder_config_init(ma_format outputFormat, ma_uint3 return config; } -MA_API ma_decoder_config ma_decoder_config_init_default() +MA_API ma_decoder_config ma_decoder_config_init_default(void) { return ma_decoder_config_init(ma_format_unknown, 0, 0); } @@ -63220,7 +63796,7 @@ MA_API ma_result ma_stbvorbis_init(ma_read_proc onRead, ma_seek_proc onSeek, ma_ #if !defined(MA_NO_VORBIS) { /* - stb_vorbis lacks a callback based API for it's pulling API which means we're stuck with the + stb_vorbis lacks a callback based API for its pulling API which means we're stuck with the pushing API. In order for us to be able to successfully initialize the decoder we need to supply it with enough data. We need to keep loading data until we have enough. */ @@ -63301,7 +63877,7 @@ MA_API ma_result ma_stbvorbis_init_memory(const void* pData, size_t dataSize, co { (void)pAllocationCallbacks; - /* stb_vorbis uses an int as it's size specifier, restricting it to 32-bit even on 64-bit systems. *sigh*. */ + /* stb_vorbis uses an int as its size specifier, restricting it to 32-bit even on 64-bit systems. *sigh*. */ if (dataSize > INT_MAX) { return MA_TOO_BIG; } @@ -63391,7 +63967,7 @@ MA_API ma_result ma_stbvorbis_read_pcm_frames(ma_stbvorbis* pVorbis, void* pFram /* The first thing to do is read from any already-cached frames. */ ma_uint32 framesToReadFromCache = (ma_uint32)ma_min(pVorbis->push.framesRemaining, (frameCount - totalFramesRead)); /* Safe cast because pVorbis->framesRemaining is 32-bit. */ - /* The output pointer can be null in which case we just treate it as a seek. */ + /* The output pointer can be null in which case we just treat it as a seek. */ if (pFramesOut != NULL) { ma_uint64 iFrame; for (iFrame = 0; iFrame < framesToReadFromCache; iFrame += 1) { @@ -63465,7 +64041,7 @@ MA_API ma_result ma_stbvorbis_read_pcm_frames(ma_stbvorbis* pVorbis, void* pFram } } - /* If we don't have a success code at this point it means we've encounted an error or the end of the file has been reached (probably the latter). */ + /* If we don't have a success code at this point it means we've encountered an error or the end of the file has been reached (probably the latter). */ if (result != MA_SUCCESS) { break; } @@ -64279,8 +64855,7 @@ MA_API ma_result ma_decoder_init_memory(const void* pData, size_t dataSize, cons #if defined(MA_HAS_WAV) || \ defined(MA_HAS_MP3) || \ defined(MA_HAS_FLAC) || \ - defined(MA_HAS_VORBIS) || \ - defined(MA_HAS_OPUS) + defined(MA_HAS_VORBIS) #define MA_HAS_PATH_API #endif @@ -65095,7 +65670,7 @@ MA_API ma_result ma_decoder_read_pcm_frames(ma_decoder* pDecoder, void* pFramesO } else { /* Getting here means we need to do data conversion. If we're seeking forward and are _not_ doing resampling we can run this in a fast path. If we're doing resampling we - need to run through each sample because we need to ensure it's internal cache is updated. + need to run through each sample because we need to ensure its internal cache is updated. */ if (pFramesOut == NULL && pDecoder->converter.hasResampler == MA_FALSE) { result = ma_data_source_read_pcm_frames(pDecoder->pBackend, NULL, frameCount, &totalFramesReadOut); @@ -65185,8 +65760,17 @@ MA_API ma_result ma_decoder_read_pcm_frames(ma_decoder* pDecoder, void* pFramesO if (requiredInputFrameCount > 0) { result = ma_data_source_read_pcm_frames(pDecoder->pBackend, pIntermediaryBuffer, framesToReadThisIterationIn, &framesReadThisIterationIn); + + /* + Note here that even if we've reached the end, we don't want to abort because there might be more output frames needing to be + generated from cached input data, which might happen if resampling is being performed. + */ + if (result != MA_SUCCESS && result != MA_AT_END) { + break; + } } else { framesReadThisIterationIn = 0; + pIntermediaryBuffer[0] = 0; /* <-- This is just to silence a static analysis warning. */ } /* @@ -66667,7 +67251,7 @@ MA_API ma_result ma_noise_set_type(ma_noise* pNoise, ma_noise_type type) /* This function should never have been implemented in the first place. Changing the type dynamically is not - supported. Instead you need to uninitialize and reinitiailize a fresh `ma_noise` object. This function + supported. Instead you need to uninitialize and reinitialize a fresh `ma_noise` object. This function will be removed in version 0.12. */ MA_ASSERT(MA_FALSE); @@ -67713,7 +68297,7 @@ MA_API ma_result ma_resource_manager_init(const ma_resource_manager_config* pCon pResourceManager->config.pVFS = &pResourceManager->defaultVFS; } - /* If threading has been disabled at compile time, enfore it at run time as well. */ + /* If threading has been disabled at compile time, enforce it at run time as well. */ #ifdef MA_NO_THREADING { pResourceManager->config.flags |= MA_RESOURCE_MANAGER_FLAG_NO_THREADING; @@ -67750,15 +68334,17 @@ MA_API ma_result ma_resource_manager_init(const ma_resource_manager_config* pCon /* Custom decoding backends. */ if (pConfig->ppCustomDecodingBackendVTables != NULL && pConfig->customDecodingBackendCount > 0) { size_t sizeInBytes = sizeof(*pResourceManager->config.ppCustomDecodingBackendVTables) * pConfig->customDecodingBackendCount; + ma_decoding_backend_vtable** ppCustomDecodingBackendVTables; - pResourceManager->config.ppCustomDecodingBackendVTables = (ma_decoding_backend_vtable**)ma_malloc(sizeInBytes, &pResourceManager->config.allocationCallbacks); + ppCustomDecodingBackendVTables = (ma_decoding_backend_vtable**)ma_malloc(sizeInBytes, &pResourceManager->config.allocationCallbacks); if (pResourceManager->config.ppCustomDecodingBackendVTables == NULL) { ma_job_queue_uninit(&pResourceManager->jobQueue, &pResourceManager->config.allocationCallbacks); return MA_OUT_OF_MEMORY; } - MA_COPY_MEMORY(pResourceManager->config.ppCustomDecodingBackendVTables, pConfig->ppCustomDecodingBackendVTables, sizeInBytes); + MA_COPY_MEMORY(ppCustomDecodingBackendVTables, pConfig->ppCustomDecodingBackendVTables, sizeInBytes); + pResourceManager->config.ppCustomDecodingBackendVTables = ppCustomDecodingBackendVTables; pResourceManager->config.customDecodingBackendCount = pConfig->customDecodingBackendCount; pResourceManager->config.pCustomDecodingBackendUserData = pConfig->pCustomDecodingBackendUserData; } @@ -67809,7 +68395,7 @@ static void ma_resource_manager_delete_all_data_buffer_nodes(ma_resource_manager ma_resource_manager_data_buffer_node* pDataBufferNode = pResourceManager->pRootDataBufferNode; ma_resource_manager_data_buffer_node_remove(pResourceManager, pDataBufferNode); - /* The data buffer has been removed from the BST, so now we need to free it's data. */ + /* The data buffer has been removed from the BST, so now we need to free its data. */ ma_resource_manager_data_buffer_node_free(pResourceManager, pDataBufferNode); } } @@ -67822,7 +68408,7 @@ MA_API void ma_resource_manager_uninit(ma_resource_manager* pResourceManager) /* Job threads need to be killed first. To do this we need to post a quit message to the message queue and then wait for the thread. The quit message will never be removed from the - queue which means it will never not be returned after being encounted for the first time which means all threads will eventually receive it. + queue which means it will never not be returned after being encountered for the first time which means all threads will eventually receive it. */ ma_resource_manager_post_job_quit(pResourceManager); @@ -67862,7 +68448,7 @@ MA_API void ma_resource_manager_uninit(ma_resource_manager* pResourceManager) #endif } - ma_free(pResourceManager->config.ppCustomDecodingBackendVTables, &pResourceManager->config.allocationCallbacks); + ma_free((ma_decoding_backend_vtable**)pResourceManager->config.ppCustomDecodingBackendVTables, &pResourceManager->config.allocationCallbacks); /* <-- Naughty const-cast, but this is safe. */ if (pResourceManager->config.pLog == &pResourceManager->log) { ma_log_uninit(&pResourceManager->log); @@ -68280,7 +68866,7 @@ static ma_result ma_resource_manager_data_buffer_node_decode_next_page(ma_resour } result = ma_decoder_read_pcm_frames(pDecoder, pPage->pAudioData, framesToTryReading, &framesRead); - if (framesRead > 0) { + if (result == MA_SUCCESS && framesRead > 0) { pPage->sizeInFrames = framesRead; result = ma_paged_audio_buffer_data_append_page(&pDataBufferNode->data.backend.decodedPaged.data, pPage); @@ -68433,7 +69019,7 @@ static ma_result ma_resource_manager_data_buffer_node_acquire_critical_section(m if ((flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT) != 0) { ma_resource_manager_inline_notification_uninit(pInitNotification); } else { - /* These will have been freed by the job thread, but with WAIT_INIT they will already have happend sinced the job has already been handled. */ + /* These will have been freed by the job thread, but with WAIT_INIT they will already have happened since the job has already been handled. */ ma_free(pFilePathCopy, &pResourceManager->config.allocationCallbacks); ma_free(pFilePathWCopy, &pResourceManager->config.allocationCallbacks); } @@ -68798,6 +69384,10 @@ static ma_result ma_resource_manager_data_buffer_init_ex_internal(ma_resource_ma flags &= ~MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC; } + if (pConfig->isLooping) { + flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } + async = (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC) != 0; /* @@ -68810,7 +69400,7 @@ static ma_result ma_resource_manager_data_buffer_init_ex_internal(ma_resource_ma These fences are always released at the "done" tag at the end of this function. They'll be acquired a second if loading asynchronously. This double acquisition system is just done to - simplify code maintanence. + simplify code maintenance. */ ma_resource_manager_pipeline_notifications_acquire_all_fences(¬ifications); { @@ -68855,7 +69445,7 @@ static ma_result ma_resource_manager_data_buffer_init_ex_internal(ma_resource_ma /* The status of the data buffer needs to be set to MA_BUSY before posting the job so that the - worker thread is aware of it's busy state. If the LOAD_DATA_BUFFER job sees a status other + worker thread is aware of its busy state. If the LOAD_DATA_BUFFER job sees a status other than MA_BUSY, it'll assume an error and fall through to an early exit. */ ma_atomic_exchange_i32(&pDataBuffer->result, MA_BUSY); @@ -68874,7 +69464,7 @@ static ma_result ma_resource_manager_data_buffer_init_ex_internal(ma_resource_ma job.data.resourceManager.loadDataBuffer.rangeEndInPCMFrames = pConfig->rangeEndInPCMFrames; job.data.resourceManager.loadDataBuffer.loopPointBegInPCMFrames = pConfig->loopPointBegInPCMFrames; job.data.resourceManager.loadDataBuffer.loopPointEndInPCMFrames = pConfig->loopPointEndInPCMFrames; - job.data.resourceManager.loadDataBuffer.isLooping = pConfig->isLooping; + job.data.resourceManager.loadDataBuffer.isLooping = (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING) != 0; /* If we need to wait for initialization to complete we can just process the job in place. */ if ((flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT) != 0) { @@ -69095,22 +69685,29 @@ MA_API ma_result ma_resource_manager_data_buffer_read_pcm_frames(ma_resource_man isDecodedBufferBusy = (ma_resource_manager_data_buffer_node_result(pDataBuffer->pNode) == MA_BUSY); if (ma_resource_manager_data_buffer_get_available_frames(pDataBuffer, &availableFrames) == MA_SUCCESS) { - /* Don't try reading more than the available frame count. */ - if (frameCount > availableFrames) { - frameCount = availableFrames; + /* Don't try reading more than the available frame count if the data buffer node is still loading. */ + if (isDecodedBufferBusy) { + if (frameCount > availableFrames) { + frameCount = availableFrames; - /* - If there's no frames available we want to set the status to MA_AT_END. The logic below - will check if the node is busy, and if so, change it to MA_BUSY. The reason we do this - is because we don't want to call `ma_data_source_read_pcm_frames()` if the frame count - is 0 because that'll result in a situation where it's possible MA_AT_END won't get - returned. - */ - if (frameCount == 0) { - result = MA_AT_END; + /* + If there's no frames available we want to set the status to MA_AT_END. The logic below + will check if the node is busy, and if so, change it to MA_BUSY. The reason we do this + is because we don't want to call `ma_data_source_read_pcm_frames()` if the frame count + is 0 because that'll result in a situation where it's possible MA_AT_END won't get + returned. + */ + if (frameCount == 0) { + result = MA_AT_END; + } + } else { + isDecodedBufferBusy = MA_FALSE; /* We have enough frames available in the buffer to avoid a MA_BUSY status. */ } } else { - isDecodedBufferBusy = MA_FALSE; /* We have enough frames available in the buffer to avoid a MA_BUSY status. */ + /* + Getting here means the buffer has been fully loaded. We can just pass the frame count straight + into ma_data_source_read_pcm_frames() below and let ma_data_source handle it. + */ } } } @@ -69510,6 +70107,7 @@ MA_API ma_result ma_resource_manager_data_stream_init_ex(ma_resource_manager* pR ma_bool32 waitBeforeReturning = MA_FALSE; ma_resource_manager_inline_notification waitNotification; ma_resource_manager_pipeline_notifications notifications; + ma_uint32 flags; if (pDataStream == NULL) { if (pConfig != NULL && pConfig->pNotifications != NULL) { @@ -69540,13 +70138,18 @@ MA_API ma_result ma_resource_manager_data_stream_init_ex(ma_resource_manager* pR return result; } + flags = pConfig->flags; + if (pConfig->isLooping) { + flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } + pDataStream->pResourceManager = pResourceManager; pDataStream->flags = pConfig->flags; pDataStream->result = MA_BUSY; ma_data_source_set_range_in_pcm_frames(pDataStream, pConfig->rangeBegInPCMFrames, pConfig->rangeEndInPCMFrames); ma_data_source_set_loop_point_in_pcm_frames(pDataStream, pConfig->loopPointBegInPCMFrames, pConfig->loopPointEndInPCMFrames); - ma_data_source_set_looping(pDataStream, pConfig->isLooping); + ma_data_source_set_looping(pDataStream, (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING) != 0); if (pResourceManager == NULL || (pConfig->pFilePath == NULL && pConfig->pFilePathW == NULL)) { ma_resource_manager_pipeline_notifications_signal_all_notifications(¬ifications); @@ -70168,6 +70771,9 @@ static ma_result ma_resource_manager_data_source_preinit(ma_resource_manager* pR } pDataSource->flags = pConfig->flags; + if (pConfig->isLooping) { + pDataSource->flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } return MA_SUCCESS; } @@ -70726,9 +71332,10 @@ static ma_result ma_job_process__resource_manager__load_data_buffer(ma_job* pJob */ result = ma_resource_manager_data_buffer_result(pDataBuffer); if (result != MA_BUSY) { - goto done; /* <-- This will ensure the exucution pointer is incremented. */ + goto done; /* <-- This will ensure the execution pointer is incremented. */ } else { result = MA_SUCCESS; /* <-- Make sure this is reset. */ + (void)result; /* <-- This is to suppress a static analysis diagnostic about "result" not being used. But for safety when I do future maintenance I don't want to delete that assignment. */ } /* Try initializing the connector if we haven't already. */ @@ -71075,11 +71682,74 @@ static ma_result ma_job_process__resource_manager__seek_data_stream(ma_job* pJob #ifndef MA_NO_NODE_GRAPH + +static ma_stack* ma_stack_init(size_t sizeInBytes, const ma_allocation_callbacks* pAllocationCallbacks) +{ + ma_stack* pStack; + + if (sizeInBytes == 0) { + return NULL; + } + + pStack = (ma_stack*)ma_malloc(sizeof(*pStack) - sizeof(pStack->_data) + sizeInBytes, pAllocationCallbacks); + if (pStack == NULL) { + return NULL; + } + + pStack->offset = 0; + pStack->sizeInBytes = sizeInBytes; + + return pStack; +} + +static void ma_stack_uninit(ma_stack* pStack, const ma_allocation_callbacks* pAllocationCallbacks) +{ + if (pStack == NULL) { + return; + } + + ma_free(pStack, pAllocationCallbacks); +} + +static void* ma_stack_alloc(ma_stack* pStack, size_t sz) +{ + /* The size of the allocation is stored in the memory directly before the pointer. This needs to include padding to keep it aligned to ma_uintptr */ + void* p = (void*)((char*)pStack->_data + pStack->offset); + size_t* pSize = (size_t*)p; + + sz = (sz + (sizeof(ma_uintptr) - 1)) & ~(sizeof(ma_uintptr) - 1); /* Padding. */ + if (pStack->offset + sz + sizeof(size_t) > pStack->sizeInBytes) { + return NULL; /* Out of memory. */ + } + + pStack->offset += sz + sizeof(size_t); + + *pSize = sz; + return (void*)((char*)p + sizeof(size_t)); +} + +static void ma_stack_free(ma_stack* pStack, void* p) +{ + size_t* pSize; + + if (p == NULL) { + return; + } + + pSize = (size_t*)p - 1; + pStack->offset -= *pSize + sizeof(size_t); +} + + + /* 10ms @ 48K = 480. Must never exceed 65535. */ #ifndef MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS #define MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS 480 #endif +#ifndef MA_DEFAULT_PREMIX_STACK_SIZE_PER_CHANNEL +#define MA_DEFAULT_PREMIX_STACK_SIZE_PER_CHANNEL 524288 +#endif static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusIndex, float* pFramesOut, ma_uint32 frameCount, ma_uint32* pFramesRead, ma_uint64 globalTime); @@ -71119,8 +71789,8 @@ MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels) ma_node_graph_config config; MA_ZERO_OBJECT(&config); - config.channels = channels; - config.nodeCacheCapInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; + config.channels = channels; + config.processingSizeInFrames = 0; return config; } @@ -71207,11 +71877,7 @@ MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const m } MA_ZERO_OBJECT(pNodeGraph); - pNodeGraph->nodeCacheCapInFrames = pConfig->nodeCacheCapInFrames; - if (pNodeGraph->nodeCacheCapInFrames == 0) { - pNodeGraph->nodeCacheCapInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; - } - + pNodeGraph->processingSizeInFrames = pConfig->processingSizeInFrames; /* Base node so we can use the node graph as a node into another graph. */ baseConfig = ma_node_config_init(); @@ -71236,6 +71902,40 @@ MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const m return result; } + + /* Processing cache. */ + if (pConfig->processingSizeInFrames > 0) { + pNodeGraph->pProcessingCache = (float*)ma_malloc(pConfig->processingSizeInFrames * pConfig->channels * sizeof(float), pAllocationCallbacks); + if (pNodeGraph->pProcessingCache == NULL) { + ma_node_uninit(&pNodeGraph->endpoint, pAllocationCallbacks); + ma_node_uninit(&pNodeGraph->base, pAllocationCallbacks); + return MA_OUT_OF_MEMORY; + } + } + + + /* + We need a pre-mix stack. The size of this stack is configurable via the config. The default value depends on the channel count. + */ + { + size_t preMixStackSizeInBytes = pConfig->preMixStackSizeInBytes; + if (preMixStackSizeInBytes == 0) { + preMixStackSizeInBytes = pConfig->channels * MA_DEFAULT_PREMIX_STACK_SIZE_PER_CHANNEL; + } + + pNodeGraph->pPreMixStack = ma_stack_init(preMixStackSizeInBytes, pAllocationCallbacks); + if (pNodeGraph->pPreMixStack == NULL) { + ma_node_uninit(&pNodeGraph->endpoint, pAllocationCallbacks); + ma_node_uninit(&pNodeGraph->base, pAllocationCallbacks); + if (pNodeGraph->pProcessingCache != NULL) { + ma_free(pNodeGraph->pProcessingCache, pAllocationCallbacks); + } + + return MA_OUT_OF_MEMORY; + } + } + + return MA_SUCCESS; } @@ -71246,6 +71946,17 @@ MA_API void ma_node_graph_uninit(ma_node_graph* pNodeGraph, const ma_allocation_ } ma_node_uninit(&pNodeGraph->endpoint, pAllocationCallbacks); + ma_node_uninit(&pNodeGraph->base, pAllocationCallbacks); + + if (pNodeGraph->pProcessingCache != NULL) { + ma_free(pNodeGraph->pProcessingCache, pAllocationCallbacks); + pNodeGraph->pProcessingCache = NULL; + } + + if (pNodeGraph->pPreMixStack != NULL) { + ma_stack_uninit(pNodeGraph->pPreMixStack, pAllocationCallbacks); + pNodeGraph->pPreMixStack = NULL; + } } MA_API ma_node* ma_node_graph_get_endpoint(ma_node_graph* pNodeGraph) @@ -71278,27 +71989,72 @@ MA_API ma_result ma_node_graph_read_pcm_frames(ma_node_graph* pNodeGraph, void* totalFramesRead = 0; while (totalFramesRead < frameCount) { ma_uint32 framesJustRead; - ma_uint64 framesToRead = frameCount - totalFramesRead; + ma_uint64 framesToRead; + float* pRunningFramesOut; + framesToRead = frameCount - totalFramesRead; if (framesToRead > 0xFFFFFFFF) { framesToRead = 0xFFFFFFFF; } - ma_node_graph_set_is_reading(pNodeGraph, MA_TRUE); - { - result = ma_node_read_pcm_frames(&pNodeGraph->endpoint, 0, (float*)ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, ma_format_f32, channels), (ma_uint32)framesToRead, &framesJustRead, ma_node_get_time(&pNodeGraph->endpoint)); - } - ma_node_graph_set_is_reading(pNodeGraph, MA_FALSE); + pRunningFramesOut = (float*)ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, ma_format_f32, channels); - totalFramesRead += framesJustRead; + /* If there's anything in the cache, consume that first. */ + if (pNodeGraph->processingCacheFramesRemaining > 0) { + ma_uint32 framesToReadFromCache; - if (result != MA_SUCCESS) { - break; - } + framesToReadFromCache = (ma_uint32)framesToRead; + if (framesToReadFromCache > pNodeGraph->processingCacheFramesRemaining) { + framesToReadFromCache = pNodeGraph->processingCacheFramesRemaining; + } - /* Abort if we weren't able to read any frames or else we risk getting stuck in a loop. */ - if (framesJustRead == 0) { - break; + MA_COPY_MEMORY(pRunningFramesOut, pNodeGraph->pProcessingCache, framesToReadFromCache * channels * sizeof(float)); + MA_MOVE_MEMORY(pNodeGraph->pProcessingCache, pNodeGraph->pProcessingCache + (framesToReadFromCache * channels), (pNodeGraph->processingCacheFramesRemaining - framesToReadFromCache) * channels * sizeof(float)); + pNodeGraph->processingCacheFramesRemaining -= framesToReadFromCache; + + totalFramesRead += framesToReadFromCache; + continue; + } else { + /* + If processingSizeInFrames is non-zero, we need to make sure we always read in chunks of that size. If the frame count is less than + that, we need to read into the cache and then continue on. + */ + float* pReadDst = pRunningFramesOut; + + if (pNodeGraph->processingSizeInFrames > 0) { + if (framesToRead < pNodeGraph->processingSizeInFrames) { + pReadDst = pNodeGraph->pProcessingCache; /* We need to read into the cache because otherwise we'll overflow the output buffer. */ + } + + framesToRead = pNodeGraph->processingSizeInFrames; + } + + ma_node_graph_set_is_reading(pNodeGraph, MA_TRUE); + { + result = ma_node_read_pcm_frames(&pNodeGraph->endpoint, 0, pReadDst, (ma_uint32)framesToRead, &framesJustRead, ma_node_get_time(&pNodeGraph->endpoint)); + } + ma_node_graph_set_is_reading(pNodeGraph, MA_FALSE); + + /* + Do not increment the total frames read counter if we read into the cache. We use this to determine how many frames have + been written to the final output buffer. + */ + if (pReadDst == pNodeGraph->pProcessingCache) { + /* We read into the cache. */ + pNodeGraph->processingCacheFramesRemaining = framesJustRead; + } else { + /* We read straight into the output buffer. */ + totalFramesRead += framesJustRead; + } + + if (result != MA_SUCCESS) { + break; + } + + /* Abort if we weren't able to read any frames or else we risk getting stuck in a loop. */ + if (framesJustRead == 0) { + break; + } } } @@ -71499,7 +72255,7 @@ static void ma_node_input_bus_detach__no_output_bus_lock(ma_node_input_bus* pInp *not* using a lock when iterating over the list in the audio thread. We therefore need to craft this in a way such that the iteration on the audio thread doesn't break. - The the first thing to do is swap out the "next" pointer of the previous output bus with the + The first thing to do is swap out the "next" pointer of the previous output bus with the new "next" output bus. This is the operation that matters for iteration on the audio thread. After that, the previous pointer on the new "next" pointer needs to be updated, after which point the linked list will be in a good state. @@ -71592,7 +72348,7 @@ static void ma_node_input_bus_attach(ma_node_input_bus* pInputBus, ma_node_outpu /* Now we need to attach the output bus to the linked list. This involves updating two pointers on two different output buses so I'm going to go ahead and keep this simple and just use a lock. - There are ways to do this without a lock, but it's just too hard to maintain for it's value. + There are ways to do this without a lock, but it's just too hard to maintain for its value. Although we're locking here, it's important to remember that we're *not* locking when iterating and reading audio data since that'll be running on the audio thread. As a result we need to be @@ -71685,11 +72441,9 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ ma_uint32 inputChannels; ma_bool32 doesOutputBufferHaveContent = MA_FALSE; - (void)pInputNode; /* Not currently used. */ - /* This will be called from the audio thread which means we can't be doing any locking. Basically, - this function will not perfom any locking, whereas attaching and detaching will, but crafted in + this function will not perform any locking, whereas attaching and detaching will, but crafted in such a way that we don't need to perform any locking here. The important thing to remember is to always iterate in a forward direction. @@ -71735,19 +72489,12 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ if (pFramesOut != NULL) { /* Read. */ - float temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE / sizeof(float)]; - ma_uint32 tempCapInFrames = ma_countof(temp) / inputChannels; - while (framesProcessed < frameCount) { float* pRunningFramesOut; ma_uint32 framesToRead; - ma_uint32 framesJustRead; + ma_uint32 framesJustRead = 0; framesToRead = frameCount - framesProcessed; - if (framesToRead > tempCapInFrames) { - framesToRead = tempCapInFrames; - } - pRunningFramesOut = ma_offset_pcm_frames_ptr_f32(pFramesOut, framesProcessed, inputChannels); if (doesOutputBufferHaveContent == MA_FALSE) { @@ -71755,11 +72502,32 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ result = ma_node_read_pcm_frames(pOutputBus->pNode, pOutputBus->outputBusIndex, pRunningFramesOut, framesToRead, &framesJustRead, globalTime + framesProcessed); } else { /* Slow path. Not the first attachment. Mixing required. */ - result = ma_node_read_pcm_frames(pOutputBus->pNode, pOutputBus->outputBusIndex, temp, framesToRead, &framesJustRead, globalTime + framesProcessed); - if (result == MA_SUCCESS || result == MA_AT_END) { - if (isSilentOutput == MA_FALSE) { /* Don't mix if the node outputs silence. */ - ma_mix_pcm_frames_f32(pRunningFramesOut, temp, framesJustRead, inputChannels, /*volume*/1); + ma_uint32 preMixBufferCapInFrames = ((ma_node_base*)pInputNode)->cachedDataCapInFramesPerBus; + float* pPreMixBuffer = (float*)ma_stack_alloc(((ma_node_base*)pInputNode)->pNodeGraph->pPreMixStack, preMixBufferCapInFrames * inputChannels * sizeof(float)); + + if (pPreMixBuffer == NULL) { + /* + If you're hitting this assert it means you've got an unusually deep chain of nodes, you've got an excessively large processing + size, or you have a combination of both, and as a result have run out of stack space. You can increase this using the + preMixStackSizeInBytes variable in ma_node_graph_config. If you're using ma_engine, you can do it via the preMixStackSizeInBytes + variable in ma_engine_config. It defaults to 512KB per output channel. + */ + MA_ASSERT(MA_FALSE); + } else { + if (framesToRead > preMixBufferCapInFrames) { + framesToRead = preMixBufferCapInFrames; } + + result = ma_node_read_pcm_frames(pOutputBus->pNode, pOutputBus->outputBusIndex, pPreMixBuffer, framesToRead, &framesJustRead, globalTime + framesProcessed); + if (result == MA_SUCCESS || result == MA_AT_END) { + if (isSilentOutput == MA_FALSE) { /* Don't mix if the node outputs silence. */ + ma_mix_pcm_frames_f32(pRunningFramesOut, pPreMixBuffer, framesJustRead, inputChannels, /*volume*/1); + } + } + + /* The pre-mix buffer is no longer required. */ + ma_stack_free(((ma_node_base*)pInputNode)->pNodeGraph->pPreMixStack, pPreMixBuffer); + pPreMixBuffer = NULL; } } @@ -71814,6 +72582,25 @@ MA_API ma_node_config ma_node_config_init(void) return config; } +static ma_uint16 ma_node_config_get_cache_size_in_frames(const ma_node_config* pConfig, const ma_node_graph* pNodeGraph) +{ + ma_uint32 cacheSizeInFrames; + + (void)pConfig; + + if (pNodeGraph->processingSizeInFrames > 0) { + cacheSizeInFrames = pNodeGraph->processingSizeInFrames; + } else { + cacheSizeInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; + } + + if (cacheSizeInFrames > 0xFFFF) { + cacheSizeInFrames = 0xFFFF; + } + + return (ma_uint16)cacheSizeInFrames; +} + static ma_result ma_node_detach_full(ma_node* pNode); @@ -71968,7 +72755,7 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod /* Cached audio data. - We need to allocate memory for a caching both input and output data. We have an optimization + We need to allocate memory for caching both input and output data. We have an optimization where no caching is necessary for specific conditions: - The node has 0 inputs and 1 output. @@ -71987,14 +72774,18 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod } else { /* Slow path. Cache needed. */ size_t cachedDataSizeInBytes = 0; + ma_uint32 cacheCapInFrames; ma_uint32 iBus; + /* The capacity of the cache is based on our callback processing size. */ + cacheCapInFrames = ma_node_config_get_cache_size_in_frames(pConfig, pNodeGraph); + for (iBus = 0; iBus < inputBusCount; iBus += 1) { - cachedDataSizeInBytes += pNodeGraph->nodeCacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pInputChannels[iBus]); + cachedDataSizeInBytes += cacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pInputChannels[iBus]); } for (iBus = 0; iBus < outputBusCount; iBus += 1) { - cachedDataSizeInBytes += pNodeGraph->nodeCacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pOutputChannels[iBus]); + cachedDataSizeInBytes += cacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pOutputChannels[iBus]); } pHeapLayout->cachedDataOffset = pHeapLayout->sizeInBytes; @@ -72080,13 +72871,12 @@ MA_API ma_result ma_node_init_preallocated(ma_node_graph* pNodeGraph, const ma_n if (heapLayout.cachedDataOffset != MA_SIZE_MAX) { pNodeBase->pCachedData = (float*)ma_offset_ptr(pHeap, heapLayout.cachedDataOffset); - pNodeBase->cachedDataCapInFramesPerBus = pNodeGraph->nodeCacheCapInFrames; + pNodeBase->cachedDataCapInFramesPerBus = ma_node_config_get_cache_size_in_frames(pConfig, pNodeGraph); } else { pNodeBase->pCachedData = NULL; } - /* We need to run an initialization step for each input and output bus. */ for (iInputBus = 0; iInputBus < ma_node_get_input_bus_count(pNodeBase); iInputBus += 1) { result = ma_node_input_bus_init(pConfig->pInputChannels[iInputBus], &pNodeBase->pInputBuses[iInputBus]); @@ -72260,7 +73050,7 @@ static ma_result ma_node_detach_full(ma_node* pNode) /* At this point all output buses will have been detached from the graph and we can be guaranteed - that none of it's input nodes will be getting processed by the graph. We can detach these + that none of its input nodes will be getting processed by the graph. We can detach these without needing to worry about the audio thread touching them. */ for (iInputBus = 0; iInputBus < ma_node_get_input_bus_count(pNode); iInputBus += 1) { @@ -72275,7 +73065,7 @@ static ma_result ma_node_detach_full(ma_node* pNode) linked list logic. We don't need to worry about the audio thread referencing these because the step above severed the connection to the graph. */ - for (pOutputBus = (ma_node_output_bus*)ma_atomic_load_ptr(&pInputBus->head.pNext); pOutputBus != NULL; pOutputBus = (ma_node_output_bus*)ma_atomic_load_ptr(&pOutputBus->pNext)) { + for (pOutputBus = (ma_node_output_bus*)ma_atomic_load_ptr(&pInputBus->head.pNext); pOutputBus != NULL; pOutputBus = (ma_node_output_bus*)ma_atomic_load_ptr(&pInputBus->head.pNext)) { ma_node_detach_output_bus(pOutputBus->pNode, pOutputBus->outputBusIndex); /* This won't do any waiting in practice and should be efficient. */ } } @@ -72297,7 +73087,7 @@ MA_API ma_result ma_node_detach_output_bus(ma_node* pNode, ma_uint32 outputBusIn return MA_INVALID_ARGS; /* Invalid output bus index. */ } - /* We need to lock the output bus because we need to inspect the input node and grab it's input bus. */ + /* We need to lock the output bus because we need to inspect the input node and grab its input bus. */ ma_node_output_bus_lock(&pNodeBase->pOutputBuses[outputBusIndex]); { pInputNodeBase = (ma_node_base*)pNodeBase->pOutputBuses[outputBusIndex].pInputNode; @@ -72463,7 +73253,7 @@ MA_API ma_node_state ma_node_get_state_by_time_range(const ma_node* pNode, ma_ui /* Getting here means the node is marked as started, but it may still not be truly started due to - it's start time not having been reached yet. Also, the stop time may have also been reached in + its start time not having been reached yet. Also, the stop time may have also been reached in which case it'll be considered stopped. */ if (ma_node_get_state_time(pNode, ma_node_state_started) > globalTimeBeg) { @@ -72474,7 +73264,7 @@ MA_API ma_node_state ma_node_get_state_by_time_range(const ma_node* pNode, ma_ui return ma_node_state_stopped; /* Stop time has been reached. */ } - /* Getting here means the node is marked as started and is within it's start/stop times. */ + /* Getting here means the node is marked as started and is within its start/stop times. */ return ma_node_state_started; } @@ -72636,12 +73426,12 @@ static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusInde frameCountOut = totalFramesRead; if (totalFramesRead > 0) { - ma_node_process_pcm_frames_internal(pNode, (const float**)ppFramesIn, &frameCountIn, ppFramesOut, &frameCountOut); /* From GCC: expected 'const float **' but argument is of type 'float **'. Shouldn't this be implicit? Excplicit cast to silence the warning. */ + ma_node_process_pcm_frames_internal(pNode, (const float**)ppFramesIn, &frameCountIn, ppFramesOut, &frameCountOut); /* From GCC: expected 'const float **' but argument is of type 'float **'. Shouldn't this be implicit? Explicit cast to silence the warning. */ } /* A passthrough should never have modified the input and output frame counts. If you're - triggering these assers you need to fix your processing callback. + triggering these asserts you need to fix your processing callback. */ MA_ASSERT(frameCountIn == totalFramesRead); MA_ASSERT(frameCountOut == totalFramesRead); @@ -72819,7 +73609,7 @@ static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusInde frames available right now. */ if (frameCountIn > 0 || (pNodeBase->vtable->flags & MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES) != 0) { - ma_node_process_pcm_frames_internal(pNode, (const float**)ppFramesIn, &frameCountIn, ppFramesOut, &frameCountOut); /* From GCC: expected 'const float **' but argument is of type 'float **'. Shouldn't this be implicit? Excplicit cast to silence the warning. */ + ma_node_process_pcm_frames_internal(pNode, (const float**)ppFramesIn, &frameCountIn, ppFramesOut, &frameCountOut); /* From GCC: expected 'const float **' but argument is of type 'float **'. Shouldn't this be implicit? Explicit cast to silence the warning. */ } else { frameCountOut = 0; /* No data was processed. */ } @@ -74056,7 +74846,7 @@ static ma_bool32 ma_engine_node_is_pitching_enabled(const ma_engine_node* pEngin { MA_ASSERT(pEngineNode != NULL); - /* Don't try to be clever by skiping resampling in the pitch=1 case or else you'll glitch when moving away from 1. */ + /* Don't try to be clever by skipping resampling in the pitch=1 case or else you'll glitch when moving away from 1. */ return !ma_atomic_load_explicit_32(&pEngineNode->isPitchDisabled, ma_atomic_memory_order_acquire); } @@ -74093,7 +74883,7 @@ static ma_result ma_engine_node_set_volume(ma_engine_node* pEngineNode, float vo /* If we're not smoothing we should bypass the volume gainer entirely. */ if (pEngineNode->volumeSmoothTimeInPCMFrames == 0) { - /* We should always have an active spatializer because it can be enabled and disabled dynamically. We can just use that for hodling our volume. */ + /* We should always have an active spatializer because it can be enabled and disabled dynamically. We can just use that for holding our volume. */ ma_spatializer_set_master_volume(&pEngineNode->spatializer, volume); } else { /* We're using volume smoothing, so apply the master volume to the gainer. */ @@ -74408,7 +75198,7 @@ static void ma_engine_node_process_pcm_frames__sound(ma_node* pNode, const float ma_sound_set_at_end(pSound, MA_TRUE); /* This will be set to false in ma_sound_start(). */ } - pRunningFramesOut = ma_offset_pcm_frames_ptr_f32(ppFramesOut[0], totalFramesRead, ma_engine_get_channels(ma_sound_get_engine(pSound))); + pRunningFramesOut = ma_offset_pcm_frames_ptr_f32(ppFramesOut[0], totalFramesRead, ma_node_get_output_channels(pNode, 0)); frameCountIn = (ma_uint32)framesJustRead; frameCountOut = framesRemaining; @@ -74739,7 +75529,7 @@ MA_API ma_result ma_engine_node_init_preallocated(const ma_engine_node_config* p /* - Spatialization comes next. We spatialize based ont he node's output channel count. It's up the caller to + Spatialization comes next. We spatialize based on the node's output channel count. It's up the caller to ensure channels counts link up correctly in the node graph. */ spatializerConfig = ma_engine_node_spatializer_config_init(&baseNodeConfig); @@ -74929,6 +75719,21 @@ static void ma_engine_data_callback_internal(ma_device* pDevice, void* pFramesOu ma_engine_read_pcm_frames(pEngine, pFramesOut, frameCount, NULL); } + +static ma_uint32 ma_device__get_processing_size_in_frames(ma_device* pDevice) +{ + /* + The processing size is the period size. The device can have a fixed sized processing size, or + it can be decided by the backend in which case it can be variable. + */ + if (pDevice->playback.intermediaryBufferCap > 0) { + /* Using a fixed sized processing callback. */ + return pDevice->playback.intermediaryBufferCap; + } else { + /* Not using a fixed sized processing callback. Need to estimate the processing size based on the backend. */ + return pDevice->playback.internalPeriodSizeInFrames; + } +} #endif MA_API ma_result ma_engine_init(const ma_engine_config* pConfig, ma_engine* pEngine) @@ -75022,6 +75827,14 @@ MA_API ma_result ma_engine_init(const ma_engine_config* pConfig, ma_engine* pEng if (pEngine->pDevice != NULL) { engineConfig.channels = pEngine->pDevice->playback.channels; engineConfig.sampleRate = pEngine->pDevice->sampleRate; + + /* + The processing size used by the engine is determined by engineConfig.periodSizeInFrames. We want + to make this equal to what the device is using for it's period size. If we don't do that, it's + possible that the node graph will split it's processing into multiple passes which can introduce + glitching. + */ + engineConfig.periodSizeInFrames = ma_device__get_processing_size_in_frames(pEngine->pDevice); } } #endif @@ -75048,9 +75861,10 @@ MA_API ma_result ma_engine_init(const ma_engine_config* pConfig, ma_engine* pEng } - /* The engine is a node graph. This needs to be initialized after we have the device so we can can determine the channel count. */ + /* The engine is a node graph. This needs to be initialized after we have the device so we can determine the channel count. */ nodeGraphConfig = ma_node_graph_config_init(engineConfig.channels); - nodeGraphConfig.nodeCacheCapInFrames = (engineConfig.periodSizeInFrames > 0xFFFF) ? 0xFFFF : (ma_uint16)engineConfig.periodSizeInFrames; + nodeGraphConfig.processingSizeInFrames = engineConfig.periodSizeInFrames; + nodeGraphConfig.preMixStackSizeInBytes = engineConfig.preMixStackSizeInBytes; result = ma_node_graph_init(&nodeGraphConfig, &pEngine->allocationCallbacks, &pEngine->nodeGraph); if (result != MA_SUCCESS) { @@ -75130,8 +75944,8 @@ MA_API ma_result ma_engine_init(const ma_engine_config* pConfig, ma_engine* pEng ma_allocation_callbacks_init_copy(&resourceManagerConfig.allocationCallbacks, &pEngine->allocationCallbacks); resourceManagerConfig.pVFS = engineConfig.pResourceManagerVFS; - /* The Emscripten build cannot use threads. */ - #if defined(MA_EMSCRIPTEN) + /* The Emscripten build cannot use threads unless it's targeting pthreads. */ + #if defined(MA_EMSCRIPTEN) && !defined(__EMSCRIPTEN_PTHREADS__) { resourceManagerConfig.jobThreadCount = 0; resourceManagerConfig.flags |= MA_RESOURCE_MANAGER_FLAG_NO_THREADING; @@ -75646,7 +76460,7 @@ MA_API ma_result ma_engine_play_sound_ex(ma_engine* pEngine, const char* pFilePa return MA_INVALID_ARGS; } - /* Attach to the endpoint node if nothing is specicied. */ + /* Attach to the endpoint node if nothing is specified. */ if (pNode == NULL) { pNode = ma_node_graph_get_endpoint(&pEngine->nodeGraph); nodeInputBusIndex = 0; @@ -75863,7 +76677,7 @@ static ma_result ma_sound_init_from_data_source_internal(ma_engine* pEngine, con ma_data_source_set_range_in_pcm_frames(ma_sound_get_data_source(pSound), pConfig->loopPointBegInPCMFrames, pConfig->loopPointEndInPCMFrames); } - ma_sound_set_looping(pSound, pConfig->isLooping); + ma_sound_set_looping(pSound, pConfig->isLooping || ((pConfig->flags & MA_SOUND_FLAG_LOOPING) != 0)); return MA_SUCCESS; } @@ -75887,6 +76701,9 @@ MA_API ma_result ma_sound_init_from_file_internal(ma_engine* pEngine, const ma_s it and can avoid accessing the sound from within the notification. */ flags = pConfig->flags | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT; + if (pConfig->isLooping) { + flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } pSound->pResourceManagerDataSource = (ma_resource_manager_data_source*)ma_malloc(sizeof(*pSound->pResourceManagerDataSource), &pEngine->allocationCallbacks); if (pSound->pResourceManagerDataSource == NULL) { @@ -75915,7 +76732,7 @@ MA_API ma_result ma_sound_init_from_file_internal(ma_engine* pEngine, const ma_s resourceManagerDataSourceConfig.rangeEndInPCMFrames = pConfig->rangeEndInPCMFrames; resourceManagerDataSourceConfig.loopPointBegInPCMFrames = pConfig->loopPointBegInPCMFrames; resourceManagerDataSourceConfig.loopPointEndInPCMFrames = pConfig->loopPointEndInPCMFrames; - resourceManagerDataSourceConfig.isLooping = pConfig->isLooping; + resourceManagerDataSourceConfig.isLooping = (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING) != 0; result = ma_resource_manager_data_source_init_ex(pEngine->pResourceManager, &resourceManagerDataSourceConfig, pSound->pResourceManagerDataSource); if (result != MA_SUCCESS) { @@ -76067,7 +76884,7 @@ MA_API ma_result ma_sound_init_ex(ma_engine* pEngine, const ma_sound_config* pCo { /* Getting here means we're not loading from a file. We may be loading from an already-initialized - data source, or none at all. If we aren't specifying any data source, we'll be initializing the + data source, or none at all. If we aren't specifying any data source, we'll be initializing the equivalent to a group. ma_data_source_init_from_data_source_internal() will deal with this for us, so no special treatment required here. */ @@ -76787,6 +77604,27 @@ MA_API ma_result ma_sound_seek_to_pcm_frame(ma_sound* pSound, ma_uint64 frameInd return MA_SUCCESS; } +MA_API ma_result ma_sound_seek_to_second(ma_sound* pSound, float seekPointInSeconds) +{ + ma_uint64 frameIndex; + ma_uint32 sampleRate; + ma_result result; + + if (pSound == NULL) { + return MA_INVALID_ARGS; + } + + result = ma_sound_get_data_format(pSound, NULL, NULL, &sampleRate, NULL, 0); + if (result != MA_SUCCESS) { + return result; + } + + /* We need PCM frames. We need to convert first */ + frameIndex = (ma_uint64)(seekPointInSeconds * sampleRate); + + return ma_sound_seek_to_pcm_frame(pSound, frameIndex); +} + MA_API ma_result ma_sound_get_data_format(ma_sound* pSound, ma_format* pFormat, ma_uint32* pChannels, ma_uint32* pSampleRate, ma_channel* pChannelMap, size_t channelMapCap) { if (pSound == NULL) { @@ -77233,7 +78071,7 @@ code below please report the bug to the respective repository for the relevant p *************************************************************************************************************************************************************** **************************************************************************************************************************************************************/ #if !defined(MA_NO_WAV) && (!defined(MA_NO_DECODING) || !defined(MA_NO_ENCODING)) -#if !defined(MA_DR_WAV_IMPLEMENTATION) && !defined(MA_DR_WAV_IMPLEMENTATION) /* For backwards compatibility. Will be removed in version 0.11 for cleanliness. */ +#if !defined(MA_DR_WAV_IMPLEMENTATION) /* dr_wav_c begin */ #ifndef ma_dr_wav_c #define ma_dr_wav_c @@ -78555,7 +79393,6 @@ MA_PRIVATE ma_bool32 ma_dr_wav_init__internal(ma_dr_wav* pWav, ma_dr_wav_chunk_p } if (pWav->container == ma_dr_wav_container_riff || pWav->container == ma_dr_wav_container_rifx) { if (ma_dr_wav_bytes_to_u32_ex(chunkSizeBytes, pWav->container) < 36) { - return MA_FALSE; } } else if (pWav->container == ma_dr_wav_container_rf64) { if (ma_dr_wav_bytes_to_u32_le(chunkSizeBytes) != 0xFFFFFFFF) { @@ -78824,7 +79661,9 @@ MA_PRIVATE ma_bool32 ma_dr_wav_init__internal(ma_dr_wav* pWav, ma_dr_wav_chunk_p compressionFormat = MA_DR_WAVE_FORMAT_MULAW; } else if (ma_dr_wav_fourcc_equal(type, "ima4")) { compressionFormat = MA_DR_WAVE_FORMAT_DVI_ADPCM; - sampleSizeInBits = 4; + sampleSizeInBits = 4; + (void)compressionFormat; + (void)sampleSizeInBits; return MA_FALSE; } else { return MA_FALSE; @@ -78882,9 +79721,7 @@ MA_PRIVATE ma_bool32 ma_dr_wav_init__internal(ma_dr_wav* pWav, ma_dr_wav_chunk_p } } if (isProcessingMetadata) { - ma_uint64 metadataBytesRead; - metadataBytesRead = ma_dr_wav__metadata_process_chunk(&metadataParser, &header, ma_dr_wav_metadata_type_all_including_unknown); - MA_DR_WAV_ASSERT(metadataBytesRead <= header.sizeInBytes); + ma_dr_wav__metadata_process_chunk(&metadataParser, &header, ma_dr_wav_metadata_type_all_including_unknown); if (ma_dr_wav__seek_from_start(pWav->onSeek, cursor, pWav->pUserData) == MA_FALSE) { break; } @@ -80332,6 +81169,12 @@ MA_API ma_uint64 ma_dr_wav_write_pcm_frames(ma_dr_wav* pWav, ma_uint64 framesToW MA_PRIVATE ma_uint64 ma_dr_wav_read_pcm_frames_s16__msadpcm(ma_dr_wav* pWav, ma_uint64 framesToRead, ma_int16* pBufferOut) { ma_uint64 totalFramesRead = 0; + static ma_int32 adaptationTable[] = { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + static ma_int32 coeff1Table[] = { 256, 512, 0, 192, 240, 460, 392 }; + static ma_int32 coeff2Table[] = { 0, -256, 0, 64, 0, -208, -232 }; MA_DR_WAV_ASSERT(pWav != NULL); MA_DR_WAV_ASSERT(framesToRead > 0); while (pWav->readCursorInPCMFrames < pWav->totalPCMFrameCount) { @@ -80350,6 +81193,9 @@ MA_PRIVATE ma_uint64 ma_dr_wav_read_pcm_frames_s16__msadpcm(ma_dr_wav* pWav, ma_ pWav->msadpcm.cachedFrames[2] = pWav->msadpcm.prevFrames[0][0]; pWav->msadpcm.cachedFrames[3] = pWav->msadpcm.prevFrames[0][1]; pWav->msadpcm.cachedFrameCount = 2; + if (pWav->msadpcm.predictor[0] >= ma_dr_wav_countof(coeff1Table)) { + return totalFramesRead; + } } else { ma_uint8 header[14]; if (pWav->onRead(pWav->pUserData, header, sizeof(header)) != sizeof(header)) { @@ -80369,6 +81215,9 @@ MA_PRIVATE ma_uint64 ma_dr_wav_read_pcm_frames_s16__msadpcm(ma_dr_wav* pWav, ma_ pWav->msadpcm.cachedFrames[2] = pWav->msadpcm.prevFrames[0][1]; pWav->msadpcm.cachedFrames[3] = pWav->msadpcm.prevFrames[1][1]; pWav->msadpcm.cachedFrameCount = 2; + if (pWav->msadpcm.predictor[0] >= ma_dr_wav_countof(coeff1Table) || pWav->msadpcm.predictor[1] >= ma_dr_wav_countof(coeff2Table)) { + return totalFramesRead; + } } } while (framesToRead > 0 && pWav->msadpcm.cachedFrameCount > 0 && pWav->readCursorInPCMFrames < pWav->totalPCMFrameCount) { @@ -80391,12 +81240,6 @@ MA_PRIVATE ma_uint64 ma_dr_wav_read_pcm_frames_s16__msadpcm(ma_dr_wav* pWav, ma_ if (pWav->msadpcm.bytesRemainingInBlock == 0) { continue; } else { - static ma_int32 adaptationTable[] = { - 230, 230, 230, 230, 307, 409, 512, 614, - 768, 614, 512, 409, 307, 230, 230, 230 - }; - static ma_int32 coeff1Table[] = { 256, 512, 0, 192, 240, 460, 392 }; - static ma_int32 coeff2Table[] = { 0, -256, 0, 64, 0, -208, -232 }; ma_uint8 nibbles; ma_int32 nibble0; ma_int32 nibble1; @@ -81647,7 +82490,7 @@ MA_API void ma_dr_wav_f32_to_s32(ma_int32* pOut, const float* pIn, size_t sample return; } for (i = 0; i < sampleCount; ++i) { - *pOut++ = (ma_int32)(2147483648.0 * pIn[i]); + *pOut++ = (ma_int32)(2147483648.0f * pIn[i]); } } MA_API void ma_dr_wav_f64_to_s32(ma_int32* pOut, const double* pIn, size_t sampleCount) @@ -82061,7 +82904,7 @@ MA_API ma_bool32 ma_dr_wav_fourcc_equal(const ma_uint8* a, const char* b) #endif /* MA_NO_WAV */ #if !defined(MA_NO_FLAC) && !defined(MA_NO_DECODING) -#if !defined(MA_DR_FLAC_IMPLEMENTATION) && !defined(MA_DR_FLAC_IMPLEMENTATION) /* For backwards compatibility. Will be removed in version 0.11 for cleanliness. */ +#if !defined(MA_DR_FLAC_IMPLEMENTATION) /* dr_flac_c begin */ #ifndef ma_dr_flac_c #define ma_dr_flac_c @@ -85093,6 +85936,7 @@ static ma_bool32 ma_dr_flac__read_subframe_header(ma_dr_flac_bs* bs, ma_dr_flac_ if ((header & 0x80) != 0) { return MA_FALSE; } + pSubframe->lpcOrder = 0; type = (header & 0x7E) >> 1; if (type == 0) { pSubframe->subframeType = MA_DR_FLAC_SUBFRAME_CONSTANT; @@ -85150,6 +85994,9 @@ static ma_bool32 ma_dr_flac__decode_subframe(ma_dr_flac_bs* bs, ma_dr_flac_frame } subframeBitsPerSample -= pSubframe->wastedBitsPerSample; pSubframe->pSamplesS32 = pDecodedSamplesOut; + if (frame->header.blockSizeInPCMFrames < pSubframe->lpcOrder) { + return MA_FALSE; + } switch (pSubframe->subframeType) { case MA_DR_FLAC_SUBFRAME_CONSTANT: @@ -89806,7 +90653,7 @@ MA_API ma_bool32 ma_dr_flac_next_cuesheet_track(ma_dr_flac_cuesheet_track_iterat #endif /* MA_NO_FLAC */ #if !defined(MA_NO_MP3) && !defined(MA_NO_DECODING) -#if !defined(MA_DR_MP3_IMPLEMENTATION) && !defined(MA_DR_MP3_IMPLEMENTATION) /* For backwards compatibility. Will be removed in version 0.11 for cleanliness. */ +#if !defined(MA_DR_MP3_IMPLEMENTATION) /* dr_mp3_c begin */ #ifndef ma_dr_mp3_c #define ma_dr_mp3_c @@ -89867,7 +90714,7 @@ MA_API const char* ma_dr_mp3_version_string(void) #define MA_DR_MP3_MIN(a, b) ((a) > (b) ? (b) : (a)) #define MA_DR_MP3_MAX(a, b) ((a) < (b) ? (b) : (a)) #if !defined(MA_DR_MP3_NO_SIMD) -#if !defined(MA_DR_MP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(_M_ARM64)) +#if !defined(MA_DR_MP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC)) #define MA_DR_MP3_ONLY_SIMD #endif #if ((defined(_MSC_VER) && _MSC_VER >= 1400) && defined(_M_X64)) || ((defined(__i386) || defined(_M_IX86) || defined(__i386__) || defined(__x86_64__)) && ((defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__))) @@ -89940,7 +90787,7 @@ end: return g_have_simd - 1; #endif } -#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) #include #define MA_DR_MP3_HAVE_SSE 0 #define MA_DR_MP3_HAVE_SIMD 1 @@ -89969,7 +90816,7 @@ static int ma_dr_mp3_have_simd(void) #else #define MA_DR_MP3_HAVE_SIMD 0 #endif -#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(__ARM_ARCH_6M__) +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(_M_ARM64EC) && !defined(__ARM_ARCH_6M__) #define MA_DR_MP3_HAVE_ARMV6 1 static __inline__ __attribute__((always_inline)) ma_int32 ma_dr_mp3_clip_int16_arm(ma_int32 a) { @@ -91135,8 +91982,8 @@ static ma_int16 ma_dr_mp3d_scale_pcm(float sample) s32 -= (s32 < 0); s = (ma_int16)ma_dr_mp3_clip_int16_arm(s32); #else - if (sample >= 32766.5) return (ma_int16) 32767; - if (sample <= -32767.5) return (ma_int16)-32768; + if (sample >= 32766.5f) return (ma_int16) 32767; + if (sample <= -32767.5f) return (ma_int16)-32768; s = (ma_int16)(sample + .5f); s -= (s < 0); #endif @@ -91522,9 +92369,9 @@ MA_API void ma_dr_mp3dec_f32_to_s16(const float *in, ma_int16 *out, size_t num_s for(; i < num_samples; i++) { float sample = in[i] * 32768.0f; - if (sample >= 32766.5) + if (sample >= 32766.5f) out[i] = (ma_int16) 32767; - else if (sample <= -32767.5) + else if (sample <= -32767.5f) out[i] = (ma_int16)-32768; else { @@ -92602,7 +93449,7 @@ For more information, please refer to =============================================================================== ALTERNATIVE 2 - MIT No Attribution =============================================================================== -Copyright 2023 David Reid +Copyright 2025 David Reid Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/vendor/miniaudio/synchronization.odin b/vendor/miniaudio/synchronization.odin index 012f52c2c..08182c32b 100644 --- a/vendor/miniaudio/synchronization.odin +++ b/vendor/miniaudio/synchronization.odin @@ -62,6 +62,11 @@ when !NO_THREADING { Signals the specified auto-reset event. */ event_signal :: proc(pEvent: ^event) -> result --- + + semaphore_init :: proc(initialValue: i32, pSemaphore: ^semaphore) -> result --- + semaphore_uninit :: proc(pSemaphore: ^semaphore) --- + semaphore_wait :: proc(pSemaphore: ^semaphore) -> result --- + semaphore_release :: proc(pSemaphore: ^semaphore) -> result --- } /* NO_THREADING */ } diff --git a/vendor/miniaudio/utilities.odin b/vendor/miniaudio/utilities.odin index 9285874b6..d59bb75a4 100644 --- a/vendor/miniaudio/utilities.odin +++ b/vendor/miniaudio/utilities.odin @@ -7,7 +7,7 @@ foreign import lib { LIB } @(default_calling_convention="c", link_prefix="ma_") foreign lib { /* - Calculates a buffer size in milliseconds from the specified number of frames and sample rate. + Calculates a buffer size in milliseconds (rounded up) from the specified number of frames and sample rate. */ calculate_buffer_size_in_milliseconds_from_frames :: proc(bufferSizeInFrames: u32, sampleRate: u32) -> u32 --- @@ -163,6 +163,8 @@ foreign lib { data_source_read_pcm_frames :: proc(pDataSource: ^data_source, pFramesOut: rawptr, frameCount: u64, pFramesRead: ^u64) -> result --- /* Must support pFramesOut = NULL in which case a forward seek should be performed. */ data_source_seek_pcm_frames :: proc(pDataSource: ^data_source, frameCount: u64, pFramesSeeked: ^u64) -> result --- /* Can only seek forward. Equivalent to ma_data_source_read_pcm_frames(pDataSource, NULL, frameCount); */ data_source_seek_to_pcm_frame :: proc(pDataSource: ^data_source, frameIndex: u64) -> result --- + data_source_seek_seconds :: proc(pDataSource: ^data_source, secondCount: f32, pSecondsSeeked: ^f32) -> result --- /* Can only seek forward. Abstraction to ma_data_source_seek_pcm_frames() */ + data_source_seek_to_seconds :: proc(pDataSource: ^data_source, seekPointInSeconds: f32) -> result --- /* Abstraction to ma_data_source_seek_to_pcm_frame() */ data_source_get_data_format :: proc(pDataSource: ^data_source, pFormat: ^format, pChannels: ^u32, pSampleRate: ^u32, pChannelMap: [^]channel, channelMapCap: c.size_t) -> result --- data_source_get_cursor_in_pcm_frames :: proc(pDataSource: ^data_source, pCursor: ^u64) -> result --- data_source_get_length_in_pcm_frames :: proc(pDataSource: ^data_source, pLength: ^u64) -> result --- /* Returns MA_NOT_IMPLEMENTED if the length is unknown or cannot be determined. Decoders can return this. */ diff --git a/vendor/raylib/raymath.odin b/vendor/raylib/raymath.odin index c8420d60a..9874d5086 100644 --- a/vendor/raylib/raymath.odin +++ b/vendor/raylib/raymath.odin @@ -831,8 +831,8 @@ fmaxf :: proc "contextless" (x, y: f32) -> f32 { return x } - if math.signbit(x) != math.signbit(y) { - return y if math.signbit(x) else x + if math.sign_bit(x) != math.sign_bit(y) { + return y if math.sign_bit(x) else x } return y if x < y else x diff --git a/vendor/raylib/rlgl/rlgl.odin b/vendor/raylib/rlgl/rlgl.odin index 6ac19695d..b6cc23c48 100644 --- a/vendor/raylib/rlgl/rlgl.odin +++ b/vendor/raylib/rlgl/rlgl.odin @@ -375,17 +375,20 @@ foreign lib { //------------------------------------------------------------------------------------ // Functions Declaration - Matrix operations //------------------------------------------------------------------------------------ - MatrixMode :: proc(mode: c.int) --- // Choose the current matrix to be transformed - PushMatrix :: proc() --- // Push the current matrix to stack - PopMatrix :: proc() --- // Pop lattest inserted matrix from stack - LoadIdentity :: proc() --- // Reset current matrix to identity matrix - Translatef :: proc(x, y, z: f32) --- // Multiply the current matrix by a translation matrix - Rotatef :: proc(angleDeg: f32, x, y, z: f32) --- // Multiply the current matrix by a rotation matrix - Scalef :: proc(x, y, z: f32) --- // Multiply the current matrix by a scaling matrix - MultMatrixf :: proc(matf: [^]f32) --- // Multiply the current matrix by another matrix - Frustum :: proc(left, right, bottom, top, znear, zfar: f64) --- - Ortho :: proc(left, right, bottom, top, znear, zfar: f64) --- - Viewport :: proc(x, y, width, height: c.int) --- // Set the viewport area + MatrixMode :: proc(mode: c.int) --- // Choose the current matrix to be transformed + PushMatrix :: proc() --- // Push the current matrix to stack + PopMatrix :: proc() --- // Pop lattest inserted matrix from stack + LoadIdentity :: proc() --- // Reset current matrix to identity matrix + Translatef :: proc(x, y, z: f32) --- // Multiply the current matrix by a translation matrix + Rotatef :: proc(angleDeg: f32, x, y, z: f32) --- // Multiply the current matrix by a rotation matrix + Scalef :: proc(x, y, z: f32) --- // Multiply the current matrix by a scaling matrix + MultMatrixf :: proc(matf: [^]f32) --- // Multiply the current matrix by another matrix + Frustum :: proc(left, right, bottom, top, znear, zfar: f64) --- + Ortho :: proc(left, right, bottom, top, znear, zfar: f64) --- + Viewport :: proc(x, y, width, height: c.int) --- // Set the viewport area + SetClipPlanes :: proc(near, far: f64) --- // Set clip planes distances + GetCullDistanceNear :: proc() -> f64 --- // Get cull plane distance near + GetCullDistanceFar :: proc() -> f64 --- // Get cull plane distance far //------------------------------------------------------------------------------------ // Functions Declaration - Vertex level operations diff --git a/vendor/sdl3/sdl3_gpu.odin b/vendor/sdl3/sdl3_gpu.odin index ec414f98e..f0017a525 100644 --- a/vendor/sdl3/sdl3_gpu.odin +++ b/vendor/sdl3/sdl3_gpu.odin @@ -46,7 +46,7 @@ GPUIndexElementSize :: enum c.int { GPUTextureFormat :: enum c.int { INVALID, - /* Unsigned Normalized Float Color Formats */ + /* Unsigned Normalized Float Color Formats */ A8_UNORM, R8_UNORM, R8G8_UNORM, @@ -59,34 +59,41 @@ GPUTextureFormat :: enum c.int { B5G5R5A1_UNORM, B4G4R4A4_UNORM, B8G8R8A8_UNORM, - /* Compressed Unsigned Normalized Float Color Formats */ + + /* Compressed Unsigned Normalized Float Color Formats */ BC1_RGBA_UNORM, BC2_RGBA_UNORM, BC3_RGBA_UNORM, BC4_R_UNORM, BC5_RG_UNORM, BC7_RGBA_UNORM, - /* Compressed Signed Float Color Formats */ + + /* Compressed Signed Float Color Formats */ BC6H_RGB_FLOAT, - /* Compressed Unsigned Float Color Formats */ + + /* Compressed Unsigned Float Color Formats */ BC6H_RGB_UFLOAT, - /* Signed Normalized Float Color Formats */ + + /* Signed Normalized Float Color Formats */ R8_SNORM, R8G8_SNORM, R8G8B8A8_SNORM, R16_SNORM, R16G16_SNORM, R16G16B16A16_SNORM, - /* Signed Float Color Formats */ + + /* Signed Float Color Formats */ R16_FLOAT, R16G16_FLOAT, R16G16B16A16_FLOAT, R32_FLOAT, R32G32_FLOAT, R32G32B32A32_FLOAT, - /* Unsigned Float Color Formats */ + + /* Unsigned Float Color Formats */ R11G11B10_UFLOAT, - /* Unsigned Integer Color Formats */ + + /* Unsigned Integer Color Formats */ R8_UINT, R8G8_UINT, R8G8B8A8_UINT, @@ -96,7 +103,8 @@ GPUTextureFormat :: enum c.int { R32_UINT, R32G32_UINT, R32G32B32A32_UINT, - /* Signed Integer Color Formats */ + + /* Signed Integer Color Formats */ R8_INT, R8G8_INT, R8G8B8A8_INT, @@ -106,21 +114,25 @@ GPUTextureFormat :: enum c.int { R32_INT, R32G32_INT, R32G32B32A32_INT, - /* SRGB Unsigned Normalized Color Formats */ + + /* SRGB Unsigned Normalized Color Formats */ R8G8B8A8_UNORM_SRGB, B8G8R8A8_UNORM_SRGB, - /* Compressed SRGB Unsigned Normalized Color Formats */ + + /* Compressed SRGB Unsigned Normalized Color Formats */ BC1_RGBA_UNORM_SRGB, BC2_RGBA_UNORM_SRGB, BC3_RGBA_UNORM_SRGB, BC7_RGBA_UNORM_SRGB, - /* Depth Formats */ + + /* Depth Formats */ D16_UNORM, D24_UNORM, D32_FLOAT, D24_UNORM_S8_UINT, D32_FLOAT_S8_UINT, - /* Compressed ASTC Normalized Float Color Formats*/ + + /* Compressed ASTC Normalized Float Color Formats*/ ASTC_4x4_UNORM, ASTC_5x4_UNORM, ASTC_5x5_UNORM, @@ -135,7 +147,8 @@ GPUTextureFormat :: enum c.int { ASTC_10x10_UNORM, ASTC_12x10_UNORM, ASTC_12x12_UNORM, - /* Compressed SRGB ASTC Normalized Float Color Formats*/ + + /* Compressed SRGB ASTC Normalized Float Color Formats*/ ASTC_4x4_UNORM_SRGB, ASTC_5x4_UNORM_SRGB, ASTC_5x5_UNORM_SRGB, @@ -150,7 +163,8 @@ GPUTextureFormat :: enum c.int { ASTC_10x10_UNORM_SRGB, ASTC_12x10_UNORM_SRGB, ASTC_12x12_UNORM_SRGB, - /* Compressed ASTC Signed Float Color Formats*/ + + /* Compressed ASTC Signed Float Color Formats*/ ASTC_4x4_FLOAT, ASTC_5x4_FLOAT, ASTC_5x5_FLOAT, diff --git a/vendor/sdl3/sdl3_mutex.odin b/vendor/sdl3/sdl3_mutex.odin index ada8006bc..8067473f3 100644 --- a/vendor/sdl3/sdl3_mutex.odin +++ b/vendor/sdl3/sdl3_mutex.odin @@ -1,8 +1,8 @@ package sdl3 -Mutex :: struct {} -RWLock :: struct {} - +Mutex :: struct {} +RWLock :: struct {} +Semaphore :: struct {} @(default_calling_convention="c", link_prefix="SDL_", require_results) foreign lib { @@ -19,4 +19,12 @@ foreign lib { TryLockRWLockForWriting :: proc(rwlock: ^RWLock) -> bool --- UnlockRWLock :: proc(rwlock: ^RWLock) --- DestroyRWLock :: proc(rwlock: ^RWLock) --- -} \ No newline at end of file + + CreateSemaphore :: proc(initial_value: Uint32) -> ^Semaphore --- + DestroySemaphore :: proc(sem: ^Semaphore) --- + GetSemaphoreValue :: proc(sem: ^Semaphore) -> Uint32 --- + SignalSemaphore :: proc(sem: ^Semaphore) --- + TryWaitSemaphore :: proc(sem: ^Semaphore) -> bool --- + WaitSemaphore :: proc(sem: ^Semaphore) --- + WaitSemaphoreTimeout :: proc(sem: ^Semaphore, timeout_ms: Sint32) --- +} diff --git a/vendor/sdl3/sdl3_thread.odin b/vendor/sdl3/sdl3_thread.odin index 9b2d068d9..e6bbf7d16 100644 --- a/vendor/sdl3/sdl3_thread.odin +++ b/vendor/sdl3/sdl3_thread.odin @@ -49,7 +49,7 @@ PROP_THREAD_CREATE_STACKSIZE_NUMBER :: "SDL.thread.create.stacksize" BeginThreadFunction :: proc "c" () -> FunctionPointer { when ODIN_OS == .Windows { foreign { - _beginthreadx :: proc "c" ( + _beginthreadex :: proc "c" ( security: rawptr, stack_size: c.uint, start_address: proc "c" (rawptr), @@ -58,7 +58,7 @@ BeginThreadFunction :: proc "c" () -> FunctionPointer { thraddr: ^c.uint, ) -> uintptr --- } - return FunctionPointer(_beginthreadx) + return FunctionPointer(_beginthreadex) } else { return nil } diff --git a/vendor/sdl3/sdl3_video.odin b/vendor/sdl3/sdl3_video.odin index 3be912d15..3dd045214 100644 --- a/vendor/sdl3/sdl3_video.odin +++ b/vendor/sdl3/sdl3_video.odin @@ -336,7 +336,7 @@ foreign lib { GetNaturalDisplayOrientation :: proc(displayID: DisplayID) -> DisplayOrientation --- GetCurrentDisplayOrientation :: proc(displayID: DisplayID) -> DisplayOrientation --- GetDisplayContentScale :: proc(displayID: DisplayID) -> f32 --- - GetFullscreenDisplayModes :: proc(displayID: DisplayID, count: c.int) -> [^]^DisplayMode --- + GetFullscreenDisplayModes :: proc(displayID: DisplayID, count: ^c.int) -> [^]^DisplayMode --- GetClosestFullscreenDisplayMode :: proc(displayID: DisplayID, w, h: c.int, refresh_rate: f32, include_high_density_modes: bool, closest: ^DisplayMode) -> bool --- GetDesktopDisplayMode :: proc(displayID: DisplayID) -> ^DisplayMode --- GetCurrentDisplayMode :: proc(displayID: DisplayID) -> ^DisplayMode --- @@ -397,7 +397,7 @@ foreign lib { GetWindowKeyboardGrab :: proc(window: ^Window) -> bool --- GetWindowMouseGrab :: proc(window: ^Window) -> bool --- GetGrabbedWindow :: proc() -> ^Window --- - SetWindowMouseRect :: proc(window: ^Window, #by_ptr rect: Rect) -> bool --- + SetWindowMouseRect :: proc(window: ^Window, rect: ^Rect) -> bool --- GetWindowMouseRect :: proc(window: ^Window) -> ^Rect --- SetWindowOpacity :: proc(window: ^Window, opacity: f32) -> bool --- GetWindowOpacity :: proc(window: ^Window) -> f32 --- diff --git a/vendor/sdl3/ttf/LICENSE.freetype.txt b/vendor/sdl3/ttf/LICENSE.freetype.txt new file mode 100644 index 000000000..c406d150f --- /dev/null +++ b/vendor/sdl3/ttf/LICENSE.freetype.txt @@ -0,0 +1,169 @@ + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://www.freetype.org + + +--- end of FTL.TXT --- diff --git a/vendor/sdl3/ttf/LICENSE.harfbuzz.txt b/vendor/sdl3/ttf/LICENSE.harfbuzz.txt new file mode 100644 index 000000000..1dd917e9f --- /dev/null +++ b/vendor/sdl3/ttf/LICENSE.harfbuzz.txt @@ -0,0 +1,42 @@ +HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. +For parts of HarfBuzz that are licensed under different licenses see individual +files names COPYING in subdirectories where applicable. + +Copyright © 2010-2022 Google, Inc. +Copyright © 2015-2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. +Copyright © 2012,2015 Mozilla Foundation +Copyright © 2011 Codethink Limited +Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) +Copyright © 2009 Keith Stribley +Copyright © 2011 Martin Hosken and SIL International +Copyright © 2007 Chris Wilson +Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod +Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. +Copyright © 1998-2005 David Turner and Werner Lemberg +Copyright © 2016 Igalia S.L. +Copyright © 2022 Matthias Clasen +Copyright © 2018,2021 Khaled Hosny +Copyright © 2018,2019,2020 Adobe, Inc +Copyright © 2013-2015 Alexei Podtelezhnikov + +For full copyright notices consult the individual files in the package. + + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/vendor/sdl3/ttf/LICENSE.plutosvg.txt b/vendor/sdl3/ttf/LICENSE.plutosvg.txt new file mode 100644 index 000000000..62a964e73 --- /dev/null +++ b/vendor/sdl3/ttf/LICENSE.plutosvg.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2025 Samuel Ugochukwu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/sdl3/ttf/LICENSE.plutovg.txt b/vendor/sdl3/ttf/LICENSE.plutovg.txt new file mode 100644 index 000000000..62a964e73 --- /dev/null +++ b/vendor/sdl3/ttf/LICENSE.plutovg.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2025 Samuel Ugochukwu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/sdl3/ttf/LICENSE.txt b/vendor/sdl3/ttf/LICENSE.txt new file mode 100644 index 000000000..52d0ed38b --- /dev/null +++ b/vendor/sdl3/ttf/LICENSE.txt @@ -0,0 +1,17 @@ +Copyright (C) 1997-2025 Sam Lantinga + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/vendor/sdl3/ttf/SDL3_ttf.dll b/vendor/sdl3/ttf/SDL3_ttf.dll new file mode 100644 index 000000000..b1616f966 Binary files /dev/null and b/vendor/sdl3/ttf/SDL3_ttf.dll differ diff --git a/vendor/sdl3/ttf/SDL3_ttf.lib b/vendor/sdl3/ttf/SDL3_ttf.lib new file mode 100644 index 000000000..5716c5661 Binary files /dev/null and b/vendor/sdl3/ttf/SDL3_ttf.lib differ diff --git a/vendor/sdl3/ttf/include/SDL_textengine.h b/vendor/sdl3/ttf/include/SDL_textengine.h new file mode 100644 index 000000000..9f5f1f0c3 --- /dev/null +++ b/vendor/sdl3/ttf/include/SDL_textengine.h @@ -0,0 +1,181 @@ +/* + SDL_ttf: A companion library to SDL for working with TrueType (tm) fonts + Copyright (C) 2001-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + + +/** + * \file SDL_textengine.h + * + * Definitions for implementations of the TTF_TextEngine interface. + */ +#ifndef SDL_TTF_TEXTENGINE_H_ +#define SDL_TTF_TEXTENGINE_H_ + +#include +#include + +#include + +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A font atlas draw command. + * + * \since This enum is available since SDL_ttf 3.0.0. + */ +typedef enum TTF_DrawCommand +{ + TTF_DRAW_COMMAND_NOOP, + TTF_DRAW_COMMAND_FILL, + TTF_DRAW_COMMAND_COPY +} TTF_DrawCommand; + +/** + * A filled rectangle draw operation. + * + * \since This struct is available since SDL_ttf 3.0.0. + * + * \sa TTF_DrawOperation + */ +typedef struct TTF_FillOperation +{ + TTF_DrawCommand cmd; /**< TTF_DRAW_COMMAND_FILL */ + SDL_Rect rect; /**< The rectangle to fill, in pixels. The x coordinate is relative to the left side of the text area, going right, and the y coordinate is relative to the top side of the text area, going down. */ +} TTF_FillOperation; + +/** + * A texture copy draw operation. + * + * \since This struct is available since SDL_ttf 3.0.0. + * + * \sa TTF_DrawOperation + */ +typedef struct TTF_CopyOperation +{ + TTF_DrawCommand cmd; /**< TTF_DRAW_COMMAND_COPY */ + int text_offset; /**< The offset in the text corresponding to this glyph. + There may be multiple glyphs with the same text offset + and the next text offset might be several Unicode codepoints + later. In this case the glyphs and codepoints are grouped + together and the group bounding box is the union of the dst + rectangles for the corresponding glyphs. */ + TTF_Font *glyph_font; /**< The font containing the glyph to be drawn, can be passed to TTF_GetGlyphImageForIndex() */ + Uint32 glyph_index; /**< The glyph index of the glyph to be drawn, can be passed to TTF_GetGlyphImageForIndex() */ + SDL_Rect src; /**< The area within the glyph to be drawn */ + SDL_Rect dst; /**< The drawing coordinates of the glyph, in pixels. The x coordinate is relative to the left side of the text area, going right, and the y coordinate is relative to the top side of the text area, going down. */ + void *reserved; +} TTF_CopyOperation; + +/** + * A text engine draw operation. + * + * \since This struct is available since SDL_ttf 3.0.0. + */ +typedef union TTF_DrawOperation +{ + TTF_DrawCommand cmd; + TTF_FillOperation fill; + TTF_CopyOperation copy; +} TTF_DrawOperation; + + +/* Private data in TTF_Text, to assist in text measurement and layout */ +typedef struct TTF_TextLayout TTF_TextLayout; + + +/* Private data in TTF_Text, available to implementations */ +struct TTF_TextData +{ + TTF_Font *font; /**< The font used by this text, read-only. */ + SDL_FColor color; /**< The color of the text, read-only. */ + + bool needs_layout_update; /**< True if the layout needs to be updated */ + TTF_TextLayout *layout; /**< Cached layout information, read-only. */ + int x; /**< The x offset of the upper left corner of this text, in pixels, read-only. */ + int y; /**< The y offset of the upper left corner of this text, in pixels, read-only. */ + int w; /**< The width of this text, in pixels, read-only. */ + int h; /**< The height of this text, in pixels, read-only. */ + int num_ops; /**< The number of drawing operations to render this text, read-only. */ + TTF_DrawOperation *ops; /**< The drawing operations used to render this text, read-only. */ + int num_clusters; /**< The number of substrings representing clusters of glyphs in the string, read-only */ + TTF_SubString *clusters; /**< Substrings representing clusters of glyphs in the string, read-only */ + + SDL_PropertiesID props; /**< Custom properties associated with this text, read-only. This field is created as-needed using TTF_GetTextProperties() and the properties may be then set and read normally */ + + bool needs_engine_update; /**< True if the engine text needs to be updated */ + TTF_TextEngine *engine; /**< The engine used to render this text, read-only. */ + void *engine_text; /**< The implementation-specific representation of this text */ +}; + +/** + * A text engine interface. + * + * This structure should be initialized using SDL_INIT_INTERFACE() + * + * \since This struct is available since SDL_ttf 3.0.0. + * + * \sa SDL_INIT_INTERFACE + */ +struct TTF_TextEngine +{ + Uint32 version; /**< The version of this interface */ + + void *userdata; /**< User data pointer passed to callbacks */ + + /* Create a text representation from draw instructions. + * + * All fields of `text` except `internal->engine_text` will already be filled out. + * + * This function should set the `internal->engine_text` field to a non-NULL value. + * + * \param userdata the userdata pointer in this interface. + * \param text the text object being created. + */ + bool (SDLCALL *CreateText)(void *userdata, TTF_Text *text); + + /** + * Destroy a text representation. + */ + void (SDLCALL *DestroyText)(void *userdata, TTF_Text *text); + +}; + +/* Check the size of TTF_TextEngine + * + * If this assert fails, either the compiler is padding to an unexpected size, + * or the interface has been updated and this should be updated to match and + * the code using this interface should be updated to handle the old version. + */ +SDL_COMPILE_TIME_ASSERT(TTF_TextEngine_SIZE, + (sizeof(void *) == 4 && sizeof(TTF_TextEngine) == 16) || + (sizeof(void *) == 8 && sizeof(TTF_TextEngine) == 32)); + + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_TTF_TEXTENGINE_H_ */ + diff --git a/vendor/sdl3/ttf/include/SDL_ttf.h b/vendor/sdl3/ttf/include/SDL_ttf.h new file mode 100644 index 000000000..b723bc74e --- /dev/null +++ b/vendor/sdl3/ttf/include/SDL_ttf.h @@ -0,0 +1,2833 @@ +/* + SDL_ttf: A companion library to SDL for working with TrueType (tm) fonts + Copyright (C) 2001-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/* WIKI CATEGORY: SDLTTF */ + +/** + * # CategorySDLTTF + * + * Header file for SDL_ttf library + * + * This library is a wrapper around the excellent FreeType 2.0 library, + * available at: https://www.freetype.org/ + */ + +#ifndef SDL_TTF_H_ +#define SDL_TTF_H_ + +#include +#include + +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Printable format: "%d.%d.%d", MAJOR, MINOR, MICRO + */ +#define SDL_TTF_MAJOR_VERSION 3 +#define SDL_TTF_MINOR_VERSION 2 +#define SDL_TTF_MICRO_VERSION 2 + +/** + * This is the version number macro for the current SDL_ttf version. + */ +#define SDL_TTF_VERSION \ + SDL_VERSIONNUM(SDL_TTF_MAJOR_VERSION, SDL_TTF_MINOR_VERSION, SDL_TTF_MICRO_VERSION) + +/** + * This macro will evaluate to true if compiled with SDL_ttf at least X.Y.Z. + */ +#define SDL_TTF_VERSION_ATLEAST(X, Y, Z) \ + ((SDL_TTF_MAJOR_VERSION >= X) && \ + (SDL_TTF_MAJOR_VERSION > X || SDL_TTF_MINOR_VERSION >= Y) && \ + (SDL_TTF_MAJOR_VERSION > X || SDL_TTF_MINOR_VERSION > Y || SDL_TTF_MICRO_VERSION >= Z)) + +/** + * This function gets the version of the dynamically linked SDL_ttf library. + * + * \returns SDL_ttf version. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC int SDLCALL TTF_Version(void); + +/** + * Query the version of the FreeType library in use. + * + * TTF_Init() should be called before calling this function. + * + * \param major to be filled in with the major version number. Can be NULL. + * \param minor to be filled in with the minor version number. Can be NULL. + * \param patch to be filled in with the param version number. Can be NULL. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_Init + */ +extern SDL_DECLSPEC void SDLCALL TTF_GetFreeTypeVersion(int *major, int *minor, int *patch); + +/** + * Query the version of the HarfBuzz library in use. + * + * If HarfBuzz is not available, the version reported is 0.0.0. + * + * \param major to be filled in with the major version number. Can be NULL. + * \param minor to be filled in with the minor version number. Can be NULL. + * \param patch to be filled in with the param version number. Can be NULL. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC void SDLCALL TTF_GetHarfBuzzVersion(int *major, int *minor, int *patch); + +/** + * The internal structure containing font information. + * + * Opaque data! + */ +typedef struct TTF_Font TTF_Font; + +/** + * Initialize SDL_ttf. + * + * You must successfully call this function before it is safe to call any + * other function in this library. + * + * It is safe to call this more than once, and each successful TTF_Init() call + * should be paired with a matching TTF_Quit() call. + * + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_Quit + */ +extern SDL_DECLSPEC bool SDLCALL TTF_Init(void); + +/** + * Create a font from a file, using a specified point size. + * + * Some .fon fonts will have several sizes embedded in the file, so the point + * size becomes the index of choosing which size. If the value is too high, + * the last indexed size will be the default. + * + * When done with the returned TTF_Font, use TTF_CloseFont() to dispose of it. + * + * \param file path to font file. + * \param ptsize point size to use for the newly-opened font. + * \returns a valid TTF_Font, or NULL on failure; call SDL_GetError() for more + * information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CloseFont + */ +extern SDL_DECLSPEC TTF_Font * SDLCALL TTF_OpenFont(const char *file, float ptsize); + +/** + * Create a font from an SDL_IOStream, using a specified point size. + * + * Some .fon fonts will have several sizes embedded in the file, so the point + * size becomes the index of choosing which size. If the value is too high, + * the last indexed size will be the default. + * + * If `closeio` is true, `src` will be automatically closed once the font is + * closed. Otherwise you should close `src` yourself after closing the font. + * + * When done with the returned TTF_Font, use TTF_CloseFont() to dispose of it. + * + * \param src an SDL_IOStream to provide a font file's data. + * \param closeio true to close `src` when the font is closed, false to leave + * it open. + * \param ptsize point size to use for the newly-opened font. + * \returns a valid TTF_Font, or NULL on failure; call SDL_GetError() for more + * information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CloseFont + */ +extern SDL_DECLSPEC TTF_Font * SDLCALL TTF_OpenFontIO(SDL_IOStream *src, bool closeio, float ptsize); + +/** + * Create a font with the specified properties. + * + * These are the supported properties: + * + * - `TTF_PROP_FONT_CREATE_FILENAME_STRING`: the font file to open, if an + * SDL_IOStream isn't being used. This is required if + * `TTF_PROP_FONT_CREATE_IOSTREAM_POINTER` and + * `TTF_PROP_FONT_CREATE_EXISTING_FONT` aren't set. + * - `TTF_PROP_FONT_CREATE_IOSTREAM_POINTER`: an SDL_IOStream containing the + * font to be opened. This should not be closed until the font is closed. + * This is required if `TTF_PROP_FONT_CREATE_FILENAME_STRING` and + * `TTF_PROP_FONT_CREATE_EXISTING_FONT` aren't set. + * - `TTF_PROP_FONT_CREATE_IOSTREAM_OFFSET_NUMBER`: the offset in the iostream + * for the beginning of the font, defaults to 0. + * - `TTF_PROP_FONT_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN`: true if closing the + * font should also close the associated SDL_IOStream. + * - `TTF_PROP_FONT_CREATE_SIZE_FLOAT`: the point size of the font. Some .fon + * fonts will have several sizes embedded in the file, so the point size + * becomes the index of choosing which size. If the value is too high, the + * last indexed size will be the default. + * - `TTF_PROP_FONT_CREATE_FACE_NUMBER`: the face index of the font, if the + * font contains multiple font faces. + * - `TTF_PROP_FONT_CREATE_HORIZONTAL_DPI_NUMBER`: the horizontal DPI to use + * for font rendering, defaults to + * `TTF_PROP_FONT_CREATE_VERTICAL_DPI_NUMBER` if set, or 72 otherwise. + * - `TTF_PROP_FONT_CREATE_VERTICAL_DPI_NUMBER`: the vertical DPI to use for + * font rendering, defaults to `TTF_PROP_FONT_CREATE_HORIZONTAL_DPI_NUMBER` + * if set, or 72 otherwise. + * - `TTF_PROP_FONT_CREATE_EXISTING_FONT`: an optional TTF_Font that, if set, + * will be used as the font data source and the initial size and style of + * the new font. + * + * \param props the properties to use. + * \returns a valid TTF_Font, or NULL on failure; call SDL_GetError() for more + * information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CloseFont + */ +extern SDL_DECLSPEC TTF_Font * SDLCALL TTF_OpenFontWithProperties(SDL_PropertiesID props); + +#define TTF_PROP_FONT_CREATE_FILENAME_STRING "SDL_ttf.font.create.filename" +#define TTF_PROP_FONT_CREATE_IOSTREAM_POINTER "SDL_ttf.font.create.iostream" +#define TTF_PROP_FONT_CREATE_IOSTREAM_OFFSET_NUMBER "SDL_ttf.font.create.iostream.offset" +#define TTF_PROP_FONT_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN "SDL_ttf.font.create.iostream.autoclose" +#define TTF_PROP_FONT_CREATE_SIZE_FLOAT "SDL_ttf.font.create.size" +#define TTF_PROP_FONT_CREATE_FACE_NUMBER "SDL_ttf.font.create.face" +#define TTF_PROP_FONT_CREATE_HORIZONTAL_DPI_NUMBER "SDL_ttf.font.create.hdpi" +#define TTF_PROP_FONT_CREATE_VERTICAL_DPI_NUMBER "SDL_ttf.font.create.vdpi" +#define TTF_PROP_FONT_CREATE_EXISTING_FONT "SDL_ttf.font.create.existing_font" + +/** + * Create a copy of an existing font. + * + * The copy will be distinct from the original, but will share the font file + * and have the same size and style as the original. + * + * When done with the returned TTF_Font, use TTF_CloseFont() to dispose of it. + * + * \param existing_font the font to copy. + * \returns a valid TTF_Font, or NULL on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * original font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CloseFont + */ +extern SDL_DECLSPEC TTF_Font * SDLCALL TTF_CopyFont(TTF_Font *existing_font); + +/** + * Get the properties associated with a font. + * + * The following read-write properties are provided by SDL: + * + * - `TTF_PROP_FONT_OUTLINE_LINE_CAP_NUMBER`: The FT_Stroker_LineCap value + * used when setting the font outline, defaults to + * `FT_STROKER_LINECAP_ROUND`. + * - `TTF_PROP_FONT_OUTLINE_LINE_JOIN_NUMBER`: The FT_Stroker_LineJoin value + * used when setting the font outline, defaults to + * `FT_STROKER_LINEJOIN_ROUND`. + * - `TTF_PROP_FONT_OUTLINE_MITER_LIMIT_NUMBER`: The FT_Fixed miter limit used + * when setting the font outline, defaults to 0. + * + * \param font the font to query. + * \returns a valid property ID on success or 0 on failure; call + * SDL_GetError() for more information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC SDL_PropertiesID SDLCALL TTF_GetFontProperties(TTF_Font *font); + +#define TTF_PROP_FONT_OUTLINE_LINE_CAP_NUMBER "SDL_ttf.font.outline.line_cap" +#define TTF_PROP_FONT_OUTLINE_LINE_JOIN_NUMBER "SDL_ttf.font.outline.line_join" +#define TTF_PROP_FONT_OUTLINE_MITER_LIMIT_NUMBER "SDL_ttf.font.outline.miter_limit" + +/** + * Get the font generation. + * + * The generation is incremented each time font properties change that require + * rebuilding glyphs, such as style, size, etc. + * + * \param font the font to query. + * \returns the font generation or 0 on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC Uint32 SDLCALL TTF_GetFontGeneration(TTF_Font *font); + +/** + * Add a fallback font. + * + * Add a font that will be used for glyphs that are not in the current font. + * The fallback font should have the same size and style as the current font. + * + * If there are multiple fallback fonts, they are used in the order added. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to modify. + * \param fallback the font to add as a fallback. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created + * both fonts. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_ClearFallbackFonts + * \sa TTF_RemoveFallbackFont + */ +extern SDL_DECLSPEC bool SDLCALL TTF_AddFallbackFont(TTF_Font *font, TTF_Font *fallback); + +/** + * Remove a fallback font. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to modify. + * \param fallback the font to remove as a fallback. + * + * \threadsafety This function should be called on the thread that created + * both fonts. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_AddFallbackFont + * \sa TTF_ClearFallbackFonts + */ +extern SDL_DECLSPEC void SDLCALL TTF_RemoveFallbackFont(TTF_Font *font, TTF_Font *fallback); + +/** + * Remove all fallback fonts. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to modify. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_AddFallbackFont + * \sa TTF_RemoveFallbackFont + */ +extern SDL_DECLSPEC void SDLCALL TTF_ClearFallbackFonts(TTF_Font *font); + +/** + * Set a font's size dynamically. + * + * This updates any TTF_Text objects using this font, and clears + * already-generated glyphs, if any, from the cache. + * + * \param font the font to resize. + * \param ptsize the new point size. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontSize + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontSize(TTF_Font *font, float ptsize); + +/** + * Set font size dynamically with target resolutions, in dots per inch. + * + * This updates any TTF_Text objects using this font, and clears + * already-generated glyphs, if any, from the cache. + * + * \param font the font to resize. + * \param ptsize the new point size. + * \param hdpi the target horizontal DPI. + * \param vdpi the target vertical DPI. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontSize + * \sa TTF_GetFontSizeDPI + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontSizeDPI(TTF_Font *font, float ptsize, int hdpi, int vdpi); + +/** + * Get the size of a font. + * + * \param font the font to query. + * \returns the size of the font, or 0.0f on failure; call SDL_GetError() for + * more information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontSize + * \sa TTF_SetFontSizeDPI + */ +extern SDL_DECLSPEC float SDLCALL TTF_GetFontSize(TTF_Font *font); + +/** + * Get font target resolutions, in dots per inch. + * + * \param font the font to query. + * \param hdpi a pointer filled in with the target horizontal DPI. + * \param vdpi a pointer filled in with the target vertical DPI. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontSizeDPI + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetFontDPI(TTF_Font *font, int *hdpi, int *vdpi); + +/** + * Font style flags for TTF_Font + * + * These are the flags which can be used to set the style of a font in + * SDL_ttf. A combination of these flags can be used with functions that set + * or query font style, such as TTF_SetFontStyle or TTF_GetFontStyle. + * + * \since This datatype is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontStyle + * \sa TTF_GetFontStyle + */ +typedef Uint32 TTF_FontStyleFlags; + +#define TTF_STYLE_NORMAL 0x00 /**< No special style */ +#define TTF_STYLE_BOLD 0x01 /**< Bold style */ +#define TTF_STYLE_ITALIC 0x02 /**< Italic style */ +#define TTF_STYLE_UNDERLINE 0x04 /**< Underlined text */ +#define TTF_STYLE_STRIKETHROUGH 0x08 /**< Strikethrough text */ + +/** + * Set a font's current style. + * + * This updates any TTF_Text objects using this font, and clears + * already-generated glyphs, if any, from the cache. + * + * The font styles are a set of bit flags, OR'd together: + * + * - `TTF_STYLE_NORMAL` (is zero) + * - `TTF_STYLE_BOLD` + * - `TTF_STYLE_ITALIC` + * - `TTF_STYLE_UNDERLINE` + * - `TTF_STYLE_STRIKETHROUGH` + * + * \param font the font to set a new style on. + * \param style the new style values to set, OR'd together. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontStyle + */ +extern SDL_DECLSPEC void SDLCALL TTF_SetFontStyle(TTF_Font *font, TTF_FontStyleFlags style); + +/** + * Query a font's current style. + * + * The font styles are a set of bit flags, OR'd together: + * + * - `TTF_STYLE_NORMAL` (is zero) + * - `TTF_STYLE_BOLD` + * - `TTF_STYLE_ITALIC` + * - `TTF_STYLE_UNDERLINE` + * - `TTF_STYLE_STRIKETHROUGH` + * + * \param font the font to query. + * \returns the current font style, as a set of bit flags. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontStyle + */ +extern SDL_DECLSPEC TTF_FontStyleFlags SDLCALL TTF_GetFontStyle(const TTF_Font *font); + +/** + * Set a font's current outline. + * + * This uses the font properties `TTF_PROP_FONT_OUTLINE_LINE_CAP_NUMBER`, + * `TTF_PROP_FONT_OUTLINE_LINE_JOIN_NUMBER`, and + * `TTF_PROP_FONT_OUTLINE_MITER_LIMIT_NUMBER` when setting the font outline. + * + * This updates any TTF_Text objects using this font, and clears + * already-generated glyphs, if any, from the cache. + * + * \param font the font to set a new outline on. + * \param outline positive outline value, 0 to default. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontOutline + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontOutline(TTF_Font *font, int outline); + +/** + * Query a font's current outline. + * + * \param font the font to query. + * \returns the font's current outline value. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontOutline + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetFontOutline(const TTF_Font *font); + +/** + * Hinting flags for TTF (TrueType Fonts) + * + * This enum specifies the level of hinting to be applied to the font + * rendering. The hinting level determines how much the font's outlines are + * adjusted for better alignment on the pixel grid. + * + * \since This enum is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontHinting + * \sa TTF_GetFontHinting + */ +typedef enum TTF_HintingFlags +{ + TTF_HINTING_INVALID = -1, + TTF_HINTING_NORMAL, /**< Normal hinting applies standard grid-fitting. */ + TTF_HINTING_LIGHT, /**< Light hinting applies subtle adjustments to improve rendering. */ + TTF_HINTING_MONO, /**< Monochrome hinting adjusts the font for better rendering at lower resolutions. */ + TTF_HINTING_NONE, /**< No hinting, the font is rendered without any grid-fitting. */ + TTF_HINTING_LIGHT_SUBPIXEL /**< Light hinting with subpixel rendering for more precise font edges. */ +} TTF_HintingFlags; + +/** + * Set a font's current hinter setting. + * + * This updates any TTF_Text objects using this font, and clears + * already-generated glyphs, if any, from the cache. + * + * The hinter setting is a single value: + * + * - `TTF_HINTING_NORMAL` + * - `TTF_HINTING_LIGHT` + * - `TTF_HINTING_MONO` + * - `TTF_HINTING_NONE` + * - `TTF_HINTING_LIGHT_SUBPIXEL` (available in SDL_ttf 3.0.0 and later) + * + * \param font the font to set a new hinter setting on. + * \param hinting the new hinter setting. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontHinting + */ +extern SDL_DECLSPEC void SDLCALL TTF_SetFontHinting(TTF_Font *font, TTF_HintingFlags hinting); + +/** + * Query the number of faces of a font. + * + * \param font the font to query. + * \returns the number of FreeType font faces. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetNumFontFaces(const TTF_Font *font); + +/** + * Query a font's current FreeType hinter setting. + * + * The hinter setting is a single value: + * + * - `TTF_HINTING_NORMAL` + * - `TTF_HINTING_LIGHT` + * - `TTF_HINTING_MONO` + * - `TTF_HINTING_NONE` + * - `TTF_HINTING_LIGHT_SUBPIXEL` (available in SDL_ttf 3.0.0 and later) + * + * \param font the font to query. + * \returns the font's current hinter value, or TTF_HINTING_INVALID if the + * font is invalid. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontHinting + */ +extern SDL_DECLSPEC TTF_HintingFlags SDLCALL TTF_GetFontHinting(const TTF_Font *font); + +/** + * Enable Signed Distance Field rendering for a font. + * + * SDF is a technique that helps fonts look sharp even when scaling and + * rotating, and requires special shader support for display. + * + * This works with Blended APIs, and generates the raw signed distance values + * in the alpha channel of the resulting texture. + * + * This updates any TTF_Text objects using this font, and clears + * already-generated glyphs, if any, from the cache. + * + * \param font the font to set SDF support on. + * \param enabled true to enable SDF, false to disable. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontSDF + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontSDF(TTF_Font *font, bool enabled); + +/** + * Query whether Signed Distance Field rendering is enabled for a font. + * + * \param font the font to query. + * \returns true if enabled, false otherwise. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontSDF + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetFontSDF(const TTF_Font *font); + +/** + * Query a font's weight, in terms of the lightness/heaviness of the strokes. + * + * \param font the font to query. + * \returns the font's current weight. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.4.0. + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetFontWeight(const TTF_Font *font); + +#define TTF_FONT_WEIGHT_THIN 100 /**< Thin (100) named font weight value */ +#define TTF_FONT_WEIGHT_EXTRA_LIGHT 200 /**< ExtraLight (200) named font weight value */ +#define TTF_FONT_WEIGHT_LIGHT 300 /**< Light (300) named font weight value */ +#define TTF_FONT_WEIGHT_NORMAL 400 /**< Normal (400) named font weight value */ +#define TTF_FONT_WEIGHT_MEDIUM 500 /**< Medium (500) named font weight value */ +#define TTF_FONT_WEIGHT_SEMI_BOLD 600 /**< SemiBold (600) named font weight value */ +#define TTF_FONT_WEIGHT_BOLD 700 /**< Bold (700) named font weight value */ +#define TTF_FONT_WEIGHT_EXTRA_BOLD 800 /**< ExtraBold (800) named font weight value */ +#define TTF_FONT_WEIGHT_BLACK 900 /**< Black (900) named font weight value */ +#define TTF_FONT_WEIGHT_EXTRA_BLACK 950 /**< ExtraBlack (950) named font weight value */ + +/** + * The horizontal alignment used when rendering wrapped text. + * + * \since This enum is available since SDL_ttf 3.0.0. + */ +typedef enum TTF_HorizontalAlignment +{ + TTF_HORIZONTAL_ALIGN_INVALID = -1, + TTF_HORIZONTAL_ALIGN_LEFT, + TTF_HORIZONTAL_ALIGN_CENTER, + TTF_HORIZONTAL_ALIGN_RIGHT +} TTF_HorizontalAlignment; + +/** + * Set a font's current wrap alignment option. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to set a new wrap alignment option on. + * \param align the new wrap alignment option. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontWrapAlignment + */ +extern SDL_DECLSPEC void SDLCALL TTF_SetFontWrapAlignment(TTF_Font *font, TTF_HorizontalAlignment align); + +/** + * Query a font's current wrap alignment option. + * + * \param font the font to query. + * \returns the font's current wrap alignment option. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontWrapAlignment + */ +extern SDL_DECLSPEC TTF_HorizontalAlignment SDLCALL TTF_GetFontWrapAlignment(const TTF_Font *font); + +/** + * Query the total height of a font. + * + * This is usually equal to point size. + * + * \param font the font to query. + * \returns the font's height. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetFontHeight(const TTF_Font *font); + +/** + * Query the offset from the baseline to the top of a font. + * + * This is a positive value, relative to the baseline. + * + * \param font the font to query. + * \returns the font's ascent. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetFontAscent(const TTF_Font *font); + +/** + * Query the offset from the baseline to the bottom of a font. + * + * This is a negative value, relative to the baseline. + * + * \param font the font to query. + * \returns the font's descent. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetFontDescent(const TTF_Font *font); + +/** + * Set the spacing between lines of text for a font. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to modify. + * \param lineskip the new line spacing for the font. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontLineSkip + */ +extern SDL_DECLSPEC void SDLCALL TTF_SetFontLineSkip(TTF_Font *font, int lineskip); + +/** + * Query the spacing between lines of text for a font. + * + * \param font the font to query. + * \returns the font's recommended spacing. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontLineSkip + */ +extern SDL_DECLSPEC int SDLCALL TTF_GetFontLineSkip(const TTF_Font *font); + +/** + * Set if kerning is enabled for a font. + * + * Newly-opened fonts default to allowing kerning. This is generally a good + * policy unless you have a strong reason to disable it, as it tends to + * produce better rendering (with kerning disabled, some fonts might render + * the word `kerning` as something that looks like `keming` for example). + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to set kerning on. + * \param enabled true to enable kerning, false to disable. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetFontKerning + */ +extern SDL_DECLSPEC void SDLCALL TTF_SetFontKerning(TTF_Font *font, bool enabled); + +/** + * Query whether or not kerning is enabled for a font. + * + * \param font the font to query. + * \returns true if kerning is enabled, false otherwise. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontKerning + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetFontKerning(const TTF_Font *font); + +/** + * Query whether a font is fixed-width. + * + * A "fixed-width" font means all glyphs are the same width across; a + * lowercase 'i' will be the same size across as a capital 'W', for example. + * This is common for terminals and text editors, and other apps that treat + * text as a grid. Most other things (WYSIWYG word processors, web pages, etc) + * are more likely to not be fixed-width in most cases. + * + * \param font the font to query. + * \returns true if the font is fixed-width, false otherwise. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_FontIsFixedWidth(const TTF_Font *font); + +/** + * Query whether a font is scalable or not. + * + * Scalability lets us distinguish between outline and bitmap fonts. + * + * \param font the font to query. + * \returns true if the font is scalable, false otherwise. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontSDF + */ +extern SDL_DECLSPEC bool SDLCALL TTF_FontIsScalable(const TTF_Font *font); + +/** + * Query a font's family name. + * + * This string is dictated by the contents of the font file. + * + * Note that the returned string is to internal storage, and should not be + * modified or free'd by the caller. The string becomes invalid, with the rest + * of the font, when `font` is handed to TTF_CloseFont(). + * + * \param font the font to query. + * \returns the font's family name. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC const char * SDLCALL TTF_GetFontFamilyName(const TTF_Font *font); + +/** + * Query a font's style name. + * + * This string is dictated by the contents of the font file. + * + * Note that the returned string is to internal storage, and should not be + * modified or free'd by the caller. The string becomes invalid, with the rest + * of the font, when `font` is handed to TTF_CloseFont(). + * + * \param font the font to query. + * \returns the font's style name. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC const char * SDLCALL TTF_GetFontStyleName(const TTF_Font *font); + +/** + * Direction flags + * + * The values here are chosen to match + * [hb_direction_t](https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-direction-t) + * . + * + * \since This enum is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetFontDirection + */ +typedef enum TTF_Direction +{ + TTF_DIRECTION_INVALID = 0, + TTF_DIRECTION_LTR = 4, /**< Left to Right */ + TTF_DIRECTION_RTL, /**< Right to Left */ + TTF_DIRECTION_TTB, /**< Top to Bottom */ + TTF_DIRECTION_BTT /**< Bottom to Top */ +} TTF_Direction; + +/** + * Set the direction to be used for text shaping by a font. + * + * This function only supports left-to-right text shaping if SDL_ttf was not + * built with HarfBuzz support. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to modify. + * \param direction the new direction for text to flow. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontDirection(TTF_Font *font, TTF_Direction direction); + +/** + * Get the direction to be used for text shaping by a font. + * + * This defaults to TTF_DIRECTION_INVALID if it hasn't been set. + * + * \param font the font to query. + * \returns the direction to be used for text shaping. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC TTF_Direction SDLCALL TTF_GetFontDirection(TTF_Font *font); + +/** + * Convert from a 4 character string to a 32-bit tag. + * + * \param string the 4 character string to convert. + * \returns the 32-bit representation of the string. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_TagToString + */ +extern SDL_DECLSPEC Uint32 SDLCALL TTF_StringToTag(const char *string); + +/** + * Convert from a 32-bit tag to a 4 character string. + * + * \param tag the 32-bit tag to convert. + * \param string a pointer filled in with the 4 character representation of + * the tag. + * \param size the size of the buffer pointed at by string, should be at least + * 4. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_TagToString + */ +extern SDL_DECLSPEC void SDLCALL TTF_TagToString(Uint32 tag, char *string, size_t size); + +/** + * Set the script to be used for text shaping by a font. + * + * This returns false if SDL_ttf isn't built with HarfBuzz support. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to modify. + * \param script an + * [ISO 15924 code](https://unicode.org/iso15924/iso15924-codes.html) + * . + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_StringToTag + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontScript(TTF_Font *font, Uint32 script); + +/** + * Get the script used for text shaping a font. + * + * \param font the font to query. + * \returns an + * [ISO 15924 code](https://unicode.org/iso15924/iso15924-codes.html) + * or 0 if a script hasn't been set. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_TagToString + */ +extern SDL_DECLSPEC Uint32 SDLCALL TTF_GetFontScript(TTF_Font *font); + +/** + * Get the script used by a 32-bit codepoint. + * + * \param ch the character code to check. + * \returns an + * [ISO 15924 code](https://unicode.org/iso15924/iso15924-codes.html) + * on success, or 0 on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function is thread-safe. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_TagToString + */ +extern SDL_DECLSPEC Uint32 SDLCALL TTF_GetGlyphScript(Uint32 ch); + +/** + * Set language to be used for text shaping by a font. + * + * If SDL_ttf was not built with HarfBuzz support, this function returns + * false. + * + * This updates any TTF_Text objects using this font. + * + * \param font the font to specify a language for. + * \param language_bcp47 a null-terminated string containing the desired + * language's BCP47 code. Or null to reset the value. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetFontLanguage(TTF_Font *font, const char *language_bcp47); + +/** + * Check whether a glyph is provided by the font for a UNICODE codepoint. + * + * \param font the font to query. + * \param ch the codepoint to check. + * \returns true if font provides a glyph for this character, false if not. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_FontHasGlyph(TTF_Font *font, Uint32 ch); + +/** + * The type of data in a glyph image + * + * \since This enum is available since SDL_ttf 3.0.0. + */ +typedef enum TTF_ImageType +{ + TTF_IMAGE_INVALID, + TTF_IMAGE_ALPHA, /**< The color channels are white */ + TTF_IMAGE_COLOR, /**< The color channels have image data */ + TTF_IMAGE_SDF, /**< The alpha channel has signed distance field information */ +} TTF_ImageType; + +/** + * Get the pixel image for a UNICODE codepoint. + * + * \param font the font to query. + * \param ch the codepoint to check. + * \param image_type a pointer filled in with the glyph image type, may be + * NULL. + * \returns an SDL_Surface containing the glyph, or NULL on failure; call + * SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_GetGlyphImage(TTF_Font *font, Uint32 ch, TTF_ImageType *image_type); + +/** + * Get the pixel image for a character index. + * + * This is useful for text engine implementations, which can call this with + * the `glyph_index` in a TTF_CopyOperation + * + * \param font the font to query. + * \param glyph_index the index of the glyph to return. + * \param image_type a pointer filled in with the glyph image type, may be + * NULL. + * \returns an SDL_Surface containing the glyph, or NULL on failure; call + * SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_GetGlyphImageForIndex(TTF_Font *font, Uint32 glyph_index, TTF_ImageType *image_type); + +/** + * Query the metrics (dimensions) of a font's glyph for a UNICODE codepoint. + * + * To understand what these metrics mean, here is a useful link: + * + * https://freetype.sourceforge.net/freetype2/docs/tutorial/step2.html + * + * \param font the font to query. + * \param ch the codepoint to check. + * \param minx a pointer filled in with the minimum x coordinate of the glyph + * from the left edge of its bounding box. This value may be + * negative. + * \param maxx a pointer filled in with the maximum x coordinate of the glyph + * from the left edge of its bounding box. + * \param miny a pointer filled in with the minimum y coordinate of the glyph + * from the bottom edge of its bounding box. This value may be + * negative. + * \param maxy a pointer filled in with the maximum y coordinate of the glyph + * from the bottom edge of its bounding box. + * \param advance a pointer filled in with the distance to the next glyph from + * the left edge of this glyph's bounding box. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetGlyphMetrics(TTF_Font *font, Uint32 ch, int *minx, int *maxx, int *miny, int *maxy, int *advance); + +/** + * Query the kerning size between the glyphs of two UNICODE codepoints. + * + * \param font the font to query. + * \param previous_ch the previous codepoint. + * \param ch the current codepoint. + * \param kerning a pointer filled in with the kerning size between the two + * glyphs, in pixels, may be NULL. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetGlyphKerning(TTF_Font *font, Uint32 previous_ch, Uint32 ch, int *kerning); + +/** + * Calculate the dimensions of a rendered string of UTF-8 text. + * + * This will report the width and height, in pixels, of the space that the + * specified string will take to fully render. + * + * \param font the font to query. + * \param text text to calculate, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param w will be filled with width, in pixels, on return. + * \param h will be filled with height, in pixels, on return. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetStringSize(TTF_Font *font, const char *text, size_t length, int *w, int *h); + +/** + * Calculate the dimensions of a rendered string of UTF-8 text. + * + * This will report the width and height, in pixels, of the space that the + * specified string will take to fully render. + * + * Text is wrapped to multiple lines on line endings and on word boundaries if + * it extends beyond `wrap_width` in pixels. + * + * If wrap_width is 0, this function will only wrap on newline characters. + * + * \param font the font to query. + * \param text text to calculate, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param wrap_width the maximum width or 0 to wrap on newline characters. + * \param w will be filled with width, in pixels, on return. + * \param h will be filled with height, in pixels, on return. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetStringSizeWrapped(TTF_Font *font, const char *text, size_t length, int wrap_width, int *w, int *h); + +/** + * Calculate how much of a UTF-8 string will fit in a given width. + * + * This reports the number of characters that can be rendered before reaching + * `max_width`. + * + * This does not need to render the string to do this calculation. + * + * \param font the font to query. + * \param text text to calculate, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param max_width maximum width, in pixels, available for the string, or 0 + * for unbounded width. + * \param measured_width a pointer filled in with the width, in pixels, of the + * string that will fit, may be NULL. + * \param measured_length a pointer filled in with the length, in bytes, of + * the string that will fit, may be NULL. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_MeasureString(TTF_Font *font, const char *text, size_t length, int max_width, int *measured_width, size_t *measured_length); + +/** + * Render UTF-8 text at fast quality to a new 8-bit surface. + * + * This function will allocate a new 8-bit, palettized surface. The surface's + * 0 pixel will be the colorkey, giving a transparent background. The 1 pixel + * will be set to the text color. + * + * This will not word-wrap the string; you'll get a surface with a single line + * of text, as long as the string requires. You can use + * TTF_RenderText_Solid_Wrapped() instead if you need to wrap the output to + * multiple lines. + * + * This will not wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Shaded, + * TTF_RenderText_Blended, and TTF_RenderText_LCD. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \returns a new 8-bit, palettized surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended + * \sa TTF_RenderText_LCD + * \sa TTF_RenderText_Shaded + * \sa TTF_RenderText_Solid + * \sa TTF_RenderText_Solid_Wrapped + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_Solid(TTF_Font *font, const char *text, size_t length, SDL_Color fg); + +/** + * Render word-wrapped UTF-8 text at fast quality to a new 8-bit surface. + * + * This function will allocate a new 8-bit, palettized surface. The surface's + * 0 pixel will be the colorkey, giving a transparent background. The 1 pixel + * will be set to the text color. + * + * Text is wrapped to multiple lines on line endings and on word boundaries if + * it extends beyond `wrapLength` in pixels. + * + * If wrapLength is 0, this function will only wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Shaded_Wrapped, + * TTF_RenderText_Blended_Wrapped, and TTF_RenderText_LCD_Wrapped. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \param wrapLength the maximum width of the text surface or 0 to wrap on + * newline characters. + * \returns a new 8-bit, palettized surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended_Wrapped + * \sa TTF_RenderText_LCD_Wrapped + * \sa TTF_RenderText_Shaded_Wrapped + * \sa TTF_RenderText_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_Solid_Wrapped(TTF_Font *font, const char *text, size_t length, SDL_Color fg, int wrapLength); + +/** + * Render a single 32-bit glyph at fast quality to a new 8-bit surface. + * + * This function will allocate a new 8-bit, palettized surface. The surface's + * 0 pixel will be the colorkey, giving a transparent background. The 1 pixel + * will be set to the text color. + * + * The glyph is rendered without any padding or centering in the X direction, + * and aligned normally in the Y direction. + * + * You can render at other quality levels with TTF_RenderGlyph_Shaded, + * TTF_RenderGlyph_Blended, and TTF_RenderGlyph_LCD. + * + * \param font the font to render with. + * \param ch the character to render. + * \param fg the foreground color for the text. + * \returns a new 8-bit, palettized surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderGlyph_Blended + * \sa TTF_RenderGlyph_LCD + * \sa TTF_RenderGlyph_Shaded + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderGlyph_Solid(TTF_Font *font, Uint32 ch, SDL_Color fg); + +/** + * Render UTF-8 text at high quality to a new 8-bit surface. + * + * This function will allocate a new 8-bit, palettized surface. The surface's + * 0 pixel will be the specified background color, while other pixels have + * varying degrees of the foreground color. This function returns the new + * surface, or NULL if there was an error. + * + * This will not word-wrap the string; you'll get a surface with a single line + * of text, as long as the string requires. You can use + * TTF_RenderText_Shaded_Wrapped() instead if you need to wrap the output to + * multiple lines. + * + * This will not wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Solid, + * TTF_RenderText_Blended, and TTF_RenderText_LCD. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \param bg the background color for the text. + * \returns a new 8-bit, palettized surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended + * \sa TTF_RenderText_LCD + * \sa TTF_RenderText_Shaded_Wrapped + * \sa TTF_RenderText_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_Shaded(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg); + +/** + * Render word-wrapped UTF-8 text at high quality to a new 8-bit surface. + * + * This function will allocate a new 8-bit, palettized surface. The surface's + * 0 pixel will be the specified background color, while other pixels have + * varying degrees of the foreground color. This function returns the new + * surface, or NULL if there was an error. + * + * Text is wrapped to multiple lines on line endings and on word boundaries if + * it extends beyond `wrap_width` in pixels. + * + * If wrap_width is 0, this function will only wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Solid_Wrapped, + * TTF_RenderText_Blended_Wrapped, and TTF_RenderText_LCD_Wrapped. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \param bg the background color for the text. + * \param wrap_width the maximum width of the text surface or 0 to wrap on + * newline characters. + * \returns a new 8-bit, palettized surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended_Wrapped + * \sa TTF_RenderText_LCD_Wrapped + * \sa TTF_RenderText_Shaded + * \sa TTF_RenderText_Solid_Wrapped + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_Shaded_Wrapped(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg, int wrap_width); + +/** + * Render a single UNICODE codepoint at high quality to a new 8-bit surface. + * + * This function will allocate a new 8-bit, palettized surface. The surface's + * 0 pixel will be the specified background color, while other pixels have + * varying degrees of the foreground color. This function returns the new + * surface, or NULL if there was an error. + * + * The glyph is rendered without any padding or centering in the X direction, + * and aligned normally in the Y direction. + * + * You can render at other quality levels with TTF_RenderGlyph_Solid, + * TTF_RenderGlyph_Blended, and TTF_RenderGlyph_LCD. + * + * \param font the font to render with. + * \param ch the codepoint to render. + * \param fg the foreground color for the text. + * \param bg the background color for the text. + * \returns a new 8-bit, palettized surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderGlyph_Blended + * \sa TTF_RenderGlyph_LCD + * \sa TTF_RenderGlyph_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderGlyph_Shaded(TTF_Font *font, Uint32 ch, SDL_Color fg, SDL_Color bg); + +/** + * Render UTF-8 text at high quality to a new ARGB surface. + * + * This function will allocate a new 32-bit, ARGB surface, using alpha + * blending to dither the font with the given color. This function returns the + * new surface, or NULL if there was an error. + * + * This will not word-wrap the string; you'll get a surface with a single line + * of text, as long as the string requires. You can use + * TTF_RenderText_Blended_Wrapped() instead if you need to wrap the output to + * multiple lines. + * + * This will not wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Solid, + * TTF_RenderText_Shaded, and TTF_RenderText_LCD. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \returns a new 32-bit, ARGB surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended_Wrapped + * \sa TTF_RenderText_LCD + * \sa TTF_RenderText_Shaded + * \sa TTF_RenderText_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_Blended(TTF_Font *font, const char *text, size_t length, SDL_Color fg); + +/** + * Render word-wrapped UTF-8 text at high quality to a new ARGB surface. + * + * This function will allocate a new 32-bit, ARGB surface, using alpha + * blending to dither the font with the given color. This function returns the + * new surface, or NULL if there was an error. + * + * Text is wrapped to multiple lines on line endings and on word boundaries if + * it extends beyond `wrap_width` in pixels. + * + * If wrap_width is 0, this function will only wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Solid_Wrapped, + * TTF_RenderText_Shaded_Wrapped, and TTF_RenderText_LCD_Wrapped. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \param wrap_width the maximum width of the text surface or 0 to wrap on + * newline characters. + * \returns a new 32-bit, ARGB surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended + * \sa TTF_RenderText_LCD_Wrapped + * \sa TTF_RenderText_Shaded_Wrapped + * \sa TTF_RenderText_Solid_Wrapped + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_Blended_Wrapped(TTF_Font *font, const char *text, size_t length, SDL_Color fg, int wrap_width); + +/** + * Render a single UNICODE codepoint at high quality to a new ARGB surface. + * + * This function will allocate a new 32-bit, ARGB surface, using alpha + * blending to dither the font with the given color. This function returns the + * new surface, or NULL if there was an error. + * + * The glyph is rendered without any padding or centering in the X direction, + * and aligned normally in the Y direction. + * + * You can render at other quality levels with TTF_RenderGlyph_Solid, + * TTF_RenderGlyph_Shaded, and TTF_RenderGlyph_LCD. + * + * \param font the font to render with. + * \param ch the codepoint to render. + * \param fg the foreground color for the text. + * \returns a new 32-bit, ARGB surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderGlyph_LCD + * \sa TTF_RenderGlyph_Shaded + * \sa TTF_RenderGlyph_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderGlyph_Blended(TTF_Font *font, Uint32 ch, SDL_Color fg); + +/** + * Render UTF-8 text at LCD subpixel quality to a new ARGB surface. + * + * This function will allocate a new 32-bit, ARGB surface, and render + * alpha-blended text using FreeType's LCD subpixel rendering. This function + * returns the new surface, or NULL if there was an error. + * + * This will not word-wrap the string; you'll get a surface with a single line + * of text, as long as the string requires. You can use + * TTF_RenderText_LCD_Wrapped() instead if you need to wrap the output to + * multiple lines. + * + * This will not wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Solid, + * TTF_RenderText_Shaded, and TTF_RenderText_Blended. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \param bg the background color for the text. + * \returns a new 32-bit, ARGB surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended + * \sa TTF_RenderText_LCD_Wrapped + * \sa TTF_RenderText_Shaded + * \sa TTF_RenderText_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_LCD(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg); + +/** + * Render word-wrapped UTF-8 text at LCD subpixel quality to a new ARGB + * surface. + * + * This function will allocate a new 32-bit, ARGB surface, and render + * alpha-blended text using FreeType's LCD subpixel rendering. This function + * returns the new surface, or NULL if there was an error. + * + * Text is wrapped to multiple lines on line endings and on word boundaries if + * it extends beyond `wrap_width` in pixels. + * + * If wrap_width is 0, this function will only wrap on newline characters. + * + * You can render at other quality levels with TTF_RenderText_Solid_Wrapped, + * TTF_RenderText_Shaded_Wrapped, and TTF_RenderText_Blended_Wrapped. + * + * \param font the font to render with. + * \param text text to render, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \param fg the foreground color for the text. + * \param bg the background color for the text. + * \param wrap_width the maximum width of the text surface or 0 to wrap on + * newline characters. + * \returns a new 32-bit, ARGB surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderText_Blended_Wrapped + * \sa TTF_RenderText_LCD + * \sa TTF_RenderText_Shaded_Wrapped + * \sa TTF_RenderText_Solid_Wrapped + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_LCD_Wrapped(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg, int wrap_width); + +/** + * Render a single UNICODE codepoint at LCD subpixel quality to a new ARGB + * surface. + * + * This function will allocate a new 32-bit, ARGB surface, and render + * alpha-blended text using FreeType's LCD subpixel rendering. This function + * returns the new surface, or NULL if there was an error. + * + * The glyph is rendered without any padding or centering in the X direction, + * and aligned normally in the Y direction. + * + * You can render at other quality levels with TTF_RenderGlyph_Solid, + * TTF_RenderGlyph_Shaded, and TTF_RenderGlyph_Blended. + * + * \param font the font to render with. + * \param ch the codepoint to render. + * \param fg the foreground color for the text. + * \param bg the background color for the text. + * \returns a new 32-bit, ARGB surface, or NULL if there was an error. + * + * \threadsafety This function should be called on the thread that created the + * font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_RenderGlyph_Blended + * \sa TTF_RenderGlyph_Shaded + * \sa TTF_RenderGlyph_Solid + */ +extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderGlyph_LCD(TTF_Font *font, Uint32 ch, SDL_Color fg, SDL_Color bg); + + +/** + * A text engine used to create text objects. + * + * This is a public interface that can be used by applications and libraries + * to perform customize rendering with text objects. See + * for details. + * + * There are three text engines provided with the library: + * + * - Drawing to an SDL_Surface, created with TTF_CreateSurfaceTextEngine() + * - Drawing with an SDL 2D renderer, created with + * TTF_CreateRendererTextEngine() + * - Drawing with the SDL GPU API, created with TTF_CreateGPUTextEngine() + * + * \since This struct is available since SDL_ttf 3.0.0. + */ +typedef struct TTF_TextEngine TTF_TextEngine; + +/** + * Internal data for TTF_Text + * + * \since This struct is available since SDL_ttf 3.0.0. + */ +typedef struct TTF_TextData TTF_TextData; + +/** + * Text created with TTF_CreateText() + * + * \since This struct is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateText + * \sa TTF_GetTextProperties + * \sa TTF_DestroyText + */ +typedef struct TTF_Text +{ + char *text; /**< A copy of the UTF-8 string that this text object represents, useful for layout, debugging and retrieving substring text. This is updated when the text object is modified and will be freed automatically when the object is destroyed. */ + int num_lines; /**< The number of lines in the text, 0 if it's empty */ + + int refcount; /**< Application reference count, used when freeing surface */ + + TTF_TextData *internal; /**< Private */ + +} TTF_Text; + +/** + * Create a text engine for drawing text on SDL surfaces. + * + * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() + * for more information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_DestroySurfaceTextEngine + * \sa TTF_DrawSurfaceText + */ +extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateSurfaceTextEngine(void); + +/** + * Draw text to an SDL surface. + * + * `text` must have been created using a TTF_TextEngine from + * TTF_CreateSurfaceTextEngine(). + * + * \param text the text to draw. + * \param x the x coordinate in pixels, positive from the left edge towards + * the right. + * \param y the y coordinate in pixels, positive from the top edge towards the + * bottom. + * \param surface the surface to draw on. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateSurfaceTextEngine + * \sa TTF_CreateText + */ +extern SDL_DECLSPEC bool SDLCALL TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface); + +/** + * Destroy a text engine created for drawing text on SDL surfaces. + * + * All text created by this engine should be destroyed before calling this + * function. + * + * \param engine a TTF_TextEngine object created with + * TTF_CreateSurfaceTextEngine(). + * + * \threadsafety This function should be called on the thread that created the + * engine. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateSurfaceTextEngine + */ +extern SDL_DECLSPEC void SDLCALL TTF_DestroySurfaceTextEngine(TTF_TextEngine *engine); + +/** + * Create a text engine for drawing text on an SDL renderer. + * + * \param renderer the renderer to use for creating textures and drawing text. + * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() + * for more information. + * + * \threadsafety This function should be called on the thread that created the + * renderer. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_DestroyRendererTextEngine + * \sa TTF_DrawRendererText + * \sa TTF_CreateRendererTextEngineWithProperties + */ +extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateRendererTextEngine(SDL_Renderer *renderer); + +/** + * Create a text engine for drawing text on an SDL renderer, with the + * specified properties. + * + * These are the supported properties: + * + * - `TTF_PROP_RENDERER_TEXT_ENGINE_RENDERER`: the renderer to use for + * creating textures and drawing text + * - `TTF_PROP_RENDERER_TEXT_ENGINE_ATLAS_TEXTURE_SIZE`: the size of the + * texture atlas + * + * \param props the properties to use. + * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() + * for more information. + * + * \threadsafety This function should be called on the thread that created the + * renderer. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateRendererTextEngine + * \sa TTF_DestroyRendererTextEngine + * \sa TTF_DrawRendererText + */ +extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateRendererTextEngineWithProperties(SDL_PropertiesID props); + +#define TTF_PROP_RENDERER_TEXT_ENGINE_RENDERER "SDL_ttf.renderer_text_engine.create.renderer" +#define TTF_PROP_RENDERER_TEXT_ENGINE_ATLAS_TEXTURE_SIZE "SDL_ttf.renderer_text_engine.create.atlas_texture_size" + +/** + * Draw text to an SDL renderer. + * + * `text` must have been created using a TTF_TextEngine from + * TTF_CreateRendererTextEngine(), and will draw using the renderer passed to + * that function. + * + * \param text the text to draw. + * \param x the x coordinate in pixels, positive from the left edge towards + * the right. + * \param y the y coordinate in pixels, positive from the top edge towards the + * bottom. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateRendererTextEngine + * \sa TTF_CreateText + */ +extern SDL_DECLSPEC bool SDLCALL TTF_DrawRendererText(TTF_Text *text, float x, float y); + +/** + * Destroy a text engine created for drawing text on an SDL renderer. + * + * All text created by this engine should be destroyed before calling this + * function. + * + * \param engine a TTF_TextEngine object created with + * TTF_CreateRendererTextEngine(). + * + * \threadsafety This function should be called on the thread that created the + * engine. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateRendererTextEngine + */ +extern SDL_DECLSPEC void SDLCALL TTF_DestroyRendererTextEngine(TTF_TextEngine *engine); + +/** + * Create a text engine for drawing text with the SDL GPU API. + * + * \param device the SDL_GPUDevice to use for creating textures and drawing + * text. + * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() + * for more information. + * + * \threadsafety This function should be called on the thread that created the + * device. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateGPUTextEngineWithProperties + * \sa TTF_DestroyGPUTextEngine + * \sa TTF_GetGPUTextDrawData + */ +extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateGPUTextEngine(SDL_GPUDevice *device); + +/** + * Create a text engine for drawing text with the SDL GPU API, with the + * specified properties. + * + * These are the supported properties: + * + * - `TTF_PROP_GPU_TEXT_ENGINE_DEVICE`: the SDL_GPUDevice to use for creating + * textures and drawing text. + * - `TTF_PROP_GPU_TEXT_ENGINE_ATLAS_TEXTURE_SIZE`: the size of the texture + * atlas + * + * \param props the properties to use. + * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() + * for more information. + * + * \threadsafety This function should be called on the thread that created the + * device. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateGPUTextEngine + * \sa TTF_DestroyGPUTextEngine + * \sa TTF_GetGPUTextDrawData + */ +extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateGPUTextEngineWithProperties(SDL_PropertiesID props); + +#define TTF_PROP_GPU_TEXT_ENGINE_DEVICE "SDL_ttf.gpu_text_engine.create.device" +#define TTF_PROP_GPU_TEXT_ENGINE_ATLAS_TEXTURE_SIZE "SDL_ttf.gpu_text_engine.create.atlas_texture_size" + +/** + * Draw sequence returned by TTF_GetGPUTextDrawData + * + * \since This struct is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetGPUTextDrawData + */ +typedef struct TTF_GPUAtlasDrawSequence +{ + SDL_GPUTexture *atlas_texture; /**< Texture atlas that stores the glyphs */ + SDL_FPoint *xy; /**< An array of vertex positions */ + SDL_FPoint *uv; /**< An array of normalized texture coordinates for each vertex */ + int num_vertices; /**< Number of vertices */ + int *indices; /**< An array of indices into the 'vertices' arrays */ + int num_indices; /**< Number of indices */ + TTF_ImageType image_type; /**< The image type of this draw sequence */ + + struct TTF_GPUAtlasDrawSequence *next; /**< The next sequence (will be NULL in case of the last sequence) */ +} TTF_GPUAtlasDrawSequence; + +/** + * Get the geometry data needed for drawing the text. + * + * `text` must have been created using a TTF_TextEngine from + * TTF_CreateGPUTextEngine(). + * + * The positive X-axis is taken towards the right and the positive Y-axis is + * taken upwards for both the vertex and the texture coordinates, i.e, it + * follows the same convention used by the SDL_GPU API. If you want to use a + * different coordinate system you will need to transform the vertices + * yourself. + * + * If the text looks blocky use linear filtering. + * + * \param text the text to draw. + * \returns a NULL terminated linked list of TTF_GPUAtlasDrawSequence objects + * or NULL if the passed text is empty or in case of failure; call + * SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateGPUTextEngine + * \sa TTF_CreateText + */ +extern SDL_DECLSPEC TTF_GPUAtlasDrawSequence * SDLCALL TTF_GetGPUTextDrawData(TTF_Text *text); + +/** + * Destroy a text engine created for drawing text with the SDL GPU API. + * + * All text created by this engine should be destroyed before calling this + * function. + * + * \param engine a TTF_TextEngine object created with + * TTF_CreateGPUTextEngine(). + * + * \threadsafety This function should be called on the thread that created the + * engine. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateGPUTextEngine + */ +extern SDL_DECLSPEC void SDLCALL TTF_DestroyGPUTextEngine(TTF_TextEngine *engine); + +/** + * The winding order of the vertices returned by TTF_GetGPUTextDrawData + * + * \since This enum is available since SDL_ttf 3.0.0. + */ +typedef enum TTF_GPUTextEngineWinding +{ + TTF_GPU_TEXTENGINE_WINDING_INVALID = -1, + TTF_GPU_TEXTENGINE_WINDING_CLOCKWISE, + TTF_GPU_TEXTENGINE_WINDING_COUNTER_CLOCKWISE +} TTF_GPUTextEngineWinding; + +/** + * Sets the winding order of the vertices returned by TTF_GetGPUTextDrawData + * for a particular GPU text engine. + * + * \param engine a TTF_TextEngine object created with + * TTF_CreateGPUTextEngine(). + * \param winding the new winding order option. + * + * \threadsafety This function should be called on the thread that created the + * engine. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetGPUTextEngineWinding + */ +extern SDL_DECLSPEC void SDLCALL TTF_SetGPUTextEngineWinding(TTF_TextEngine *engine, TTF_GPUTextEngineWinding winding); + +/** + * Get the winding order of the vertices returned by TTF_GetGPUTextDrawData + * for a particular GPU text engine + * + * \param engine a TTF_TextEngine object created with + * TTF_CreateGPUTextEngine(). + * \returns the winding order used by the GPU text engine or + * TTF_GPU_TEXTENGINE_WINDING_INVALID in case of error. + * + * \threadsafety This function should be called on the thread that created the + * engine. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetGPUTextEngineWinding + */ +extern SDL_DECLSPEC TTF_GPUTextEngineWinding SDLCALL TTF_GetGPUTextEngineWinding(const TTF_TextEngine *engine); + +/** + * Create a text object from UTF-8 text and a text engine. + * + * \param engine the text engine to use when creating the text object, may be + * NULL. + * \param font the font to render with. + * \param text the text to use, in UTF-8 encoding. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \returns a TTF_Text object or NULL on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * font and text engine. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_DestroyText + */ +extern SDL_DECLSPEC TTF_Text * SDLCALL TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length); + +/** + * Get the properties associated with a text object. + * + * \param text the TTF_Text to query. + * \returns a valid property ID on success or 0 on failure; call + * SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC SDL_PropertiesID SDLCALL TTF_GetTextProperties(TTF_Text *text); + +/** + * Set the text engine used by a text object. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param engine the text engine to use for drawing. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextEngine + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextEngine(TTF_Text *text, TTF_TextEngine *engine); + +/** + * Get the text engine used by a text object. + * + * \param text the TTF_Text to query. + * \returns the TTF_TextEngine used by the text on success or NULL on failure; + * call SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetTextEngine + */ +extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_GetTextEngine(TTF_Text *text); + +/** + * Set the font used by a text object. + * + * When a text object has a font, any changes to the font will automatically + * regenerate the text. If you set the font to NULL, the text will continue to + * render but changes to the font will no longer affect the text. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param font the font to use, may be NULL. + * \returns false if the text pointer is null; otherwise, true. call + * SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextFont + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextFont(TTF_Text *text, TTF_Font *font); + +/** + * Get the font used by a text object. + * + * \param text the TTF_Text to query. + * \returns the TTF_Font used by the text on success or NULL on failure; call + * SDL_GetError() for more information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetTextFont + */ +extern SDL_DECLSPEC TTF_Font * SDLCALL TTF_GetTextFont(TTF_Text *text); + +/** + * Set the direction to be used for text shaping a text object. + * + * This function only supports left-to-right text shaping if SDL_ttf was not + * built with HarfBuzz support. + * + * \param text the text to modify. + * \param direction the new direction for text to flow. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextDirection(TTF_Text *text, TTF_Direction direction); + +/** + * Get the direction to be used for text shaping a text object. + * + * This defaults to the direction of the font used by the text object. + * + * \param text the text to query. + * \returns the direction to be used for text shaping. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC TTF_Direction SDLCALL TTF_GetTextDirection(TTF_Text *text); + +/** + * Set the script to be used for text shaping a text object. + * + * This returns false if SDL_ttf isn't built with HarfBuzz support. + * + * \param text the text to modify. + * \param script an + * [ISO 15924 code](https://unicode.org/iso15924/iso15924-codes.html) + * . + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_StringToTag + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextScript(TTF_Text *text, Uint32 script); + +/** + * Get the script used for text shaping a text object. + * + * This defaults to the script of the font used by the text object. + * + * \param text the text to query. + * \returns an + * [ISO 15924 code](https://unicode.org/iso15924/iso15924-codes.html) + * or 0 if a script hasn't been set on either the text object or the + * font. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_TagToString + */ +extern SDL_DECLSPEC Uint32 SDLCALL TTF_GetTextScript(TTF_Text *text); + +/** + * Set the color of a text object. + * + * The default text color is white (255, 255, 255, 255). + * + * \param text the TTF_Text to modify. + * \param r the red color value in the range of 0-255. + * \param g the green color value in the range of 0-255. + * \param b the blue color value in the range of 0-255. + * \param a the alpha value in the range of 0-255. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextColor + * \sa TTF_SetTextColorFloat + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextColor(TTF_Text *text, Uint8 r, Uint8 g, Uint8 b, Uint8 a); + +/** + * Set the color of a text object. + * + * The default text color is white (1.0f, 1.0f, 1.0f, 1.0f). + * + * \param text the TTF_Text to modify. + * \param r the red color value, normally in the range of 0-1. + * \param g the green color value, normally in the range of 0-1. + * \param b the blue color value, normally in the range of 0-1. + * \param a the alpha value in the range of 0-1. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextColorFloat + * \sa TTF_SetTextColor + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextColorFloat(TTF_Text *text, float r, float g, float b, float a); + +/** + * Get the color of a text object. + * + * \param text the TTF_Text to query. + * \param r a pointer filled in with the red color value in the range of + * 0-255, may be NULL. + * \param g a pointer filled in with the green color value in the range of + * 0-255, may be NULL. + * \param b a pointer filled in with the blue color value in the range of + * 0-255, may be NULL. + * \param a a pointer filled in with the alpha value in the range of 0-255, + * may be NULL. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextColorFloat + * \sa TTF_SetTextColor + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextColor(TTF_Text *text, Uint8 *r, Uint8 *g, Uint8 *b, Uint8 *a); + +/** + * Get the color of a text object. + * + * \param text the TTF_Text to query. + * \param r a pointer filled in with the red color value, normally in the + * range of 0-1, may be NULL. + * \param g a pointer filled in with the green color value, normally in the + * range of 0-1, may be NULL. + * \param b a pointer filled in with the blue color value, normally in the + * range of 0-1, may be NULL. + * \param a a pointer filled in with the alpha value in the range of 0-1, may + * be NULL. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextColor + * \sa TTF_SetTextColorFloat + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextColorFloat(TTF_Text *text, float *r, float *g, float *b, float *a); + +/** + * Set the position of a text object. + * + * This can be used to position multiple text objects within a single wrapping + * text area. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param x the x offset of the upper left corner of this text in pixels. + * \param y the y offset of the upper left corner of this text in pixels. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextPosition + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextPosition(TTF_Text *text, int x, int y); + +/** + * Get the position of a text object. + * + * \param text the TTF_Text to query. + * \param x a pointer filled in with the x offset of the upper left corner of + * this text in pixels, may be NULL. + * \param y a pointer filled in with the y offset of the upper left corner of + * this text in pixels, may be NULL. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetTextPosition + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextPosition(TTF_Text *text, int *x, int *y); + +/** + * Set whether wrapping is enabled on a text object. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param wrap_width the maximum width in pixels, 0 to wrap on newline + * characters. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetTextWrapWidth + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextWrapWidth(TTF_Text *text, int wrap_width); + +/** + * Get whether wrapping is enabled on a text object. + * + * \param text the TTF_Text to query. + * \param wrap_width a pointer filled in with the maximum width in pixels or 0 + * if the text is being wrapped on newline characters. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetTextWrapWidth + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextWrapWidth(TTF_Text *text, int *wrap_width); + +/** + * Set whether whitespace should be visible when wrapping a text object. + * + * If the whitespace is visible, it will take up space for purposes of + * alignment and wrapping. This is good for editing, but looks better when + * centered or aligned if whitespace around line wrapping is hidden. This + * defaults false. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param visible true to show whitespace when wrapping text, false to hide + * it. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_TextWrapWhitespaceVisible + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextWrapWhitespaceVisible(TTF_Text *text, bool visible); + +/** + * Return whether whitespace is shown when wrapping a text object. + * + * \param text the TTF_Text to query. + * \returns true if whitespace is shown when wrapping text, or false + * otherwise. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_SetTextWrapWhitespaceVisible + */ +extern SDL_DECLSPEC bool SDLCALL TTF_TextWrapWhitespaceVisible(TTF_Text *text); + +/** + * Set the UTF-8 text used by a text object. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param string the UTF-8 text to use, may be NULL. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_AppendTextString + * \sa TTF_DeleteTextString + * \sa TTF_InsertTextString + */ +extern SDL_DECLSPEC bool SDLCALL TTF_SetTextString(TTF_Text *text, const char *string, size_t length); + +/** + * Insert UTF-8 text into a text object. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param offset the offset, in bytes, from the beginning of the string if >= + * 0, the offset from the end of the string if < 0. Note that + * this does not do UTF-8 validation, so you should only insert + * at UTF-8 sequence boundaries. + * \param string the UTF-8 text to insert. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_AppendTextString + * \sa TTF_DeleteTextString + * \sa TTF_SetTextString + */ +extern SDL_DECLSPEC bool SDLCALL TTF_InsertTextString(TTF_Text *text, int offset, const char *string, size_t length); + +/** + * Append UTF-8 text to a text object. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param string the UTF-8 text to insert. + * \param length the length of the text, in bytes, or 0 for null terminated + * text. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_DeleteTextString + * \sa TTF_InsertTextString + * \sa TTF_SetTextString + */ +extern SDL_DECLSPEC bool SDLCALL TTF_AppendTextString(TTF_Text *text, const char *string, size_t length); + +/** + * Delete UTF-8 text from a text object. + * + * This function may cause the internal text representation to be rebuilt. + * + * \param text the TTF_Text to modify. + * \param offset the offset, in bytes, from the beginning of the string if >= + * 0, the offset from the end of the string if < 0. Note that + * this does not do UTF-8 validation, so you should only delete + * at UTF-8 sequence boundaries. + * \param length the length of text to delete, in bytes, or -1 for the + * remainder of the string. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_AppendTextString + * \sa TTF_InsertTextString + * \sa TTF_SetTextString + */ +extern SDL_DECLSPEC bool SDLCALL TTF_DeleteTextString(TTF_Text *text, int offset, int length); + +/** + * Get the size of a text object. + * + * The size of the text may change when the font or font style and size + * change. + * + * \param text the TTF_Text to query. + * \param w a pointer filled in with the width of the text, in pixels, may be + * NULL. + * \param h a pointer filled in with the height of the text, in pixels, may be + * NULL. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSize(TTF_Text *text, int *w, int *h); + +/** + * Flags for TTF_SubString + * + * \since This datatype is available since SDL_ttf 3.0.0. + * + * \sa TTF_SubString + */ +typedef Uint32 TTF_SubStringFlags; + +#define TTF_SUBSTRING_DIRECTION_MASK 0x000000FF /**< The mask for the flow direction for this substring */ +#define TTF_SUBSTRING_TEXT_START 0x00000100 /**< This substring contains the beginning of the text */ +#define TTF_SUBSTRING_LINE_START 0x00000200 /**< This substring contains the beginning of line `line_index` */ +#define TTF_SUBSTRING_LINE_END 0x00000400 /**< This substring contains the end of line `line_index` */ +#define TTF_SUBSTRING_TEXT_END 0x00000800 /**< This substring contains the end of the text */ + +/** + * The representation of a substring within text. + * + * \since This struct is available since SDL_ttf 3.0.0. + * + * \sa TTF_GetNextTextSubString + * \sa TTF_GetPreviousTextSubString + * \sa TTF_GetTextSubString + * \sa TTF_GetTextSubStringForLine + * \sa TTF_GetTextSubStringForPoint + * \sa TTF_GetTextSubStringsForRange + */ +typedef struct TTF_SubString +{ + TTF_SubStringFlags flags; /**< The flags for this substring */ + int offset; /**< The byte offset from the beginning of the text */ + int length; /**< The byte length starting at the offset */ + int line_index; /**< The index of the line that contains this substring */ + int cluster_index; /**< The internal cluster index, used for quickly iterating */ + SDL_Rect rect; /**< The rectangle, relative to the top left of the text, containing the substring */ +} TTF_SubString; + +/** + * Get the substring of a text object that surrounds a text offset. + * + * If `offset` is less than 0, this will return a zero length substring at the + * beginning of the text with the TTF_SUBSTRING_TEXT_START flag set. If + * `offset` is greater than or equal to the length of the text string, this + * will return a zero length substring at the end of the text with the + * TTF_SUBSTRING_TEXT_END flag set. + * + * \param text the TTF_Text to query. + * \param offset a byte offset into the text string. + * \param substring a pointer filled in with the substring containing the + * offset. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *substring); + +/** + * Get the substring of a text object that contains the given line. + * + * If `line` is less than 0, this will return a zero length substring at the + * beginning of the text with the TTF_SUBSTRING_TEXT_START flag set. If `line` + * is greater than or equal to `text->num_lines` this will return a zero + * length substring at the end of the text with the TTF_SUBSTRING_TEXT_END + * flag set. + * + * \param text the TTF_Text to query. + * \param line a zero-based line index, in the range [0 .. text->num_lines-1]. + * \param substring a pointer filled in with the substring containing the + * offset. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubStringForLine(TTF_Text *text, int line, TTF_SubString *substring); + +/** + * Get the substrings of a text object that contain a range of text. + * + * \param text the TTF_Text to query. + * \param offset a byte offset into the text string. + * \param length the length of the range being queried, in bytes, or -1 for + * the remainder of the string. + * \param count a pointer filled in with the number of substrings returned, + * may be NULL. + * \returns a NULL terminated array of substring pointers or NULL on failure; + * call SDL_GetError() for more information. This is a single + * allocation that should be freed with SDL_free() when it is no + * longer needed. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC TTF_SubString ** SDLCALL TTF_GetTextSubStringsForRange(TTF_Text *text, int offset, int length, int *count); + +/** + * Get the portion of a text string that is closest to a point. + * + * This will return the closest substring of text to the given point. + * + * \param text the TTF_Text to query. + * \param x the x coordinate relative to the left side of the text, may be + * outside the bounds of the text area. + * \param y the y coordinate relative to the top side of the text, may be + * outside the bounds of the text area. + * \param substring a pointer filled in with the closest substring of text to + * the given point. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubStringForPoint(TTF_Text *text, int x, int y, TTF_SubString *substring); + +/** + * Get the previous substring in a text object + * + * If called at the start of the text, this will return a zero length + * substring with the TTF_SUBSTRING_TEXT_START flag set. + * + * \param text the TTF_Text to query. + * \param substring the TTF_SubString to query. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetPreviousTextSubString(TTF_Text *text, const TTF_SubString *substring, TTF_SubString *previous); + +/** + * Get the next substring in a text object + * + * If called at the end of the text, this will return a zero length substring + * with the TTF_SUBSTRING_TEXT_END flag set. + * + * \param text the TTF_Text to query. + * \param substring the TTF_SubString to query. + * \param next a pointer filled in with the next substring. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_GetNextTextSubString(TTF_Text *text, const TTF_SubString *substring, TTF_SubString *next); + +/** + * Update the layout of a text object. + * + * This is automatically done when the layout is requested or the text is + * rendered, but you can call this if you need more control over the timing of + * when the layout and text engine representation are updated. + * + * \param text the TTF_Text to update. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC bool SDLCALL TTF_UpdateText(TTF_Text *text); + +/** + * Destroy a text object created by a text engine. + * + * \param text the text to destroy. + * + * \threadsafety This function should be called on the thread that created the + * text. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_CreateText + */ +extern SDL_DECLSPEC void SDLCALL TTF_DestroyText(TTF_Text *text); + +/** + * Dispose of a previously-created font. + * + * Call this when done with a font. This function will free any resources + * associated with it. It is safe to call this function on NULL, for example + * on the result of a failed call to TTF_OpenFont(). + * + * The font is not valid after being passed to this function. String pointers + * from functions that return information on this font, such as + * TTF_GetFontFamilyName() and TTF_GetFontStyleName(), are no longer valid + * after this call, as well. + * + * \param font the font to dispose of. + * + * \threadsafety This function should not be called while any other thread is + * using the font. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_OpenFont + * \sa TTF_OpenFontIO + */ +extern SDL_DECLSPEC void SDLCALL TTF_CloseFont(TTF_Font *font); + +/** + * Deinitialize SDL_ttf. + * + * You must call this when done with the library, to free internal resources. + * It is safe to call this when the library isn't initialized, as it will just + * return immediately. + * + * Once you have as many quit calls as you have had successful calls to + * TTF_Init, the library will actually deinitialize. + * + * Please note that this does not automatically close any fonts that are still + * open at the time of deinitialization, and it is possibly not safe to close + * them afterwards, as parts of the library will no longer be initialized to + * deal with it. A well-written program should call TTF_CloseFont() on any + * open fonts before calling this function! + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + */ +extern SDL_DECLSPEC void SDLCALL TTF_Quit(void); + +/** + * Check if SDL_ttf is initialized. + * + * This reports the number of times the library has been initialized by a call + * to TTF_Init(), without a paired deinitialization request from TTF_Quit(). + * + * In short: if it's greater than zero, the library is currently initialized + * and ready to work. If zero, it is not initialized. + * + * Despite the return value being a signed integer, this function should not + * return a negative number. + * + * \returns the current number of initialization calls, that need to + * eventually be paired with this many calls to TTF_Quit(). + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL_ttf 3.0.0. + * + * \sa TTF_Init + * \sa TTF_Quit + */ +extern SDL_DECLSPEC int SDLCALL TTF_WasInit(void); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_TTF_H_ */ + diff --git a/vendor/sdl3/ttf/sdl3_textengine.odin b/vendor/sdl3/ttf/sdl3_textengine.odin new file mode 100644 index 000000000..00bf881dd --- /dev/null +++ b/vendor/sdl3/ttf/sdl3_textengine.odin @@ -0,0 +1,62 @@ +package sdl3_ttf + +import "core:c" +import SDL "vendor:sdl3" + +DrawCommand :: enum c.int { + NOOP, + FILL, + COPY, +} + +FillOperation :: struct { + cmd: DrawCommand, + rect: SDL.Rect, +} + +CopyOperation :: struct { + cmd: DrawCommand, + text_offset: c.int, + glyph_font: ^Font, + glyph_index: u32, + src: SDL.Rect, + dst: SDL.Rect, + reserved: rawptr, +} + +DrawOperation :: struct #raw_union { + cmd: DrawCommand, + fill: FillOperation, + copy: CopyOperation, +} + +TextLayout :: struct {} + +TextData :: struct { + font: ^Font, + color: SDL.FColor, + needs_layout_update: bool, + layout: ^TextLayout, + x, y: c.int, + w, h: c.int, + num_ops: c.int, + ops: [^]DrawOperation `fmt:"v,num_ops"`, + num_clusters: c.int, + clusters: [^]SubString `fmt:"v,num_clusters"`, + props: SDL.PropertiesID, + needs_engine_update: bool, + engine: ^TextEngine, + engine_text: rawptr, +} + +TextEngine :: struct { + version: u32, + userdata: rawptr, + CreateText: proc "c" (userdata: rawptr, text: ^Text) -> bool, + DestroyText: proc "c" (userdata: rawptr, Textext: ^Text), +} + +#assert( + (size_of(TextEngine) == 16 && size_of(rawptr) == 4) || + (size_of(TextEngine) == 32 && size_of(rawptr) == 8), +) diff --git a/vendor/sdl3/ttf/sdl3_ttf.odin b/vendor/sdl3/ttf/sdl3_ttf.odin new file mode 100644 index 000000000..9b46143e8 --- /dev/null +++ b/vendor/sdl3/ttf/sdl3_ttf.odin @@ -0,0 +1,279 @@ +package sdl3_ttf + +import "core:c" +import SDL "vendor:sdl3" + +when ODIN_OS == .Windows { + foreign import lib "SDL3_ttf.lib" +} else { + foreign import lib "system:SDL3_ttf" +} + + +PROP_FONT_CREATE_FILENAME_STRING :: "SDL_ttf.font.create.filename" +PROP_FONT_CREATE_IOSTREAM_POINTER :: "SDL_ttf.font.create.iostream" +PROP_FONT_CREATE_IOSTREAM_OFFSET_NUMBER :: "SDL_ttf.font.create.iostream.offset" +PROP_FONT_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN :: "SDL_ttf.font.create.iostream.autoclose" +PROP_FONT_CREATE_SIZE_FLOAT :: "SDL_ttf.font.create.size" +PROP_FONT_CREATE_FACE_NUMBER :: "SDL_ttf.font.create.face" +PROP_FONT_CREATE_HORIZONTAL_DPI_NUMBER :: "SDL_ttf.font.create.hdpi" +PROP_FONT_CREATE_VERTICAL_DPI_NUMBER :: "SDL_ttf.font.create.vdpi" +PROP_FONT_CREATE_EXISTING_FONT :: "SDL_ttf.font.create.existing_font" + +FONT_WEIGHT_THIN :: 100 /**< Thin (100) named font weight value */ +FONT_WEIGHT_EXTRA_LIGHT :: 200 /**< ExtraLight (200) named font weight value */ +FONT_WEIGHT_LIGHT :: 300 /**< Light (300) named font weight value */ +FONT_WEIGHT_NORMAL :: 400 /**< Normal (400) named font weight value */ +FONT_WEIGHT_MEDIUM :: 500 /**< Medium (500) named font weight value */ +FONT_WEIGHT_SEMI_BOLD :: 600 /**< SemiBold (600) named font weight value */ +FONT_WEIGHT_BOLD :: 700 /**< Bold (700) named font weight value */ +FONT_WEIGHT_EXTRA_BOLD :: 800 /**< ExtraBold (800) named font weight value */ +FONT_WEIGHT_BLACK :: 900 /**< Black (900) named font weight value */ +FONT_WEIGHT_EXTRA_BLACK :: 950 /**< ExtraBlack (950) named font weight value */ + +PROP_RENDERER_TEXT_ENGINE_RENDERER :: "SDL_ttf.renderer_text_engine.create.renderer" +PROP_RENDERER_TEXT_ENGINE_ATLAS_TEXTURE_SIZE :: "SDL_ttf.renderer_text_engine.create.atlas_texture_size" + +PROP_GPU_TEXT_ENGINE_DEVICE :: "SDL_ttf.gpu_text_engine.create.device" +PROP_GPU_TEXT_ENGINE_ATLAS_TEXTURE_SIZE :: "SDL_ttf.gpu_text_engine.create.atlas_texture_size" + +MAJOR_VERSION :: 3 +MINOR_VERSION :: 2 +PATCHLEVEL :: 2 + +Font :: struct {} + +Text :: struct { + text: [^]u8, + num_lines: c.int, + refcount: c.int, + internal: ^TextData, +} + +FontStyle :: enum u32 { + NORMAL, + BOLD, + ITALIC, + UNDERLINE, + STRIKETHROUGH, +} + +FontStyleFlags :: distinct bit_set[FontStyle; u32] + +// NOTE: This is called TTF_HintingFlags but its not a bit_set so +// the "flags" doesnt really make sense, its just the hinting. +Hinting :: enum c.int { + INVALID = -1, + NORMAL, + LIGHT, + MONO, + NONE, + LIGHT_SUBPIXEL, +} + +HorizontalAlignment :: enum c.int { + INVALID = -1, + LEFT, + CENTER, + RIGHT, +} + +Direction :: enum c.int { + INVALID, + LTR = 4, + RTL, + TTB, + BTT, +} + +ImageType :: enum c.int { + INVALID, + ALPHA, + COLOR, + SDF, +} + +GPUAtlasDrawSequence :: struct { + atlas_texture: ^SDL.GPUTexture, + xy, uv: [^]SDL.FPoint `fmt:"v,num_vertices"`, + num_vertices: c.int, + indices: [^]c.int `fmt:"v,num_indices"`, + num_indices: c.int, + image_type: ImageType, + next: ^GPUAtlasDrawSequence, +} + +GPUTextEngineWinding :: enum c.int { + INVALID = -1, + CLOCKWISE = 0, + COUNTER_CLOCKWISE = +1, +} + +SubStringFlags :: bit_field u32 { + direction: u8 | 8, + text_start: bool | 1, + line_start: bool | 1, + line_end: bool | 1, + text_end: bool | 1, +} + +SubString :: struct { + flags: SubStringFlags, + offset, length: c.int, + line_index, cluster_index: c.int, + rect: SDL.Rect, +} + +@(default_calling_convention="c", link_prefix="TTF_", require_results) +foreign lib { + Version :: proc() -> c.int --- + GetFreeTypeVersion :: proc(major, minor, patch: ^c.int) --- + GetHarfBuzzVersion :: proc(major, minor, patch: ^c.int) --- + + Init :: proc() -> bool --- + + OpenFont :: proc(file: cstring, ptsize: f32) -> ^Font --- + OpenFontIO :: proc(src: ^SDL.IOStream, closeio: bool, ptsize: f32) -> ^Font --- + OpenFontWithProperties :: proc(props: SDL.PropertiesID) -> ^Font --- + + CopyFont :: proc(existing_font: ^Font) -> ^Font --- + + GetFontProperties :: proc(font: ^Font) -> SDL.PropertiesID --- + GetFontGeneration :: proc(font: ^Font) -> u32 --- + + AddFallbackFont :: proc(font: ^Font, fallback: ^Font) -> bool --- + RemoveFallbackFont :: proc(font: ^Font, fallback: ^Font) --- + ClearFallbackFonts :: proc(font: ^Font) --- + + SetFontSize :: proc(font: ^Font, ptsize: f32) -> bool --- + SetFontSizeDPI :: proc(font: ^Font, ptsize: f32, hdpi: c.int, vdpi: c.int) -> bool --- + GetFontSize :: proc(font: ^Font) -> f32 --- + GetFontDPI :: proc(font: ^Font, hdpi: ^c.int, vdpi: ^c.int) -> bool --- + + SetFontStyle :: proc(font: ^Font, style: FontStyleFlags) --- + GetFontStyle :: proc(#by_ptr font: Font) -> FontStyleFlags --- + + SetFontOutline :: proc(font: ^Font, outline: c.int) -> bool --- + GetFontOutline :: proc(#by_ptr font: Font) -> c.int --- + + SetFontHinting :: proc(font: ^Font, hinting: Hinting) --- + GetFontHinting :: proc(#by_ptr font: Font) -> Hinting --- + + GetNumFontFaces :: proc(font: ^Font) -> c.int --- + + SetFontSDF :: proc(font: ^Font, enabled: bool) -> bool --- + GetFontSDF :: proc(#by_ptr font: Font) -> bool --- + + GetFontWeight :: proc(#by_ptr font: Font) -> c.int --- + + SetFontWrapAlignment :: proc(font: ^Font, align: HorizontalAlignment) --- + GetFontWrapAlignment :: proc(#by_ptr font: Font) -> HorizontalAlignment --- + + GetFontHeight :: proc(#by_ptr font: Font) -> c.int --- + GetFontAscent :: proc(#by_ptr font: Font) -> c.int --- + GetFontDescent :: proc(#by_ptr font: Font) -> c.int --- + + SetFontLineSkip :: proc(font: ^Font, lineskip: c.int) --- + GetFontLineSkip :: proc(#by_ptr font: Font) -> c.int --- + + SetFontKerning :: proc(font: ^Font, enabled: bool) --- + GetFontKerning :: proc(#by_ptr font: Font) -> bool --- + + FontIsFixedWidth :: proc(#by_ptr font: Font) -> bool --- + FontIsScalable :: proc(#by_ptr font: Font) -> bool --- + + GetFontFamilyName :: proc(#by_ptr font: Font) -> cstring --- + GetFontStyleName :: proc(#by_ptr font: Font) -> cstring --- + + SetFontDirection :: proc(font: ^Font, direction: Direction) -> bool --- + GetFontDirection :: proc(#by_ptr font: Font) -> Direction --- + + StringToTag :: proc(string: cstring) -> u32 --- + TagToString :: proc(tag: u32, string: [^]c.char, size: c.size_t) --- + + SetFontScript :: proc(font: ^Font, script: u32) -> bool --- + GetFontScript :: proc(font: ^Font) -> u32 --- + + SetFontLanguage :: proc(font: ^Font, language_bcp47: cstring) -> bool --- + + GetGlyphScript :: proc(ch: u32) -> u32 --- + FontHasGlyph :: proc(font: ^Font, ch: u32) -> bool --- + GetGlyphImage :: proc(font: ^Font, ch: u32, image_type: ^ImageType) -> ^SDL.Surface --- + GetGlyphImageForIndex :: proc(font: ^Font, glyph_index: u32, image_type: ^ImageType) -> ^SDL.Surface --- + GetGlyphMetrics :: proc(font: ^Font, ch: u32, minx, maxx, miny, maxy, advance: ^c.int) -> bool --- + GetGlyphKerning :: proc(font: ^Font, previous_ch: u32, ch: u32, kerning: ^c.int) -> bool --- + + GetStringSize :: proc(font: ^Font, text: cstring, length: c.size_t, w, h: ^c.int) -> bool --- + GetStringSizeWrapped :: proc(font: ^Font, text: cstring, length: c.size_t, wrap_width: c.int, w, h: ^c.int) -> bool --- + MeasureString :: proc(font: ^Font, text: cstring, length: c.size_t, max_width: c.int, measured_width: ^c.int, measured_length: ^c.size_t) -> bool --- + + RenderText_Solid :: proc(font: ^Font, text: cstring, length: c.size_t, fg: SDL.Color) -> ^SDL.Surface --- + RenderText_Solid_Wrapped :: proc(font: ^Font, text: cstring, length: c.size_t, fg: SDL.Color, wrap_Length: c.int) -> ^SDL.Surface --- + RenderGylph_Solid :: proc(font: ^Font, ch: u32, fg: SDL.Color) -> ^SDL.Surface --- + RenderText_Shaded :: proc(font: ^Font, text: cstring, length: c.size_t, fg, bg: SDL.Color) -> ^SDL.Surface --- + RenderText_Shaded_Wrapped :: proc(font: ^Font, text: cstring, length: c.size_t, fg, bg: SDL.Color, wrap_width: c.int) -> ^SDL.Surface --- + RenderGlyph_Shaded :: proc(font: ^Font, ch: u32, fg, bg: SDL.Color) -> ^SDL.Surface --- + RenderText_Blended :: proc(font: ^Font, text: cstring, length: c.size_t, fg: SDL.Color) -> ^SDL.Surface --- + RnederText_Blended_Wrapped :: proc(font: ^Font, text: cstring, length: c.size_t, fg: SDL.Color, wrap_width: c.int) -> ^SDL.Surface --- + RenderGlyph_Blended :: proc(font: ^Font, ch: u32, fg: SDL.Color) -> ^SDL.Surface --- + RenderText_LCD :: proc(font: ^Font, text: cstring, length: c.size_t, fg, bg: SDL.Color) -> ^SDL.Surface --- + RenderText_LCD_Wrapped :: proc(font: ^Font, text: cstring, length: c.size_t, fg, bg: SDL.Color, wrap_width: c.int) -> ^SDL.Surface --- + RenderGlyph_LCD :: proc(font: ^Font, ch: u32, fg, bg: SDL.Color) -> ^SDL.Surface --- + + CreateSurfaceTextEngine :: proc() -> ^TextEngine --- + DrawSurfaceText :: proc(text: ^Text, x, y: c.int, surface: ^SDL.Surface) -> bool --- + DestroySurfaceTextEngine :: proc(engine: ^TextEngine) --- + + CreateRendererTextEngine :: proc(renderer: ^SDL.Renderer) -> ^TextEngine --- + CreateRendererTextEngineWithProperties :: proc(props: SDL.PropertiesID) -> ^TextEngine --- + DrawRendererText :: proc(text: ^Text, x, y: f32) -> bool --- + DestroyRendererTextEngine :: proc(engine: ^TextEngine) --- + + CreateGPUTextEngine :: proc(device: ^SDL.GPUDevice) -> ^TextEngine --- + CreateGPUTextEngineWithProperties :: proc(props: SDL.PropertiesID) -> ^TextEngine --- + GetGPUTextDrawData :: proc(text: ^Text) -> ^GPUAtlasDrawSequence --- + DestroyGPUTextEngine :: proc(engine: ^TextEngine) --- + SetGPUTextEngineWinding :: proc(engine: ^TextEngine, winding: GPUTextEngineWinding) --- + GetGPUTextEngineWinding :: proc(#by_ptr engine: TextEngine) -> GPUTextEngineWinding --- + + CreateText :: proc(engine: ^TextEngine, font: ^Font, text: cstring, length: c.size_t) -> ^Text --- + GetTextProperties :: proc(text: ^Text) -> SDL.PropertiesID --- + SetTextEngine :: proc(text: ^Text, engine: ^TextEngine) -> bool --- + GetTextEngine :: proc(text: ^Text) -> ^TextEngine --- + SetTextFont :: proc(text: ^Text, font: ^Font) -> bool --- + GetTextFont :: proc(text: ^Text) -> ^Font --- + SetTextDirection :: proc(text: ^Text, direction: Direction) -> bool --- + GetTextDirection :: proc(text: ^Text) -> Direction --- + SetTextScript :: proc(text: ^Text, script: u32) -> bool --- + GetTextScript :: proc(text: ^Text) -> u32 --- + SetTextColor :: proc(text: ^Text, r, g, b, a: u8) -> bool --- + SetTextColorFloat :: proc(text: ^Text, r, g, b, a: f32) -> bool --- + GetTextColor :: proc(text: ^Text, r, g, b, a: ^u8) -> bool --- + GetTextColorFloat :: proc(text: ^Text, r, g, b, a: ^f32) -> bool --- + SetTextPosition :: proc(text: ^Text, x, y: c.int) -> bool --- + GetTextPosition :: proc(text: ^Text, x, y: ^c.int) -> bool --- + SetTextWrapWidth :: proc(text: ^Text, wrap_width: c.int) -> bool --- + GetTextWrapWidth :: proc(text: ^Text, wrap_width: ^c.int) -> bool --- + SetTextWrapWhitespaceVisible :: proc(text: ^Text, visible: bool) -> bool --- + TextWrapWhitespaceVisible :: proc(text: ^Text) -> bool --- + + SetTextString :: proc(text: ^Text, string: cstring, length: c.size_t) -> bool --- + InsertTextString :: proc(text: ^Text, offset: c.int, string: cstring, length: c.size_t) -> bool --- + AppendTextString :: proc(text: ^Text, string: cstring, length: c.size_t) -> bool --- + DeleteTextString :: proc(text: ^Text, offset, length: c.int) -> bool --- + + GetTextSize :: proc(text: ^Text, w, h: ^c.int) -> bool --- + + GetTextSubString :: proc(text: ^Text, offset: c.int, substring: ^SubString) -> bool --- + GetTextSubStringForLine :: proc(text: ^Text, line: c.int, substring: ^SubString) -> bool --- + GetTextSubStringsForRange :: proc(text: ^Text, offset, length: c.int, count: ^c.int) -> [^]^SubString --- + GetTextSubStringForPoint :: proc(text: ^Text, x, y: c.int, substring: ^SubString) -> bool --- + GetPreviousTextSubString :: proc(text: ^Text, #by_ptr substring: SubString, previous: ^SubString) -> bool --- + GetNextTextSubString :: proc(text: ^Text, #by_ptr substring: SubString, next: ^SubString) -> bool --- + + UpdateText :: proc(text: ^Text) -> bool --- + DestroyText :: proc(text: ^Text) --- + CloseFont :: proc(font: ^Font) --- + Quit :: proc() --- + WasInit :: proc() -> c.int --- +} diff --git a/vendor/wasm/WebGL/webgl.odin b/vendor/wasm/WebGL/webgl.odin index 61e0efd58..96d363ba2 100644 --- a/vendor/wasm/WebGL/webgl.odin +++ b/vendor/wasm/WebGL/webgl.odin @@ -61,10 +61,10 @@ foreign webgl { BufferData :: proc(target: Enum, size: int, data: rawptr, usage: Enum) --- BufferSubData :: proc(target: Enum, offset: uintptr, size: int, data: rawptr) --- - Clear :: proc(bits: Enum) --- + Clear :: proc(bits: u32) --- ClearColor :: proc(r, g, b, a: f32) --- - ClearDepth :: proc(x: Enum) --- - ClearStencil :: proc(x: Enum) --- + ClearDepth :: proc(x: f32) --- + ClearStencil :: proc(x: i32) --- ColorMask :: proc(r, g, b, a: bool) --- CompileShader :: proc(shader: Shader) --- diff --git a/vendor/wgpu/wgpu.js b/vendor/wgpu/wgpu.js index f3d29eafd..578c1aefc 100644 --- a/vendor/wgpu/wgpu.js +++ b/vendor/wgpu/wgpu.js @@ -340,8 +340,8 @@ class WebGPUInterface { return STATUS_SUCCESS; } - - genericAdapterInfo(infoPtr) { + + genericGetAdapterInfo(infoPtr) { this.assert(infoPtr != 0); const off = this.struct(infoPtr); diff --git a/vendor/windows/XAudio2/x3daudio.odin b/vendor/windows/XAudio2/x3daudio.odin new file mode 100644 index 000000000..27c4dc9fa --- /dev/null +++ b/vendor/windows/XAudio2/x3daudio.odin @@ -0,0 +1,233 @@ +#+build windows + +/* NOTES: + 1. Definition of terms: + LFE: Low Frequency Effect -- always omnidirectional. + LPF: Low Pass Filter, divided into two classifications: + Direct -- Applied to the direct signal path, + used for obstruction/occlusion effects. + Reverb -- Applied to the reverb signal path, + used for occlusion effects only. + + 2. Volume level is expressed as a linear amplitude scaler: + 1.0f represents no attenuation applied to the original signal, + 0.5f denotes an attenuation of 6dB, and 0.0f results in silence. + Amplification (volume > 1.0f) is also allowed, and is not clamped. + + LPF values range from 1.0f representing all frequencies pass through, + to 0.0f which results in silence as all frequencies are filtered out. + + 3. X3DAudio uses a left-handed Cartesian coordinate system with values + on the x-axis increasing from left to right, on the y-axis from + bottom to top, and on the z-axis from near to far. + Azimuths are measured clockwise from a given reference direction. + + Distance measurement is with respect to user-defined world units. + Applications may provide coordinates using any system of measure + as all non-normalized calculations are scale invariant, with such + operations natively occurring in user-defined world unit space. + Metric constants are supplied only as a convenience. + Distance is calculated using the Euclidean norm formula. + + 4. Only real values are permissible with functions using 32-bit + float parameters -- NAN and infinite values are not accepted. + All computation occurs in 32-bit precision mode. */ + + +package windows_xaudio2 + +import "core:math" + +foreign import xa2 "system:xaudio2.lib" + +//---------------------------------------------------// +// speaker geometry configuration flags, specifies assignment of channels to speaker positions, defined as per WAVEFORMATEXTENSIBLE.dwChannelMask +SPEAKER_FLAGS :: distinct bit_set[SPEAKER_FLAG; u32] +SPEAKER_FLAG :: enum u32 { + FRONT_LEFT = 0, + FRONT_RIGHT = 1, + FRONT_CENTER = 2, + LOW_FREQUENCY = 3, + BACK_LEFT = 4, + BACK_RIGHT = 5, + FRONT_LEFT_OF_CENTER = 6, + FRONT_RIGHT_OF_CENTER = 7, + BACK_CENTER = 8, + SIDE_LEFT = 9, + SIDE_RIGHT = 10, + TOP_CENTER = 11, + TOP_FRONT_LEFT = 12, + TOP_FRONT_CENTER = 13, + TOP_FRONT_RIGHT = 14, + TOP_BACK_LEFT = 15, + TOP_BACK_CENTER = 16, + TOP_BACK_RIGHT = 17, + //RESERVED = 0x7FFC0000, // bit mask locations reserved for future use + ALL = 31, // used to specify that any possible permutation of speaker configurations +} + +// standard speaker geometry configurations, used with Initialize +SPEAKER_MONO :: SPEAKER_FLAGS{.FRONT_CENTER} +SPEAKER_STEREO :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT} +SPEAKER_2POINT1 :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .LOW_FREQUENCY} +SPEAKER_SURROUND :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .FRONT_CENTER, .BACK_CENTER} +SPEAKER_QUAD :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .BACK_LEFT, .BACK_RIGHT} +SPEAKER_4POINT1 :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .LOW_FREQUENCY, .BACK_LEFT, .BACK_RIGHT} +SPEAKER_5POINT1 :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .FRONT_CENTER, .LOW_FREQUENCY, .BACK_LEFT, .BACK_RIGHT} +SPEAKER_7POINT1 :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .FRONT_CENTER, .LOW_FREQUENCY, .BACK_LEFT, .BACK_RIGHT, .FRONT_LEFT_OF_CENTER, .FRONT_RIGHT_OF_CENTER} +SPEAKER_5POINT1_SURROUND :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .FRONT_CENTER, .LOW_FREQUENCY, .SIDE_LEFT, .SIDE_RIGHT} +SPEAKER_7POINT1_SURROUND :: SPEAKER_FLAGS{.FRONT_LEFT, .FRONT_RIGHT, .FRONT_CENTER, .LOW_FREQUENCY, .BACK_LEFT, .BACK_RIGHT, .SIDE_LEFT, .SIDE_RIGHT} + +// size of instance handle in bytes +HANDLE_BYTESIZE :: 20 + +// speed of sound in meters per second for dry air at approximately 20C, used with Initialize +SPEED_OF_SOUND :: 343.5 + +// calculation control flags, used with Calculate +CALCULATE_FLAGS :: distinct bit_set[CALCULATE_FLAG; u32] +CALCULATE_FLAG :: enum u32 { + MATRIX = 0, // enable matrix coefficient table calculation + DELAY = 1, // enable delay time array calculation (stereo final mix only) + LPF_DIRECT = 2, // enable LPF direct-path coefficient calculation + LPF_REVERB = 3, // enable LPF reverb-path coefficient calculation + REVERB = 4, // enable reverb send level calculation + DOPPLER = 5, // enable doppler shift factor calculation + EMITTER_ANGLE = 6, // enable emitter-to-listener interior angle calculation + + ZEROCENTER = 16, // do not position to front center speaker, signal positioned to remaining speakers instead, front center destination channel will be zero in returned matrix coefficient table, valid only for matrix calculations with final mix formats that have a front center channel + REDIRECT_TO_LFE = 17, // apply equal mix of all source channels to LFE destination channel, valid only for matrix calculations with sources that have no LFE channel and final mix formats that have an LFE channel +} + +//-----------------------------------------------------// +VECTOR :: [3]f32 // float 3D vector + +// instance handle of precalculated constants +HANDLE :: distinct [HANDLE_BYTESIZE]byte + +// Distance curve point: +// Defines a DSP setting at a given normalized distance. +DISTANCE_CURVE_POINT :: struct #packed { + Distance: f32, // normalized distance, must be within [0.0f, 1.0f] + DSPSetting: f32, // DSP setting +} + +// Distance curve: +// A piecewise curve made up of linear segments used to define DSP behaviour with respect to normalized distance. +// +// Note that curve point distances are normalized within [0.0f, 1.0f]. +// EMITTER.CurveDistanceScaler must be used to scale the normalized distances to user-defined world units. +// For distances beyond CurveDistanceScaler * 1.0f, pPoints[PointCount-1].DSPSetting is used as the DSP setting. +// +// All distance curve spans must be such that: +// pPoints[k-1].DSPSetting + ((pPoints[k].DSPSetting-pPoints[k-1].DSPSetting) / (pPoints[k].Distance-pPoints[k-1].Distance)) * (pPoints[k].Distance-pPoints[k-1].Distance) != NAN or infinite values +// For all points in the distance curve where 1 <= k < PointCount. +DISTANCE_CURVE :: struct #packed { + pPoints: [^]DISTANCE_CURVE_POINT `fmt:"v,PointCount"`, // distance curve point array, must have at least PointCount elements with no duplicates and be sorted in ascending order with respect to Distance + PointCount: u32, // number of distance curve points, must be >= 2 as all distance curves must have at least two endpoints, defining DSP settings at 0.0f and 1.0f normalized distance +} +Default_LinearCurvePoints := [2]DISTANCE_CURVE_POINT{{0.0, 1.0}, {1.0, 0.0}} +Default_LinearCurve := DISTANCE_CURVE{&Default_LinearCurvePoints[0], 2} + +CONE :: struct #packed { + InnerAngle: f32, // inner cone angle in radians, must be within [0.0f, TAU] + OuterAngle: f32, // outer cone angle in radians, must be within [InnerAngle, TAU] + + InnerVolume: f32, // volume level scaler on/within inner cone, used only for matrix calculations, must be within [0.0f, 2.0f] when used + OuterVolume: f32, // volume level scaler on/beyond outer cone, used only for matrix calculations, must be within [0.0f, 2.0f] when used + InnerLPF: f32, // LPF (both direct and reverb paths) coefficient subtrahend on/within inner cone, used only for LPF (both direct and reverb paths) calculations, must be within [0.0f, 1.0f] when used + OuterLPF: f32, // LPF (both direct and reverb paths) coefficient subtrahend on/beyond outer cone, used only for LPF (both direct and reverb paths) calculations, must be within [0.0f, 1.0f] when used + InnerReverb: f32, // reverb send level scaler on/within inner cone, used only for reverb calculations, must be within [0.0f, 2.0f] when used + OuterReverb: f32, // reverb send level scaler on/beyond outer cone, used only for reverb calculations, must be within [0.0f, 2.0f] when used +} +Default_DirectionalCone := CONE{math.PI / 2, math.PI, 1.0, 0.708, 0.0, 0.25, 0.708, 1.0} + +// Listener: +// Defines a point of 3D audio reception. +// +// The cone is directed by the listener's front orientation. +LISTENER :: struct #packed { + OrientFront: VECTOR, // orientation of front direction, used only for matrix and delay calculations or listeners with cones for matrix, LPF (both direct and reverb paths), and reverb calculations, must be normalized when used + OrientTop: VECTOR, // orientation of top direction, used only for matrix and delay calculations, must be orthonormal with OrientFront when used + + Position: VECTOR, // position in user-defined world units, does not affect Velocity + Velocity: VECTOR, // velocity vector in user-defined world units/second, used only for doppler calculations, does not affect Position + + pCone: ^CONE, // sound cone, used only for matrix, LPF (both direct and reverb paths), and reverb calculations, NULL specifies omnidirectionality +} + +// Emitter: +// Defines a 3D audio source, divided into two classifications: +// +// Single-point -- For use with single-channel sounds. +// Positioned at the emitter base, i.e. the channel radius and azimuth are ignored if the number of channels == 1. +// +// May be omnidirectional or directional using a cone. +// The cone originates from the emitter base position, and is directed by the emitter's front orientation. +// +// Multi-point -- For use with multi-channel sounds. +// Each non-LFE channel is positioned using an azimuth along the channel radius with respect to the front orientation vector in the plane orthogonal to the top orientation vector. +// An azimuth of TAU specifies a channel is an LFE. Such channels are positioned at the emitter base and are calculated with respect to pLFECurve only, never pVolumeCurve. +// +// Multi-point emitters are always omnidirectional, i.e. the cone is ignored if the number of channels > 1. +// +// Note that many properties are shared among all channel points, locking certain behaviour with respect to the emitter base position. +// For example, doppler shift is always calculated with respect to the emitter base position and so is constant for all its channel points. +// Distance curve calculations are also with respect to the emitter base position, with the curves being calculated independently of each other. +// For instance, volume and LFE calculations do not affect one another. +EMITTER :: struct #packed { + pCone: ^CONE, // sound cone, used only with single-channel emitters for matrix, LPF (both direct and reverb paths), and reverb calculations, NULL specifies omnidirectionality + + OrientFront: VECTOR, // orientation of front direction, used only for emitter angle calculations or with multi-channel emitters for matrix calculations or single-channel emitters with cones for matrix, LPF (both direct and reverb paths), and reverb calculations, must be normalized when used + OrientTop: VECTOR, // orientation of top direction, used only with multi-channel emitters for matrix calculations, must be orthonormal with OrientFront when used + + Position: VECTOR, // position in user-defined world units, does not affect Velocity + Velocity: VECTOR, // velocity vector in user-defined world units/second, used only for doppler calculations, does not affect Position + + InnerRadius: f32, // inner radius, must be within [0.0f, max(f32)] + InnerRadiusAngle: f32, // inner radius angle, must be within [0.0f, PI/4.0) + + ChannelCount: u32, // number of sound channels, must be > 0 + ChannelRadius: f32, // channel radius, used only with multi-channel emitters for matrix calculations, must be >= 0.0f when used + pChannelAzimuths: [^]f32 `fmt:"v,ChannelCount"`, // channel azimuth array, used only with multi-channel emitters for matrix calculations, contains positions of each channel expressed in radians along the channel radius with respect to the front orientation vector in the plane orthogonal to the top orientation vector, or TAU to specify an LFE channel, must have at least ChannelCount elements, all within [0.0f, TAU] when used + + pVolumeCurve: ^DISTANCE_CURVE, // volume level distance curve, used only for matrix calculations, NULL specifies a default curve that conforms to the inverse square law, calculated in user-defined world units with distances <= CurveDistanceScaler clamped to no attenuation + pLFECurve: ^DISTANCE_CURVE, // LFE level distance curve, used only for matrix calculations, NULL specifies a default curve that conforms to the inverse square law, calculated in user-defined world units with distances <= CurveDistanceScaler clamped to no attenuation + pLPFDirectCurve: ^DISTANCE_CURVE, // LPF direct-path coefficient distance curve, used only for LPF direct-path calculations, NULL specifies the default curve: [0.0f,1.0f], [1.0f,0.75f] + pLPFReverbCurve: ^DISTANCE_CURVE, // LPF reverb-path coefficient distance curve, used only for LPF reverb-path calculations, NULL specifies the default curve: [0.0f,0.75f], [1.0f,0.75f] + pReverbCurve: ^DISTANCE_CURVE, // reverb send level distance curve, used only for reverb calculations, NULL specifies the default curve: [0.0f,1.0f], [1.0f,0.0f] + + CurveDistanceScaler: f32, // curve distance scaler, used to scale normalized distance curves to user-defined world units and/or exaggerate their effect, used only for matrix, LPF (both direct and reverb paths), and reverb calculations, must be within [min(f32), max(f32)] when used + DopplerScaler: f32, // doppler shift scaler, used to exaggerate doppler shift effect, used only for doppler calculations, must be within [0.0f, max(f32)] when used +} + +// DSP settings: +// Receives results from a call to Calculate to be sent to the low-level audio rendering API for 3D signal processing. +// +// The user is responsible for allocating the matrix coefficient table, delay time array, and initializing the channel counts when used. +DSP_SETTINGS :: struct #packed { + pMatrixCoefficients: [^]f32, // [inout] matrix coefficient table, receives an array representing the volume level used to send from source channel S to destination channel D, stored as pMatrixCoefficients[SrcChannelCount * D + S], must have at least SrcChannelCount*DstChannelCount elements + pDelayTimes: [^]f32, // [inout] delay time array, receives delays for each destination channel in milliseconds, must have at least DstChannelCount elements (stereo final mix only) + SrcChannelCount: u32, // [in] number of source channels, must equal number of channels in respective emitter + DstChannelCount: u32, // [in] number of destination channels, must equal number of channels of the final mix + + LPFDirectCoefficient: f32, // [out] LPF direct-path coefficient + LPFReverbCoefficient: f32, // [out] LPF reverb-path coefficient + ReverbLevel: f32, // [out] reverb send level + DopplerFactor: f32, // [out] doppler shift factor, scales resampler ratio for doppler shift effect, where the effective frequency = DopplerFactor * original frequency + EmitterToListenerAngle: f32, // [out] emitter-to-listener interior angle, expressed in radians with respect to the emitter's front orientation + + EmitterToListenerDistance: f32, // [out] distance in user-defined world units from the emitter base to listener position, always calculated + EmitterVelocityComponent: f32, // [out] component of emitter velocity vector projected onto emitter->listener vector in user-defined world units/second, calculated only for doppler + ListenerVelocityComponent: f32, // [out] component of listener velocity vector projected onto emitter->listener vector in user-defined world units/second, calculated only for doppler +} + +//-------------------------------------------------------// +@(default_calling_convention="cdecl", link_prefix="X3DAudio") +foreign xa2 { + // initializes instance handle + Initialize :: proc(SpeakerChannelMask: SPEAKER_FLAGS, SpeedOfSound: f32, Instance: HANDLE) -> HRESULT --- + + // calculates DSP settings with respect to 3D parameters + Calculate :: proc(Instance: HANDLE, #by_ptr pListener: LISTENER, #by_ptr pEmitter: EMITTER, Flags: CALCULATE_FLAGS, pDSPSettings: ^DSP_SETTINGS) --- +} diff --git a/vendor/windows/XAudio2/xapo.odin b/vendor/windows/XAudio2/xapo.odin new file mode 100644 index 000000000..54fb420ca --- /dev/null +++ b/vendor/windows/XAudio2/xapo.odin @@ -0,0 +1,377 @@ +#+build windows + +/* NOTES: + 1. Definition of terms: + DSP: Digital Signal Processing. + + CBR: Constant BitRate -- DSP that consumes a constant number of + input samples to produce an output sample. + For example, a 22kHz to 44kHz resampler is CBR DSP. + Even though the number of input to output samples differ, + the ratio between input to output rate remains constant. + All user-defined XAPOs are assumed to be CBR as + XAudio2 only allows CBR DSP to be added to an effect chain. + + XAPO: Cross-platform Audio Processing Object -- + a thin wrapper that manages DSP code, allowing it + to be easily plugged into an XAudio2 effect chain. + + Frame: A block of samples, one per channel, + to be played simultaneously. + E.g. a mono stream has one sample per frame. + + In-Place: Processing such that the input buffer equals the + output buffer (i.e. input data modified directly). + This form of processing is generally more efficient + than using separate memory for input and output. + However, an XAPO may not perform format conversion + when processing in-place. + + 2. XAPO member variables are divided into three classifications: + Immutable: Set once via IXAPO.Initialize and remain + constant during the lifespan of the XAPO. + + Locked: May change before the XAPO is locked via + IXAPO.LockForProcess but remain constant + until IXAPO.UnlockForProcess is called. + + Dynamic: May change from one processing pass to the next, + usually via IXAPOParameters.SetParameters. + XAPOs should assign reasonable defaults to their dynamic + variables during IXAPO.Initialize/LockForProcess so + that calling IXAPOParameters.SetParameters is not + required before processing begins. + + When implementing an XAPO, determine the type of each variable and + initialize them in the appropriate method. Immutable variables are + generally preferable over locked which are preferable over dynamic. + That is, one should strive to minimize XAPO state changes for + best performance, maintainability, and ease of use. + + 3. To minimize glitches, the realtime audio processing thread must + not block. XAPO methods called by the realtime thread are commented + as non-blocking and therefore should not use blocking synchronization, + allocate memory, access the disk, etc. The XAPO interfaces were + designed to allow an effect implementer to move such operations + into other methods called on an application controlled thread. + + 4. Extending functionality is accomplished through the addition of new + COM interfaces. For example, if a new member is added to a parameter + structure, a new interface using the new structure should be added, + leaving the original interface unchanged. + This ensures consistent communication between future versions of + XAudio2 and various versions of XAPOs that may exist in an application. + + 5. All audio data is interleaved in XAudio2. + The default audio format for an effect chain is WAVE_FORMAT_IEEE_FLOAT. + + 6. User-defined XAPOs should assume all input and output buffers are + 16-byte aligned. + + 7. See XAPOBase.odin for an XAPO base class which provides a default + implementation for most of the interface methods defined below. */ + +package windows_xaudio2 + +import win "core:sys/windows" + +//---------------------------------------------------// + +// XAPO error codes +FORMAT_UNSUPPORTED := win.MAKE_HRESULT(win.SEVERITY.ERROR, 0x897, 0x01) // requested audio format unsupported + +// supported number of channels (samples per frame) range +XAPO_MIN_CHANNELS :: 1 +XAPO_MAX_CHANNELS :: 64 + +// supported framerate range +XAPO_MIN_FRAMERATE :: 1000 +XAPO_MAX_FRAMERATE :: 200000 + +// unicode string length, including terminator, used with XAPO_REGISTRATION_PROPERTIES +XAPO_REGISTRATION_STRING_LENGTH :: 256 + + +// XAPO property flags, used with XAPO_REGISTRATION_PROPERTIES.Flags: +XAPO_FLAGS :: distinct bit_set[XAPO_FLAG; u32] +XAPO_FLAG :: enum u32 { + // Number of channels of input and output buffers must match, applies to XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.pFormat. + CHANNELS_MUST_MATCH = 0, + + // Framerate of input and output buffers must match, applies to XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.pFormat. + FRAMERATE_MUST_MATCH = 1, + + // Bit depth of input and output buffers must match, applies to XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.pFormat. + // Container size of input and output buffers must also match if XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.pFormat is WAVEFORMATEXTENSIBLE. + BITSPERSAMPLE_MUST_MATCH = 2, + + // Number of input and output buffers must match, applies to XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS. + // Also, XAPO_REGISTRATION_PROPERTIES.MinInputBufferCount must equal XAPO_REGISTRATION_PROPERTIES.MinOutputBufferCount and XAPO_REGISTRATION_PROPERTIES.MaxInputBufferCount must equal XAPO_REGISTRATION_PROPERTIES.MaxOutputBufferCount when used. + BUFFERCOUNT_MUST_MATCH = 3, + + // XAPO must be run in-place. Use this flag only if your DSP implementation cannot process separate input and output buffers. + // If set, the following flags must also be set: + // XAPO_FLAG_CHANNELS_MUST_MATCH + // XAPO_FLAG_FRAMERATE_MUST_MATCH + // XAPO_FLAG_BITSPERSAMPLE_MUST_MATCH + // XAPO_FLAG_BUFFERCOUNT_MUST_MATCH + // XAPO_FLAG_INPLACE_SUPPORTED + // Multiple input and output buffers may be used with in-place XAPOs, though the input buffer count must equal the output buffer count. + // When multiple input/output buffers are used, the XAPO may assume input buffer [N] equals output buffer [N] for in-place processing. + INPLACE_REQUIRED = 5, + + // XAPO may be run in-place. If the XAPO is used in a chain such that the requirements for XAPO_FLAG_INPLACE_REQUIRED are met, XAudio2 will ensure the XAPO is run in-place. + // If not met, XAudio2 will still run the XAPO albeit with separate input and output buffers. + // For example, consider an effect which may be ran in stereo->5.1 mode or mono->mono mode. When set to stereo->5.1, it will be run with separate input and output buffers as format conversion is not permitted in-place. + // However, if configured to run mono->mono, the same XAPO can be run in-place. Thus the same implementation may be conveniently reused for various input/output configurations, while taking advantage of in-place processing when possible. + INPLACE_SUPPORTED = 4, +} + +//-----------------------------------------------------// + +// XAPO registration properties, describes general XAPO characteristics, used with IXAPO.GetRegistrationProperties +XAPO_REGISTRATION_PROPERTIES :: struct #packed { + clsid: win.CLSID, // COM class ID, used with CoCreate + FriendlyName: [XAPO_REGISTRATION_STRING_LENGTH]u16, // friendly name unicode string + CopyrightInfo: [XAPO_REGISTRATION_STRING_LENGTH]u16, // copyright information unicode string + MajorVersion: u32, // major version + MinorVersion: u32, // minor version + Flags: XAPO_FLAGS, // XAPO property flags, describes supported input/output configuration + MinInputBufferCount: u32, // minimum number of input buffers required for processing, can be 0 + MaxInputBufferCount: u32, // maximum number of input buffers supported for processing, must be >= MinInputBufferCount + MinOutputBufferCount: u32, // minimum number of output buffers required for processing, can be 0, must match MinInputBufferCount when XAPO_FLAG_BUFFERCOUNT_MUST_MATCH used + MaxOutputBufferCount: u32, // maximum number of output buffers supported for processing, must be >= MinOutputBufferCount, must match MaxInputBufferCount when XAPO_FLAG_BUFFERCOUNT_MUST_MATCH used +} + +// LockForProcess buffer parameters: +// Defines buffer parameters that remain constant while an XAPO is locked. +// Used with IXAPO::LockForProcess. +// For CBR XAPOs, MaxFrameCount is the only number of frames +// IXAPO::Process would have to handle for the respective buffer. +XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS :: struct #packed { + pFormat: ^WAVEFORMATEX, // buffer audio format + MaxFrameCount: u32, // maximum number of frames in respective buffer that IXAPO::Process would have to handle, irrespective of dynamic variable settings, can be 0 +} + +// Buffer flags: +// Describes assumed content of the respective buffer. +// Used with XAPO_PROCESS_BUFFER_PARAMETERS.BufferFlags. +// This meta-data can be used by an XAPO to implement optimizations that require knowledge of a buffer's content. +// For example, XAPOs that always produce silent output from silent input can check the flag on the input buffer to determine if any signal processing is necessary. +// If silent, the XAPO may simply set the flag on the output buffer to silent and return, optimizing out the work of processing silent data: XAPOs that generate silence for any reason may set the buffer's flag accordingly rather than writing out silent frames to the buffer itself. +// The flags represent what should be assumed is in the respective buffer. The flags may not reflect what is actually stored in memory. +XAPO_BUFFER_FLAGS :: enum i32 { + XAPO_BUFFER_SILENT, // silent data should be assumed, respective memory may be uninitialized + XAPO_BUFFER_VALID, // arbitrary data should be assumed (may or may not be silent frames), respective memory initialized +} + +// Process buffer parameters: +// Defines buffer parameters that may change from one +// processing pass to the next. Used with IXAPO::Process. +// +// Note the byte size of the respective buffer must be at least: +// XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount * XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.pFormat->nBlockAlign +// +// Although the audio format and maximum size of the respective +// buffer is locked (defined by XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS), +// the actual memory address of the buffer given is permitted to change +// from one processing pass to the next. +// +// For CBR XAPOs, ValidFrameCount is constant while locked and equals +// the respective XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount. +XAPO_PROCESS_BUFFER_PARAMETERS :: struct #packed { + pBuffer: rawptr, // audio data buffer, must be non-NULL + BufferFlags: XAPO_BUFFER_FLAGS, // describes assumed content of pBuffer, does not affect ValidFrameCount + ValidFrameCount: u32, // number of frames of valid data, must be within respective [0, XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount], always XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount for CBR/user-defined XAPOs, does not affect BufferFlags +} + +XAPOFree :: win.CoTaskMemFree + +IXAPO_UUID_STRING :: "A410B984-9839-4819-A0BE-2856AE6B3ADB" +IXAPO_UUID := &win.IID{0xA410B984, 0x9839, 0x4819, {0xA0, 0xBE, 0x28, 0x56, 0xAE, 0x6B, 0x3A, 0xDB}} +IXAPO :: struct #raw_union { + #subtype iunknown: IUnknown, + using ixapo_vtable: ^IXAPO_VTable, +} +IXAPO_VTable :: struct { + using iunknown_vtable: IUnknown_VTable, + + // DESCRIPTION: + // Allocates a copy of the registration properties of the XAPO. + // PARAMETERS: + // ppRegistrationProperties - [out] receives pointer to copy of registration properties, use XAPOFree to free structure, left untouched on failure + // RETURN VALUE: + // COM error code + GetRegistrationProperties: proc "system" (this: ^IXAPO, ppRegistrationProperties: ^^XAPO_REGISTRATION_PROPERTIES) -> HRESULT, + + // DESCRIPTION: + // Queries if an input/output configuration is supported. + // REMARKS: + // This method allows XAPOs to express dependency of input format with respect to output format. + // If the input/output format pair configuration is unsupported, this method also determines the nearest input format supported. + // Nearest meaning closest bit depth, framerate, and channel count, in that order of importance. + // The behaviour of this method should remain constant after the XAPO has been initialized. + // PARAMETERS: + // pOutputFormat - [in] output format known to be supported + // pRequestedInputFormat - [in] input format to examine + // ppSupportedInputFormat - [out] receives pointer to nearest input format supported if not NULL and input/output configuration unsupported, use XAPOFree to free structure, left untouched on any failure except XAPO_E_FORMAT_UNSUPPORTED + // RETURN VALUE: + // COM error code, including: + // S_OK - input/output configuration supported, ppSupportedInputFormat left untouched + // FORMAT_UNSUPPORTED - input/output configuration unsupported, ppSupportedInputFormat receives pointer to nearest input format supported if not NULL + // E_INVALIDARG - either audio format invalid, ppSupportedInputFormat left untouched + IsInputFormatSupported: proc "system" (this: ^IXAPO, pOutputFormat: ^WAVEFORMATEX, pRequestedInputFormat: ^WAVEFORMATEX, ppSupportedInputFormat: ^^WAVEFORMATEX) -> HRESULT, + + // DESCRIPTION: + // Queries if an input/output configuration is supported. + // REMARKS: + // This method allows XAPOs to express dependency of output format with respect to input format. + // If the input/output format pair configuration is unsupported, this method also determines the nearest output format supported. + // Nearest meaning closest bit depth, framerate, and channel count, in that order of importance. + // The behaviour of this method should remain constant after the XAPO has been initialized. + // PARAMETERS: + // pInputFormat - [in] input format known to be supported + // pRequestedOutputFormat - [in] output format to examine + // ppSupportedOutputFormat - [out] receives pointer to nearest output format supported if not NULL and input/output configuration unsupported, use XAPOFree to free structure, left untouched on any failure except XAPO_E_FORMAT_UNSUPPORTED + // RETURN VALUE: + // COM error code, including: + // S_OK - input/output configuration supported, ppSupportedOutputFormat left untouched + // FORMAT_UNSUPPORTED - input/output configuration unsupported, ppSupportedOutputFormat receives pointer to nearest output format supported if not NULL + // E_INVALIDARG - either audio format invalid, ppSupportedOutputFormat left untouched + IsOutputFormatSupported: proc "system" (this: ^IXAPO, pInputFormat: ^WAVEFORMATEX, pRequestedOutputFormat: ^WAVEFORMATEX, ppSupportedOutputFormat: ^^WAVEFORMATEX) -> HRESULT, + + // DESCRIPTION: + // Performs any effect-specific initialization if required. + // REMARKS: + // The contents of pData are defined by the XAPO. + // Immutable variables (constant during the lifespan of the XAPO) should be set once via this method. + // Once initialized, an XAPO cannot be initialized again. + // An XAPO should be initialized before passing it to XAudio2 as part of an effect chain. XAudio2 will not call this method; it exists for future content-driven initialization. + // PARAMETERS: + // pData - [in] effect-specific initialization parameters, may be NULL if DataByteSize == 0 + // DataByteSize - [in] size of pData in bytes, may be 0 if pData is NULL + // RETURN VALUE: + // COM error code + Initialize: proc "system" (this: ^IXAPO, pData: rawptr, DataByteSize: u32) -> HRESULT, + + // DESCRIPTION: + // Resets variables dependent on frame history. + // REMARKS: + // All other variables remain unchanged, including variables set by IXAPOParameters.SetParameters. + // For example, an effect with delay should zero out its delay line during this method, but should not reallocate anything as the + // XAPO remains locked with a constant input/output configuration. XAudio2 calls this method only if the XAPO is locked. + // This method should not block as it is called from the realtime thread. + // PARAMETERS: + // void + // RETURN VALUE: + // void + Reset: proc "system" (this: ^IXAPO), + + // DESCRIPTION: + // Locks the XAPO to a specific input/output configuration, + // allowing it to do any final initialization before Process + // is called on the realtime thread. + // REMARKS: + // Once locked, the input/output configuration and any other locked variables remain constant until UnlockForProcess is called. + // XAPOs should assert the input/output configuration is supported and that any required effect-specific initialization is complete. + // IsInputFormatSupported, IsOutputFormatSupported, and Initialize should be called as necessary before this method is called. + // All internal memory buffers required for Process should be allocated by the time this method returns successfully as Process is non-blocking and should not allocate memory. + // Once locked, an XAPO cannot be locked again until UnLockForProcess is called. + // PARAMETERS: + // InputLockedParameterCount - [in] number of input buffers, must be within [XAPO_REGISTRATION_PROPERTIES.MinInputBufferCount, XAPO_REGISTRATION_PROPERTIES.MaxInputBufferCount] + // pInputLockedParameters - [in] array of input locked buffer parameter structures, may be NULL if InputLockedParameterCount == 0, otherwise must have InputLockedParameterCount elements + // OutputLockedParameterCount - [in] number of output buffers, must be within [XAPO_REGISTRATION_PROPERTIES.MinOutputBufferCount, XAPO_REGISTRATION_PROPERTIES.MaxOutputBufferCount], must match InputLockedParameterCount when XAPO_FLAG_BUFFERCOUNT_MUST_MATCH used + // pOutputLockedParameters - [in] array of output locked buffer parameter structures, may be NULL if OutputLockedParameterCount == 0, otherwise must have OutputLockedParameterCount elements + // RETURN VALUE: + // COM error code + LockForProcess: proc "system" (this: ^IXAPO, InputLockedParameterCount: u32, pInputLockedParameters: [^]XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS, OutputLockedParameterCount: u32, pOutputLockedParameters: [^]XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS) -> HRESULT, + + // DESCRIPTION: + // Opposite of LockForProcess. Variables allocated during LockForProcess should be deallocated by this method. + // REMARKS: + // Unlocking an XAPO allows an XAPO instance to be reused with different input/output configurations. + // PARAMETERS: + // void + // RETURN VALUE: + // void + UnlockForProcess: proc "system" (this: ^IXAPO), + + // DESCRIPTION: + // Runs the XAPO's DSP code on the given input/output buffers. + // REMARKS: + // In addition to writing to the output buffers as appropriate, an XAPO must set the BufferFlags and ValidFrameCount members of all elements in pOutputProcessParameters accordingly. + // ppInputProcessParameters will not necessarily be the same as ppOutputProcessParameters for in-place processing, rather the pBuffer members of each will point to the same memory. + // Multiple input/output buffers may be used with in-place XAPOs, though the input buffer count must equal the output buffer count. + // When multiple input/output buffers are used with in-place XAPOs, the XAPO may assume input buffer [N] equals output buffer [N]. + // When IsEnabled is FALSE, the XAPO should process thru. Thru processing means an XAPO should not apply its normal processing to the given input/output buffers during Process. + // It should instead pass data from input to output with as little modification possible. Effects that perform format conversion should continue to do so. + // The effect must ensure transitions between normal and thru processing do not introduce discontinuities into the signal. + // XAudio2 calls this method only if the XAPO is locked. This method should not block as it is called from the realtime thread. + // PARAMETERS: + // InputProcessParameterCount - [in] number of input buffers, matches respective InputLockedParameterCount parameter given to LockForProcess + // pInputProcessParameters - [in] array of input process buffer parameter structures, may be NULL if InputProcessParameterCount == 0, otherwise must have InputProcessParameterCount elements + // OutputProcessParameterCount - [in] number of output buffers, matches respective OutputLockedParameterCount parameter given to LockForProcess + // pOutputProcessParameters - [in/out] array of output process buffer parameter structures, may be NULL if OutputProcessParameterCount == 0, otherwise must have OutputProcessParameterCount elements + // IsEnabled - [in] TRUE to process normally, FALSE to process thru + // RETURN VALUE: + // void + Process: proc "system" (this: ^IXAPO, InputProcessParameterCount: u32, pInputProcessParameters: [^]XAPO_PROCESS_BUFFER_PARAMETERS, OutputProcessParameterCount: u32, pOutputProcessParameters: [^]XAPO_PROCESS_BUFFER_PARAMETERS, IsEnabled: b32), + + // DESCRIPTION: + // Returns the number of input frames required to generate the requested number of output frames. + // REMARKS: + // XAudio2 may call this method to determine how many input frames an XAPO requires. + // This is constant for locked CBR XAPOs; this method need only be called once while an XAPO is locked. + // XAudio2 calls this method only if the XAPO is locked. This method should not block as it is called from the realtime thread. + // PARAMETERS: + // OutputFrameCount - [in] requested number of output frames, must be within respective [0, XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount], always XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount for CBR/user-defined XAPOs + // RETURN VALUE: + // number of input frames required + CalcInputFrames: proc "system" (this: ^IXAPO, OutputFrameCount: u32) -> u32, + + // DESCRIPTION: + // Returns the number of output frames generated for the requested number of input frames. + // REMARKS: + // XAudio2 may call this method to determine how many output frames an XAPO will generate. This is constant for locked CBR XAPOs; this method need only be called once while an XAPO is locked. + // XAudio2 calls this method only if the XAPO is locked. This method should not block as it is called from the realtime thread. + // PARAMETERS: + // InputFrameCount - [in] requested number of input frames, must be within respective [0, XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount], always XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS.MaxFrameCount for CBR/user-defined XAPOs + // RETURN VALUE: + // number of output frames generated + CalcOutputFrames: proc "system" (this: ^IXAPO, InputFrameCount: u32) -> u32, +} + +// IXAPOParameters: +// Optional XAPO COM interface that allows an XAPO to use effect-specific parameters. +IXAPOParameters_UUID_STRING :: "26D95C66-80F2-499A-AD54-5AE7F01C6D98" +IXAPOParameters_UUID := &win.IID{0x26D95C66, 0x80F2, 0x499A, {0xAD, 0x54, 0x5A, 0xE7, 0xF0, 0x1C, 0x6D, 0x98}} +IXAPOParameters :: struct #raw_union { + #subtype iunknown: IUnknown, + using ixapoparameters_vtable: ^IXAPOParameters_VTable, +} +IXAPOParameters_VTable :: struct { + using iunknown_vtable: IUnknown_VTable, + + // DESCRIPTION: + // Sets effect-specific parameters. + // REMARKS: + // This method may only be called on the realtime thread; no synchronization between it and IXAPO.Process is necessary. + // This method should not block as it is called from the realtime thread. + // PARAMETERS: + // pParameters - [in] effect-specific parameter block, must be != NULL + // ParameterByteSize - [in] size of pParameters in bytes, must be > 0 + // RETURN VALUE: + // void + SetParameters: proc "system" (this: ^IXAPOParameters, pParameters: rawptr, ParameterByteSize: u32), + + // DESCRIPTION: + // Gets effect-specific parameters. + // REMARKS: + // Unlike SetParameters, XAudio2 does not call this method on the realtime thread. Thus, the XAPO must protect variables shared with SetParameters/Process using appropriate synchronization. + // PARAMETERS: + // pParameters - [out] receives effect-specific parameter block, must be != NULL + // ParameterByteSize - [in] size of pParameters in bytes, must be > 0 + // RETURN VALUE: + // void + GetParameters: proc "system" (this: ^IXAPOParameters, pParameters: rawptr, ParameterByteSize: u32), +} diff --git a/vendor/windows/XAudio2/xapofx.odin b/vendor/windows/XAudio2/xapofx.odin new file mode 100644 index 000000000..5c4c8c7ec --- /dev/null +++ b/vendor/windows/XAudio2/xapofx.odin @@ -0,0 +1,138 @@ +#+build windows + +package windows_xaudio2 + +import win "core:sys/windows" + +foreign import xa2 "system:xaudio2.lib" + +//---------------------------------------------------// + +FXEQ_UUID_STRING :: "F5E01117-D6C4-485A-A3F5-695196F3DBFA" +FXEQ_UUID := &win.CLSID{0xF5E01117, 0xD6C4, 0x485A, {0xA3, 0xF5, 0x69, 0x51, 0x96, 0xF3, 0xDB, 0xFA}} + +FXMasteringLimiter_UUID_STRING :: "C4137916-2BE1-46FD-8599-441536F49856" +FXMasteringLimiter_UUID := &win.CLSID{0xC4137916, 0x2BE1, 0x46FD, {0x85, 0x99, 0x44, 0x15, 0x36, 0xF4, 0x98, 0x56}} + +FXReverb_UUID_STRING :: "7D9ACA56-CB68-4807-B632-B137352E8596" +FXReverb_UUID := &win.CLSID{0x7D9ACA56, 0xCB68, 0x4807, {0xB6, 0x32, 0xB1, 0x37, 0x35, 0x2E, 0x85, 0x96}} + +FXEcho_UUID_STRING :: "5039D740-F736-449A-84D3-A56202557B87" +FXEcho_UUID := &win.CLSID{0x5039D740, 0xF736, 0x449A, {0x84, 0xD3, 0xA5, 0x62, 0x02, 0x55, 0x7B, 0x87}} + +// EQ parameter bounds (inclusive), used with FXEQ: +FXEQ_MIN_FRAMERATE :: 22000 +FXEQ_MAX_FRAMERATE :: 48000 + +FXEQ_MIN_FREQUENCY_CENTER :: 20.0 +FXEQ_MAX_FREQUENCY_CENTER :: 20000.0 +FXEQ_DEFAULT_FREQUENCY_CENTER_0 :: 100.0 // band 0 +FXEQ_DEFAULT_FREQUENCY_CENTER_1 :: 800.0 // band 1 +FXEQ_DEFAULT_FREQUENCY_CENTER_2 :: 2000.0 // band 2 +FXEQ_DEFAULT_FREQUENCY_CENTER_3 :: 10000.0 // band 3 + +FXEQ_MIN_GAIN :: 0.126 // -18dB +FXEQ_MAX_GAIN :: 7.94 // +18dB +FXEQ_DEFAULT_GAIN :: 1.0 // 0dB change, all bands + +FXEQ_MIN_BANDWIDTH :: 0.1 +FXEQ_MAX_BANDWIDTH :: 2.0 +FXEQ_DEFAULT_BANDWIDTH :: 1.0 // all bands + + +// Mastering limiter parameter bounds (inclusive), used with FXMasteringLimiter: +FXMASTERINGLIMITER_MIN_RELEASE :: 1 +FXMASTERINGLIMITER_MAX_RELEASE :: 20 +FXMASTERINGLIMITER_DEFAULT_RELEASE :: 6 + +FXMASTERINGLIMITER_MIN_LOUDNESS :: 1 +FXMASTERINGLIMITER_MAX_LOUDNESS :: 1800 +FXMASTERINGLIMITER_DEFAULT_LOUDNESS :: 1000 + + +// Reverb parameter bounds (inclusive), used with FXReverb: +FXREVERB_MIN_DIFFUSION :: 0.0 +FXREVERB_MAX_DIFFUSION :: 1.0 +FXREVERB_DEFAULT_DIFFUSION :: 0.9 + +FXREVERB_MIN_ROOMSIZE :: 0.0001 +FXREVERB_MAX_ROOMSIZE :: 1.0 +FXREVERB_DEFAULT_ROOMSIZE :: 0.6 + +// Loudness defaults used with FXLoudness: +FXLOUDNESS_DEFAULT_MOMENTARY_MS :: 400 +FXLOUDNESS_DEFAULT_SHORTTERM_MS :: 3000 + +// Echo initialization data/parameter bounds (inclusive), used with FXEcho: +FXECHO_MIN_WETDRYMIX :: 0.0 +FXECHO_MAX_WETDRYMIX :: 1.0 +FXECHO_DEFAULT_WETDRYMIX :: 0.5 + +FXECHO_MIN_FEEDBACK :: 0.0 +FXECHO_MAX_FEEDBACK :: 1.0 +FXECHO_DEFAULT_FEEDBACK :: 0.5 + +FXECHO_MIN_DELAY :: 1.0 +FXECHO_MAX_DELAY :: 2000.0 +FXECHO_DEFAULT_DELAY :: 500.0 + +//-----------------------------------------------------// + +// EQ parameters (4 bands), used with IXAPOParameters.SetParameters: +// The EQ supports only f32 audio foramts. +// The framerate must be within [22000, 48000] Hz. +FXEQ_PARAMETERS :: struct #packed { + FrequencyCenter0: f32, // center frequency in Hz, band 0 + Gain0: f32, // boost/cut + Bandwidth0: f32, // bandwidth, region of EQ is center frequency +/- bandwidth/2 + FrequencyCenter1: f32, // band 1 + Gain1: f32, + Bandwidth1: f32, + FrequencyCenter2: f32, // band 2 + Gain2: f32, + Bandwidth2: f32, + FrequencyCenter3: f32, // band 3 + Gain3: f32, + Bandwidth3: f32, +} + +// Mastering limiter parameters, used with IXAPOParameters.SetParameters: +// The mastering limiter supports only f32 audio formats. +FXMASTERINGLIMITER_PARAMETERS :: struct #packed { + Release: u32, // release time (tuning factor with no specific units) + Loudness: u32, // loudness target (threshold) +} + +// Reverb parameters, used with IXAPOParameters.SetParameters: +// The reverb supports only f32 audio formats with the following channel configurations: +// Input: Mono Output: Mono +// Input: Stereo Output: Stereo +FXREVERB_PARAMETERS :: struct #packed { + Diffusion: f32, // diffusion + RoomSize: f32, // room size +} + + +// Echo initialization data, used with CreateFX: +// Use of this structure is optional, the default MaxDelay is FXECHO_DEFAULT_DELAY. +FXECHO_INITDATA :: struct #packed { + MaxDelay: f32, // maximum delay (all channels) in milliseconds, must be within [FXECHO_MIN_DELAY, FXECHO_MAX_DELAY] +} + +// Echo parameters, used with IXAPOParameters.SetParameters: +// The echo supports only f32 audio formats. +FXECHO_PARAMETERS :: struct #packed { + WetDryMix: f32, // ratio of wet (processed) signal to dry (original) signal + Feedback: f32, // amount of output fed back into input + Delay: f32, // delay (all channels) in milliseconds, must be within [FXECHO_MIN_DELAY, FXECHO_PARAMETERS.MaxDelay] +} + +//-------------------------------------------------------// + +@(default_calling_convention="cdecl") +foreign xa2 { + // creates instance of requested XAPO, use Release to free instance + // pInitData - [in] effect-specific initialization parameters, may be NULL if InitDataByteSize == 0 + // InitDataByteSize - [in] size of pInitData in bytes, may be 0 if pInitData is NULL + CreateFX :: proc(clsid: win.REFCLSID, pEffect: ^^IUnknown, pInitDat: rawptr = nil, InitDataByteSize: u32 = 0) -> HRESULT --- +} diff --git a/vendor/windows/XAudio2/xaudio2.odin b/vendor/windows/XAudio2/xaudio2.odin new file mode 100644 index 000000000..078ec4095 --- /dev/null +++ b/vendor/windows/XAudio2/xaudio2.odin @@ -0,0 +1,839 @@ +#+build windows +/* + Bindings for Windows XAudio2: + https://learn.microsoft.com/en-us/windows/win32/xaudio2/xaudio2-introduction + + Compiling for Windows 10 RS5 (1809) and later +*/ + +package windows_xaudio2 + +import win "core:sys/windows" +import "core:math" + +HRESULT :: win.HRESULT +IUnknown :: win.IUnknown +IUnknown_VTable :: win.IUnknown_VTable +WAVEFORMATEX :: win.WAVEFORMATEX + +/************************************************************************** + * + * XAudio2 constants, flags and error codes. + * + **************************************************************************/ + +// Numeric boundary values +MAX_BUFFER_BYTES :: 0x80000000 // Maximum bytes allowed in a source buffer +MAX_QUEUED_BUFFERS :: 64 // Maximum buffers allowed in a voice queue +MAX_BUFFERS_SYSTEM :: 2 // Maximum buffers allowed for system threads (Xbox 360 only) +MAX_AUDIO_CHANNELS :: 64 // Maximum channels in an audio stream +MIN_SAMPLE_RATE :: 1000 // Minimum audio sample rate supported +MAX_SAMPLE_RATE :: 200000 // Maximum audio sample rate supported +MAX_VOLUME_LEVEL :: 16777216.0 // Maximum acceptable volume level (2^24) +MIN_FREQ_RATIO :: (1.0 / 1024.0) // Minimum SetFrequencyRatio argument +MAX_FREQ_RATIO :: 1024.0 // Maximum MaxFrequencyRatio argument +DEFAULT_FREQ_RATIO :: 2.0 // Default MaxFrequencyRatio argument +MAX_FILTER_ONEOVERQ :: 1.5 // Maximum FILTER_PARAMETERS.OneOverQ +MAX_FILTER_FREQUENCY :: 1.0 // Maximum FILTER_PARAMETERS.Frequency +MAX_LOOP_COUNT :: 254 // Maximum non-infinite BUFFER.LoopCount +MAX_INSTANCES :: 8 // Maximum simultaneous XAudio2 objects on Xbox 360 + +// For XMA voices on Xbox 360 there is an additional restriction on the MaxFrequencyRatio argument and the voice's sample rate: the product of these numbers cannot exceed 600000 for one-channel voices or 300000 for voices with more than one channel. +MAX_RATIO_TIMES_RATE_XMA_MONO :: 600000 +MAX_RATIO_TIMES_RATE_XMA_MULTICHANNEL :: 300000 + +// Numeric values with special meanings +COMMIT_NOW :: 0 // Used as an OperationSet argument +COMMIT_ALL :: 0 // Used in IXAudio2.CommitChanges +INVALID_OPSET :: 0xffffffff // Not allowed for OperationSet arguments +NO_LOOP_REGION :: 0 // Used in BUFFER.LoopCount +LOOP_INFINITE :: 255 // Used in BUFFER.LoopCount +DEFAULT_CHANNELS :: 0 // Used in CreateMasteringVoice +DEFAULT_SAMPLERATE :: 0 // Used in CreateMasteringVoice + +// Flags +FLAGS :: distinct bit_set[FLAG; u32] +FLAG :: enum u32 { + DEBUG_ENGINE = 0, // Used in Create + VOICE_NOPITCH = 1, // Used in IXAudio2.CreateSourceVoice + VOICE_NOSRC = 2, // Used in IXAudio2.CreateSourceVoice + VOICE_USEFILTER = 3, // Used in IXAudio2.CreateSource/SubmixVoice + PLAY_TAILS = 5, // Used in IXAudio2SourceVoice.Stop + END_OF_STREAM = 6, // Used in BUFFER.Flags + SEND_USEFILTER = 7, // Used in SEND_DESCRIPTOR.Flags + VOICE_NOSAMPLESPLAYED = 8, // Used in IXAudio2SourceVoice.GetState + STOP_ENGINE_WHEN_IDLE = 13, // Used in Create to force the engine to Stop when no source voices are Started, and Start when a voice is Started + QUANTUM_1024 = 15, // Used in Create to specify nondefault processing quantum of 21.33 ms (1024 samples at 48KHz) + NO_VIRTUAL_AUDIO_CLIENT = 16, // Used in CreateMasteringVoice to create a virtual audio client +} + +// Default parameters for the built-in filter +DEFAULT_FILTER_TYPE :: FILTER_TYPE.LowPassFilter +DEFAULT_FILTER_FREQUENCY :: MAX_FILTER_FREQUENCY +DEFAULT_FILTER_ONEOVERQ :: 1.0 + +// Internal XAudio2 constants +// The audio frame quantum can be calculated by reducing the fraction: +// SamplesPerAudioFrame / SamplesPerSecond +QUANTUM_NUMERATOR :: 1 // On Windows, XAudio2 processes audio +QUANTUM_DENOMINATOR :: 100 // in 10ms chunks (= 1/100 seconds) +QUANTUM_MS :: (1000.0 * QUANTUM_NUMERATOR / QUANTUM_DENOMINATOR) + +// XAudio2 error codes +INVALID_CALL :: HRESULT(-2003435519) // 0x88960001 An API call or one of its arguments was illegal +XMA_DECODER_ERROR :: HRESULT(-2003435518) // 0x88960002 The XMA hardware suffered an unrecoverable error +XAPO_CREATION_FAILED :: HRESULT(-2003435517) // 0x88960003 XAudio2 failed to initialize an XAPO effect +DEVICE_INVALIDATED :: HRESULT(-2003435516) // 0x88960004 An audio device became unusable (unplugged, etc) + + +/************************************************************************** + * + * XAudio2 structures and enumerations. + * + **************************************************************************/ + +// Used in Create, specifies which CPU(s) to use. +PROCESSOR_FLAGS :: distinct bit_set[PROCESOR_FLAG; u32] +PROCESOR_FLAG :: enum u32 { + Processor1 = 0, + Processor2 = 1, + Processor3 = 2, + Processor4 = 3, + Processor5 = 4, + Processor6 = 5, + Processor7 = 6, + Processor8 = 7, + Processor9 = 8, + Processor10 = 9, + Processor11 = 10, + Processor12 = 11, + Processor13 = 12, + Processor14 = 13, + Processor15 = 14, + Processor16 = 15, + Processor17 = 16, + Processor18 = 17, + Processor19 = 18, + Processor20 = 19, + Processor21 = 20, + Processor22 = 21, + Processor23 = 22, + Processor24 = 23, + Processor25 = 24, + Processor26 = 25, + Processor27 = 26, + Processor28 = 27, + Processor29 = 28, + Processor30 = 29, + Processor31 = 30, + Processor32 = 31, +} + +USE_DEFAULT_PROCESSOR :: PROCESSOR_FLAGS{} + +// Returned by IXAudio2Voice.GetVoiceDetails +VOICE_DETAILS :: struct #packed { + CreatingFlags: FLAGS, + ActiveFlags: FLAGS, + InputChannels: u32, + InputSampleRate: u32, +} + +// Used in VOICE_SENDS below +SEND_DESCRIPTOR :: struct #packed { + Flags: FLAGS, // Either 0 or SEND_USEFILTER. + pOutputVoice: ^IXAudio2Voice, // This send's destination voice. +} + +// Used in the voice creation functions and in IXAudio2Voice.SetOutputVoices +VOICE_SENDS :: struct #packed { + SendCount: u32, // Number of sends from this voice. + pSends: [^]SEND_DESCRIPTOR, // Array of SendCount send descriptors. +} + +// Used in EFFECT_CHAIN below +EFFECT_DESCRIPTOR :: struct #packed { + pEffect: ^IUnknown, // Pointer to the effect object's IUnknown interface. + InitialState: b32, // TRUE if the effect should begin in the enabled state. + OutputChannels: u32, // How many output channels the effect should produce. +} + +// Used in the voice creation functions and in IXAudio2Voice.SetEffectChain +EFFECT_CHAIN :: struct #packed { + EffectCount: u32, // Number of effects in this voice's effect chain. + pEffectDescriptors: [^]EFFECT_DESCRIPTOR, // Array of effect descriptors. +} + +// Used in FILTER_PARAMETERS below +FILTER_TYPE :: enum i32 { + LowPassFilter, // Attenuates frequencies above the cutoff frequency (state-variable filter). + BandPassFilter, // Attenuates frequencies outside a given range (state-variable filter). + HighPassFilter, // Attenuates frequencies below the cutoff frequency (state-variable filter). + NotchFilter, // Attenuates frequencies inside a given range (state-variable filter). + LowPassOnePoleFilter, // Attenuates frequencies above the cutoff frequency (one-pole filter, FILTER_PARAMETERS.OneOverQ has no effect) + HighPassOnePoleFilter, // Attenuates frequencies below the cutoff frequency (one-pole filter, FILTER_PARAMETERS.OneOverQ has no effect) +} + +// Used in IXAudio2Voice.Set/GetFilterParameters and Set/GetOutputFilterParameters +FILTER_PARAMETERS :: struct #packed { + Type: FILTER_TYPE, // Filter type. + Frequency: f32, // Filter coefficient. Must be >= 0 and <= MAX_FILTER_FREQUENCY. See CutoffFrequencyToRadians() for state-variable filter types and CutoffFrequencyToOnePoleCoefficient() for one-pole filter types. + OneOverQ: f32, // Reciprocal of the filter's quality factor Q; must be > 0 and <= MAX_FILTER_ONEOVERQ. Has no effect for one-pole filters. +} + +// Used in IXAudio2SourceVoice.SubmitSourceBuffer +BUFFER :: struct #packed { + Flags: FLAGS, // Either 0 or END_OF_STREAM. + AudioBytes: u32, // Size of the audio data buffer in bytes. + pAudioData: [^]byte, // Pointer to the audio data buffer. + PlayBegin: u32, // First sample in this buffer to be played. + PlayLength: u32, // Length of the region to be played in samples, or 0 to play the whole buffer. + LoopBegin: u32, // First sample of the region to be looped. + LoopLength: u32, // Length of the desired loop region in samples, or 0 to loop the entire buffer. + LoopCount: u32, // Number of times to repeat the loop region, or LOOP_INFINITE to loop forever. + pContext: rawptr, // Context value to be passed back in callbacks. +} + +// Used in IXAudio2SourceVoice.SubmitSourceBuffer when submitting XWMA data. +// NOTE: If an XWMA sound is submitted in more than one buffer, each buffer's pDecodedPacketCumulativeBytes[PacketCount-1] value must be subtracted from all the entries in the next buffer's pDecodedPacketCumulativeBytes array. +// And whether a sound is submitted in more than one buffer or not, the final buffer of the sound should use the END_OF_STREAM flag, or else the client must call IXAudio2SourceVoice.Discontinuity after submitting it. +BUFFER_WMA :: struct #packed { + pDecodedPacketCumulativeBytes: [^]u32, // Decoded packet's cumulative size array. Each element is the number of bytes accumulated when the corresponding XWMA packet is decoded in order. The array must have PacketCount elements. + PacketCount: u32, // Number of XWMA packets submitted. Must be >= 1 and divide evenly into BUFFER.AudioBytes. +} + +// Returned by IXAudio2SourceVoice.GetState +VOICE_STATE :: struct #packed { + pCurrentBufferContext: rawptr, // The pContext value provided in the BUFFER that is currently being processed, or NULL if there are no buffers in the queue. + BuffersQueued: u32, // Number of buffers currently queued on the voice (including the one that is being processed). + SamplesPlayed: u64, // Total number of samples produced by the voice since it began processing the current audio stream. If VOICE_NOSAMPLESPLAYED is specified in the call to IXAudio2SourceVoice.GetState, this member will not be calculated, saving CPU. +} + +// Returned by IXAudio2.GetPerformanceData +PERFORMANCE_DATA :: struct #packed { + // CPU usage information + AudioCyclesSinceLastQuery: u64, // CPU cycles spent on audio processing since the last call to StartEngine or GetPerformanceData. + TotalCyclesSinceLastQuery: u64, // Total CPU cycles elapsed since the last call (only counts the CPU XAudio2 is running on). + MinimumCyclesPerQuantum: u32, // Fewest CPU cycles spent processing any one audio quantum since the last call. + MaximumCyclesPerQuantum: u32, // Most CPU cycles spent processing any one audio quantum since the last call. + + // Memory usage information + MemoryUsageInBytes: u32, // Total heap space currently in use. + + // Audio latency and glitching information + CurrentLatencyInSamples: u32, // Minimum delay from when a sample is read from a source buffer to when it reaches the speakers. + GlitchesSinceEngineStarted: u32, // Audio dropouts since the engine was started. + + // Data about XAudio2's current workload + ActiveSourceVoiceCount: u32, // Source voices currently playing. + TotalSourceVoiceCount: u32, // Source voices currently existing. + ActiveSubmixVoiceCount: u32, // Submix voices currently playing/existing. + + ActiveResamplerCount: u32, // Resample xAPOs currently active. + ActiveMatrixMixCount: u32, // MatrixMix xAPOs currently active. + + // Usage of the hardware XMA decoder (Xbox 360 only) + ActiveXmaSourceVoices: u32, // Number of source voices decoding XMA data. + ActiveXmaStreams: u32, // A voice can use more than one XMA stream. +} + +// Used in IXAudio2.SetDebugConfiguration +DEBUG_CONFIGURATION :: struct #packed { + TraceMask: DEBUG_CONFIG_FLAGS, // Bitmap of enabled debug message types. + BreakMask: DEBUG_CONFIG_FLAGS, // Message types that will break into the debugger. + LogThreadID: b32, // Whether to log the thread ID with each message. + LogFileline: b32, // Whether to log the source file and line number. + LogFunctionName: b32, // Whether to log the function name. + LogTiming: b32, // Whether to log message timestamps. +} + +// Values for the TraceMask and BreakMask bitmaps. Only ERRORS and WARNINGS are valid in BreakMask. +// WARNINGS implies ERRORS, DETAIL implies INFO, and FUNC_CALLS implies API_CALLS. +// By default, TraceMask is ERRORS and WARNINGS and all the other settings are zero. +DEBUG_CONFIG_FLAGS :: distinct bit_set[DEBUG_CONFIG_FLAG; u32] +DEBUG_CONFIG_FLAG :: enum u32 { + ERRORS = 0, // For handled errors with serious effects. + WARNINGS = 1, // For handled errors that may be recoverable. + INFO = 2, // Informational chit-chat (e.g. state changes). + DETAIL = 3, // More detailed chit-chat. + API_CALLS = 4, // Public API function entries and exits. + FUNC_CALLS = 5, // Internal function entries and exits. + TIMING = 6, // Delays detected and other timing data. + LOCKS = 7, // Usage of critical sections and mutexes. + MEMORY = 8, // Memory heap usage information. + STREAMING = 12, // Audio streaming information. +} + +/************************************************************************** + * + * IXAudio2: Top-level XAudio2 COM interface. + * + **************************************************************************/ + +IXAudio2_UUID_STRING :: "2B02E3CF-2E0B-4ec3-BE45-1B2A3FE7210D" +IXAudio2_UUID := &win.IID{0x2B02E3CF, 0x2E0B, 0x4ec3, {0xBE, 0x45, 0x1B, 0x2A, 0x3F, 0xE7, 0x21, 0x0D}} +IXAudio2 :: struct #raw_union { + #subtype iunknown: IUnknown, + using ixaudio2_vtable: ^IXAudio2_VTable, +} +IXAudio2_VTable :: struct { + using iunknown_vtable: IUnknown_VTable, + + // NAME: IXAudio2.RegisterForCallbacks + // DESCRIPTION: Adds a new client to receive XAudio2's engine callbacks. + // ARGUMENTS: + // pCallback - Callback interface to be called during each processing pass. + RegisterForCallbacks: proc "system" (this: ^IXAudio2, pCallback: ^IXAudio2EngineCallback) -> HRESULT, + + // NAME: IXAudio2.UnregisterForCallbacks + // DESCRIPTION: Removes an existing receiver of XAudio2 engine callbacks. + // ARGUMENTS: + // pCallback - Previously registered callback interface to be removed. + UnregisterForCallbacks: proc "system" (this: ^IXAudio2, pCallback: ^IXAudio2EngineCallback), + + // NAME: IXAudio2.CreateSourceVoice + // DESCRIPTION: Creates and configures a source voice. + // ARGUMENTS: + // ppSourceVoice - Returns the new object's IXAudio2SourceVoice interface. + // pSourceFormat - Format of the audio that will be fed to the voice. + // Flags - VOICE flags specifying the source voice's behavior. + // MaxFrequencyRatio - Maximum SetFrequencyRatio argument to be allowed. + // pCallback - Optional pointer to a client-provided callback interface. + // pSendList - Optional list of voices this voice should send audio to. + // pEffectChain - Optional list of effects to apply to the audio data. + CreateSourceVoice: proc "system" (this: ^IXAudio2, ppSourceVoice: ^^IXAudio2SourceVoice, pSourceFormat: ^WAVEFORMATEX, Flags: FLAGS = {}, MaxFrequencyRatio: f32 = DEFAULT_FREQ_RATIO, pCallback: ^IXAudio2VoiceCallback = nil, pSendList: [^]VOICE_SENDS = nil, pEffectChain: [^]EFFECT_CHAIN = nil) -> HRESULT, + + // NAME: IXAudio2.CreateSubmixVoice + // DESCRIPTION: Creates and configures a submix voice. + // ARGUMENTS: + // ppSubmixVoice - Returns the new object's IXAudio2SubmixVoice interface. + // InputChannels - Number of channels in this voice's input audio data. + // InputSampleRate - Sample rate of this voice's input audio data. + // Flags - VOICE flags specifying the submix voice's behavior. + // ProcessingStage - Arbitrary number that determines the processing order. + // pSendList - Optional list of voices this voice should send audio to. + // pEffectChain - Optional list of effects to apply to the audio data. + CreateSubmixVoice: proc "system" (this: ^IXAudio2, ppSubmixVoice: ^^IXAudio2SubmixVoice, InputChannels: u32, InputSampleRate: u32, Flags: FLAGS = {}, ProcessingStage: u32 = 0, pSendList: [^]VOICE_SENDS = nil, pEffectChain: [^]EFFECT_CHAIN = nil) -> HRESULT, + + // NAME: IXAudio2.CreateMasteringVoice + // DESCRIPTION: Creates and configures a mastering voice. + // ARGUMENTS: + // ppMasteringVoice - Returns the new object's IXAudio2MasteringVoice interface. + // InputChannels - Number of channels in this voice's input audio data. + // InputSampleRate - Sample rate of this voice's input audio data. + // Flags - VOICE flags specifying the mastering voice's behavior. + // szDeviceId - Identifier of the device to receive the output audio. + // pEffectChain - Optional list of effects to apply to the audio data. + // StreamCategory - The audio stream category to use for this mastering voice + CreateMasteringVoice: proc "system" (this: ^IXAudio2, ppMasteringVoice: ^^IXAudio2MasteringVoice, InputChannels: u32 = DEFAULT_CHANNELS, InputSampleRate: u32 = DEFAULT_SAMPLERATE, Flags: FLAGS = {}, szDeviceId: win.LPCWSTR = nil, pEffectChain: [^]EFFECT_CHAIN = nil, StreamCategory: AUDIO_STREAM_CATEGORY = .GameEffects) -> HRESULT, + + // NAME: IXAudio2.:StartEngine + // DESCRIPTION: Creates and starts the audio processing thread. + StartEngine: proc "system" (this: ^IXAudio2) -> HRESULT, + + // NAME: IXAudio2.StopEngine + // DESCRIPTION: Stops and destroys the audio processing thread. + StopEngine: proc "system" (this: ^IXAudio2), + + // NAME: IXAudio2.CommitChanges + // DESCRIPTION: Atomically applies a set of operations previously tagged + // with a given identifier. + // ARGUMENTS: + // OperationSet - Identifier of the set of operations to be applied. + CommitChanges: proc "system" (this: ^IXAudio2, OperationSet: u32) -> HRESULT, + + // NAME: IXAudio2.GetPerformanceData + // DESCRIPTION: Returns current resource usage details: memory, CPU, etc. + // ARGUMENTS: + // pPerfData - Returns the performance data structure. + GetPerformanceData: proc "system" (this: ^IXAudio2, pPerfData: ^PERFORMANCE_DATA), + + // NAME: IXAudio2.SetDebugConfiguration + // DESCRIPTION: Configures XAudio2's debug output (in debug builds only). + // ARGUMENTS: + // pDebugConfiguration - Structure describing the debug output behavior. + // pReserved - Optional parameter; must be NULL. + SetDebugConfiguration: proc "system" (this: ^IXAudio2, pDebugConfiguration: ^DEBUG_CONFIGURATION, pReserved: rawptr = nil), +} + +// This interface extends IXAudio2 with additional functionality. +// Use IXAudio2.QueryInterface to obtain a pointer to this interface. +IXAudio2Extension_UUID_STRING :: "84ac29bb-d619-44d2-b197-e4acf7df3ed6" +IXAudio2Extension_UUID := &win.IID{0x84ac29bb, 0xd619, 0x44d2, {0xb1, 0x97, 0xe4, 0xac, 0xf7, 0xdf, 0x3e, 0xd6}} +IXAudio2Extension :: struct #raw_union { + #subtype iunknown: IUnknown, + using ixaudio2extension_vtable: ^IXAudio2Extension_VTable, +} +IXAudio2Extension_VTable :: struct { + using iunknown_vtable: IUnknown_VTable, + + // NAME: IXAudio2Extension.GetProcessingQuantum + // DESCRIPTION: Returns the processing quantum + // quantumMilliseconds = (1000.0f * quantumNumerator / quantumDenominator) + // ARGUMENTS: + // quantumNumerator - Quantum numerator + // quantumDenominator - Quantum denominator + GetProcessingQuantum: proc "system" (this: ^IXAudio2Extension, quantumNumerator: ^u32, quantumDenominator: ^u32), + + // NAME: IXAudio2Extension.GetProcessor + // DESCRIPTION: Returns the number of the processor used by XAudio2 + // ARGUMENTS: + // processor - Non-zero Processor number + GetProcessor: proc "system" (this: ^IXAudio2Extension, processor: ^PROCESSOR_FLAGS), +} + +/************************************************************************** + * + * IXAudio2Voice: Base voice management interface. + * + **************************************************************************/ + +IXAudio2Voice :: struct { + using ixaudio2voice_vtable: ^IXAudio2Voice_VTable, +} +IXAudio2Voice_VTable :: struct { + // NAME: IXAudio2Voice.GetVoiceDetails + // DESCRIPTION: Returns the basic characteristics of this voice. + // ARGUMENTS: + // pVoiceDetails - Returns the voice's details. + GetVoiceDetails: proc "system" (this: ^IXAudio2Voice, pVoiceDetails: ^VOICE_DETAILS), + + // NAME: IXAudio2Voice.SetOutputVoices + // DESCRIPTION: Replaces the set of submix/mastering voices that receive + // this voice's output. + // ARGUMENTS: + // pSendList - Optional list of voices this voice should send audio to. + SetOutputVoices: proc "system" (this: ^IXAudio2Voice, pSendList: [^]VOICE_SENDS) -> HRESULT, + + // NAME: IXAudio2Voice.SetEffectChain + // DESCRIPTION: Replaces this voice's current effect chain with a new one. + // ARGUMENTS: + // pEffectChain - Structure describing the new effect chain to be used. + SetEffectChain: proc "system" (this: ^IXAudio2Voice, pEffectChain: ^EFFECT_CHAIN) -> HRESULT, + + // NAME: IXAudio2Voice.EnableEffect + // DESCRIPTION: Enables an effect in this voice's effect chain. + // ARGUMENTS: + // EffectIndex - Index of an effect within this voice's effect chain. + // OperationSet - Used to identify this call as part of a deferred batch. + EnableEffect: proc "system" (this: ^IXAudio2Voice, EffectIndex: u32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.DisableEffect + // DESCRIPTION: Disables an effect in this voice's effect chain. + // ARGUMENTS: + // EffectIndex - Index of an effect within this voice's effect chain. + // OperationSet - Used to identify this call as part of a deferred batch. + DisableEffect: proc "system" (this: ^IXAudio2Voice, EffectIndex: u32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetEffectState + // DESCRIPTION: Returns the running state of an effect. + // ARGUMENTS: + // EffectIndex - Index of an effect within this voice's effect chain. + // pEnabled - Returns the enabled/disabled state of the given effect. + GetEffectState: proc "system" (this: ^IXAudio2Voice, EffectIndex: u32, pEnabled: ^b32), + + // NAME: IXAudio2Voice.SetEffectParameters + // DESCRIPTION: Sets effect-specific parameters. + // REMARKS: Unlike IXAPOParameters.SetParameters, this method may be called from any thread. XAudio2 implements appropriate synchronization to copy the parameters to the realtime audio processing thread. + // ARGUMENTS: + // EffectIndex - Index of an effect within this voice's effect chain. + // pParameters - Pointer to an effect-specific parameters block. + // ParametersByteSize - Size of the pParameters array in bytes. + // OperationSet - Used to identify this call as part of a deferred batch. + SetEffectParameters: proc "system" (this: ^IXAudio2Voice, EffectIndex: u32, pParameters: rawptr, ParametersByteSize: u32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetEffectParameters + // DESCRIPTION: Obtains the current effect-specific parameters. + // ARGUMENTS: + // EffectIndex - Index of an effect within this voice's effect chain. + // pParameters - Returns the current values of the effect-specific parameters. + // ParametersByteSize - Size of the pParameters array in bytes. + GetEffectParameters: proc "system" (this: ^IXAudio2Voice, EffectIndex: u32, pParameters: rawptr, ParametersByteSize: u32) -> HRESULT, + + // NAME: IXAudio2Voice.SetFilterParameters + // DESCRIPTION: Sets this voice's filter parameters. + // ARGUMENTS: + // pParameters - Pointer to the filter's parameter structure. + // OperationSet - Used to identify this call as part of a deferred batch. + SetFilterParameters: proc "system" (this: ^IXAudio2Voice, pParameters: ^FILTER_PARAMETERS, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetFilterParameters + // DESCRIPTION: Returns this voice's current filter parameters. + // ARGUMENTS: + // pParameters - Returns the filter parameters. + GetFilterParameters: proc "system" (this: ^IXAudio2Voice, pParameters: ^FILTER_PARAMETERS), + + // NAME: IXAudio2Voice.SetOutputFilterParameters + // DESCRIPTION: Sets the filter parameters on one of this voice's sends. + // ARGUMENTS: + // pDestinationVoice - Destination voice of the send whose filter parameters will be set. + // pParameters - Pointer to the filter's parameter structure. + // OperationSet - Used to identify this call as part of a deferred batch. + SetOutputFilterParameters: proc "system" (this: ^IXAudio2Voice, pDestinationVoice: ^IXAudio2Voice, pParameters: ^FILTER_PARAMETERS, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetOutputFilterParameters + // DESCRIPTION: Returns the filter parameters from one of this voice's sends. + // ARGUMENTS: + // pDestinationVoice - Destination voice of the send whose filter parameters will be read. + // pParameters - Returns the filter parameters. + GetOutputFilterParameters: proc "system" (this: ^IXAudio2Voice, pDestinationVoice: ^IXAudio2Voice, pParameters: ^FILTER_PARAMETERS), + + // NAME: IXAudio2Voice.SetVolume + // DESCRIPTION: Sets this voice's overall volume level. + // ARGUMENTS: + // Volume - New overall volume level to be used, as an amplitude factor. + // OperationSet - Used to identify this call as part of a deferred batch. + SetVolume: proc "system" (this: ^IXAudio2Voice, Volume: f32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetVolume + // DESCRIPTION: Obtains this voice's current overall volume level. + // ARGUMENTS: + // pVolume: Returns the voice's current overall volume level. + GetVolume: proc "system" (this: ^IXAudio2Voice, pVolume: ^f32), + + // NAME: IXAudio2Voice.SetChannelVolumes + // DESCRIPTION: Sets this voice's per-channel volume levels. + // ARGUMENTS: + // Channels - Used to confirm the voice's channel count. + // pVolumes - Array of per-channel volume levels to be used. + // OperationSet - Used to identify this call as part of a deferred batch. + SetChannelVolumes: proc "system" (this: ^IXAudio2Voice, Channels: u32, pVolumes: [^]f32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetChannelVolumes + // DESCRIPTION: Returns this voice's current per-channel volume levels. + // ARGUMENTS: + // Channels - Used to confirm the voice's channel count. + // pVolumes - Returns an array of the current per-channel volume levels. + GetChannelVolumes: proc "system" (this: ^IXAudio2Voice, Channels: u32, pVolumes: [^]f32), + + // NAME: IXAudio2Voice.SetOutputMatrix + // DESCRIPTION: Sets the volume levels used to mix from each channel of this voice's output audio to each channel of a given destination voice's input audio. + // ARGUMENTS: + // pDestinationVoice - The destination voice whose mix matrix to change. + // SourceChannels - Used to confirm this voice's output channel count (the number of channels produced by the last effect in the chain). + // DestinationChannels - Confirms the destination voice's input channels. + // pLevelMatrix - Array of [SourceChannels * DestinationChannels] send levels. The level used to send from source channel S to destination channel D should be in pLevelMatrix[S + SourceChannels * D]. + // OperationSet - Used to identify this call as part of a deferred batch. + SetOutputMatrix: proc "system" (this: ^IXAudio2Voice, pDestinationVoice: ^IXAudio2Voice, SourceChannels: u32, DestinationChannels: u32, pLevelMatrix: [^]f32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2Voice.GetOutputMatrix + // DESCRIPTION: Obtains the volume levels used to send each channel of this voice's output audio to each channel of a given destination voice's input audio. + // ARGUMENTS: + // pDestinationVoice - The destination voice whose mix matrix to obtain. + // SourceChannels - Used to confirm this voice's output channel count (the number of channels produced by the last effect in the chain). + // DestinationChannels - Confirms the destination voice's input channels. + // pLevelMatrix - Array of send levels, as above. + GetOutputMatrix: proc "system" (this: ^IXAudio2Voice, pDestinationVoice: ^IXAudio2Voice, SourceChannels: u32, DestinationChannels: u32, pLevelMatrix: [^]f32), + + // NAME: IXAudio2Voice.DestroyVoice + // DESCRIPTION: Destroys this voice, stopping it if necessary and removing it from the XAudio2 graph. + DestroyVoice: proc "system" (this: ^IXAudio2Voice), +} + +/************************************************************************** + * + * IXAudio2SourceVoice: Source voice management interface. + * + **************************************************************************/ + +IXAudio2SourceVoice :: struct #raw_union { + #subtype ixaudio2voice: IXAudio2Voice, + using ixaudio2sourcevoice_vtable: ^IXAudio2SourceVoice_VTable, +} +IXAudio2SourceVoice_VTable :: struct { + using ixaudio2voice_vtable: IXAudio2Voice_VTable, + + // NAME: IXAudio2SourceVoice.Start + // DESCRIPTION: Makes this voice start consuming and processing audio. + // ARGUMENTS: + // Flags - Flags controlling how the voice should be started. + // OperationSet - Used to identify this call as part of a deferred batch. + Start: proc "system" (this: ^IXAudio2SourceVoice, Flags: FLAGS = {}, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2SourceVoice.Stop + // DESCRIPTION: Makes this voice stop consuming audio. + // ARGUMENTS: + // Flags - Flags controlling how the voice should be stopped. + // OperationSet - Used to identify this call as part of a deferred batch. + Stop: proc "system" (this: ^IXAudio2SourceVoice, Flags: FLAGS = {}, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2SourceVoice.SubmitSourceBuffer + // DESCRIPTION: Adds a new audio buffer to this voice's input queue. + // ARGUMENTS: + // pBuffer - Pointer to the buffer structure to be queued. + // pBufferWMA - Additional structure used only when submitting XWMA data. + SubmitSourceBuffer: proc "system" (this: ^IXAudio2SourceVoice, pBuffer: ^BUFFER, pBufferWMA: ^BUFFER_WMA = nil) -> HRESULT, + + // NAME: IXAudio2SourceVoice.FlushSourceBuffers + // DESCRIPTION: Removes all pending audio buffers from this voice's queue. + FlushSourceBuffers: proc "system" (this: ^IXAudio2SourceVoice) -> HRESULT, + + // NAME: IXAudio2SourceVoice.Discontinuity + // DESCRIPTION: Notifies the voice of an intentional break in the stream of audio buffers (e.g. the end of a sound), to prevent XAudio2 from interpreting an empty buffer queue as a glitch. + Discontinuity: proc "system" (this: ^IXAudio2SourceVoice) -> HRESULT, + + // NAME: IXAudio2SourceVoice.ExitLoop + // DESCRIPTION: Breaks out of the current loop when its end is reached. + // ARGUMENTS: + // OperationSet - Used to identify this call as part of a deferred batch. + ExitLoop: proc "system" (this: ^IXAudio2SourceVoice, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2SourceVoice.GetState + // DESCRIPTION: Returns the number of buffers currently queued on this voice, the pContext value associated with the currently processing buffer (if any), and other voice state information. + // ARGUMENTS: + // pVoiceState - Returns the state information. + // Flags - Flags controlling what voice state is returned. + GetState: proc "system" (this: ^IXAudio2SourceVoice, pVoiceState: ^VOICE_STATE, Flags: FLAGS = {}), + + // NAME: IXAudio2SourceVoice.SetFrequencyRatio + // DESCRIPTION: Sets this voice's frequency adjustment, i.e. its pitch. + // ARGUMENTS: + // Ratio - Frequency change, expressed as source frequency / target frequency. + // OperationSet - Used to identify this call as part of a deferred batch. + SetFrequencyRatio: proc "system" (this: ^IXAudio2SourceVoice, Ratio: f32, OperationSet: u32 = COMMIT_NOW) -> HRESULT, + + // NAME: IXAudio2SourceVoice.GetFrequencyRatio + // DESCRIPTION: Returns this voice's current frequency adjustment ratio. + // ARGUMENTS: + // pRatio - Returns the frequency adjustment. + GetFrequencyRatio: proc "system" (this: ^IXAudio2SourceVoice, pRatio: ^f32), + + // NAME: IXAudio2SourceVoice.SetSourceSampleRate + // DESCRIPTION: Reconfigures this voice to treat its source data as being at a different sample rate than the original one specified in CreateSourceVoice's pSourceFormat argument. + // ARGUMENTS: + // UINT32 - The intended sample rate of further submitted source data. + SetSourceSampleRate: proc "system" (this: ^IXAudio2SourceVoice, NewSourceSampleRate: u32) -> HRESULT, +} + +/************************************************************************** + * + * IXAudio2SubmixVoice: Submixing voice management interface. + * + **************************************************************************/ + +IXAudio2SubmixVoice :: struct #raw_union { + #subtype ixaudio2voice: IXAudio2Voice, + using ixaudio2submixvoice_vtable: ^IXAudio2SubmixVoice_VTable, +} +IXAudio2SubmixVoice_VTable :: struct { + using ixaudio2voice_vtable: IXAudio2Voice_VTable, + // There are currently no methods specific to submix voices. +} + + /************************************************************************** + * + * IXAudio2MasteringVoice: Mastering voice management interface. + * + **************************************************************************/ + +IXAudio2MasteringVoice :: struct #raw_union { + #subtype ixaudio2voice: IXAudio2Voice, + using ixaudio2masteringvoice_vtable: ^IXAudio2MasteringVoice_VTable, +} +IXAudio2MasteringVoice_VTable :: struct { + using ixaudio2voice_vtable: IXAudio2Voice_VTable, + + // NAME: IXAudio2MasteringVoice.GetChannelMask + // DESCRIPTION: Returns the channel mask for this voice + // ARGUMENTS: + // pChannelMask - returns the channel mask for this voice. This corresponds to the dwChannelMask member of WAVEFORMATEXTENSIBLE. + GetChannelMask: proc "system" (this: ^IXAudio2MasteringVoice, pChannelmask: ^win.DWORD) -> HRESULT, +} + +/************************************************************************** + * + * IXAudio2EngineCallback: Client notification interface for engine events. + * + * REMARKS: Contains methods to notify the client when certain events happen in the XAudio2 engine. This interface should be implemented by the client. + * XAudio2 will call these methods via the interface pointer provided by the client when it calls IXAudio2.RegisterForCallbacks. + * + **************************************************************************/ + +IXAudio2EngineCallback :: struct { + using ixaudio2enginecallback_vtable: ^IXAudio2EngineCallback_VTable, +} +IXAudio2EngineCallback_VTable :: struct { + // Called by XAudio2 just before an audio processing pass begins. + OnProcessingPassStart: proc "system" (this: ^IXAudio2EngineCallback), + + // Called just after an audio processing pass ends. + OnProcessingPassEnd: proc "system" (this: ^IXAudio2EngineCallback), + + // Called in the event of a critical system error which requires XAudio2 to be closed down and restarted. The error code is given in Error. + OnCriticalError: proc "system" (this: ^IXAudio2EngineCallback, Error: HRESULT), +} + + /************************************************************************** + * + * IXAudio2VoiceCallback: Client notification interface for voice events. + * + * REMARKS: Contains methods to notify the client when certain events happen in an XAudio2 voice. This interface should be implemented by the client. + * XAudio2 will call these methods via an interface pointer provided by the client in the IXAudio2.CreateSourceVoice call. + * + **************************************************************************/ + +IXAudio2VoiceCallback :: struct { + using ixaudio2voicecallback_vtable: ^IXAudio2VoiceCallback_VTable, +} +IXAudio2VoiceCallback_VTable :: struct { + // Called just before this voice's processing pass begins. + OnVoiceProcessingPassStart: proc "system" (this: ^IXAudio2VoiceCallback, BytesRequired: u32), + + // Called just after this voice's processing pass ends. + OnVoiceProcessingPassEnd: proc "system" (this: ^IXAudio2VoiceCallback), + + // Called when this voice has just finished playing a buffer stream (as marked with the END_OF_STREAM flag on the last buffer). + OnStreamEnd: proc "system" (this: ^IXAudio2VoiceCallback), + + // Called when this voice is about to start processing a new buffer. + OnBufferStart: proc "system" (this: ^IXAudio2VoiceCallback, pBufferContext: rawptr), + + // Called when this voice has just finished processing a buffer. + // The buffer can now be reused or destroyed. + OnBufferEnd: proc "system" (this: ^IXAudio2VoiceCallback, pBufferContext: rawptr), + + // Called when this voice has just reached the end position of a loop. + OnLoopEnd: proc "system" (this: ^IXAudio2VoiceCallback, pBufferContext: rawptr), + + // Called in the event of a critical error during voice processing, such as a failing xAPO or an error from the hardware XMA decoder. + // The voice may have to be destroyed and re-created to recover from the error. + // The callback arguments report which buffer was being processed when the error occurred, and its HRESULT code. + OnVoiceError: proc "system" (this: ^IXAudio2VoiceCallback, pBufferContext: rawptr, Error: HRESULT), +} + +/************************************************************************** + * + * XAudio2Create: Top-level function that creates an XAudio2 instance. + * + * ARGUMENTS: + * + * Flags - Flags specifying the XAudio2 object's behavior. + * + * XAudio2Processor - A PROCESSOR_FLAGS value that specifies the hardware threads (Xbox) or processors (Windows) that XAudio2 will use. + * Note that XAudio2 supports concurrent processing on multiple threads, using any combination of PROCESSOR_FLAGS flags. + * The values are platform-specific; platform-independent code can use USE_DEFAULT_PROCESSOR to use the default on each platform. + * + **************************************************************************/ + +Create :: proc "stdcall" (ppXAudio2: ^^IXAudio2, Flags: FLAGS = {}, XAudio2Processor: PROCESSOR_FLAGS = {.Processor1}) -> HRESULT { + CreateWithVersionInfoFunc :: #type proc "c" (a0: ^^IXAudio2, a1: FLAGS, a2: PROCESSOR_FLAGS, a3: win.DWORD) -> HRESULT + CreateInfoFunc :: #type proc "c" (a0: ^^IXAudio2, a1: FLAGS, a2: PROCESSOR_FLAGS) -> HRESULT + + dll_Instance: win.HMODULE + create_with_version_info: CreateWithVersionInfoFunc + create_info: CreateInfoFunc + + if dll_Instance == nil { + dll_Instance = win.LoadLibraryExW(win.L("xaudio2_9.dll"), nil, {.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS}) + if dll_Instance == nil { + return HRESULT(win.GetLastError()) + } + create_with_version_info = cast(CreateWithVersionInfoFunc)win.GetProcAddress(dll_Instance, "XAudio2CreateWithVersionInfo") + if create_with_version_info == nil { + create_info = cast(CreateInfoFunc)win.GetProcAddress(dll_Instance, "XAudio2Create") + if create_info == nil { + return HRESULT(win.GetLastError()) + } + } + } + if create_with_version_info != nil { + return create_with_version_info(ppXAudio2, Flags, XAudio2Processor, 0x0A000010) + } + return create_info(ppXAudio2, Flags, XAudio2Processor) +} + +/************************************************************************** + * + * Utility functions used to convert from pitch in semitones and volume in decibels to the frequency and amplitude ratio units used by XAudio2. + * + **************************************************************************/ + +// Calculate the argument to SetVolume from a decibel value +DecibelsToAmplitudeRatio :: proc "contextless" (Decibels: f32) -> f32 { + return math.pow_f32(10.0, Decibels / 20.0) +} + +// Recover a volume in decibels from an amplitude factor +AmplitudeRatioToDecibels :: proc "contextless" (Volume: f32) -> f32 { + if Volume == 0 { + return min(f32) + } + return 20.0 * math.log10_f32(Volume) +} + +// Calculate the argument to SetFrequencyRatio from a semitone value +SemitonesToFrequencyRatio :: proc "contextless" (Semitones: f32) -> f32 { + // FrequencyRatio = 2 ^ Octaves + // = 2 ^ (Semitones / 12) + return math.pow_f32(2.0, Semitones / 12.0) +} + +// Recover a pitch in semitones from a frequency ratio +FrequencyRatioToSemitones :: proc "contextless" (FrequencyRatio: f32) -> f32 { + // Semitones = 12 * log2(FrequencyRatio) + // = 12 * log2(10) * log10(FrequencyRatio) + return 12.0 * math.log2_f32(FrequencyRatio) +} + +// Convert from filter cutoff frequencies expressed in Hertz to the radian frequency values used in FILTER_PARAMETERS.Frequency, state-variable filter types only. +// Use CutoffFrequencyToOnePoleCoefficient() for one-pole filter types. +// Note that the highest CutoffFrequency supported is SampleRate/6. +// Higher values of CutoffFrequency will return MAX_FILTER_FREQUENCY. +CutoffFrequencyToRadians :: proc "contextless" (CutoffFrequency: f32, SampleRate: u32) -> f32 { + if u32(CutoffFrequency * 6.0) >= SampleRate { + return MAX_FILTER_FREQUENCY + } + return 2.0 * math.sin_f32(math.PI * CutoffFrequency / f32(SampleRate)) +} + +// Convert from radian frequencies back to absolute frequencies in Hertz +RadiansToCutoffFrequency :: proc "contextless" (Radians: f32, SampleRate: f32) -> f32 { + return SampleRate * math.asin_f32(Radians / 2.0) / math.PI +} + +// Convert from filter cutoff frequencies expressed in Hertz to the filter coefficients used with FILTER_PARAMETERS.Frequency, +// LowPassOnePoleFilter and HighPassOnePoleFilter filter types only. +// Use CutoffFrequencyToRadians() for state-variable filter types. +CutoffFrequencyToOnePoleCoefficient :: proc "contextless" (CutoffFrequency: f32, SampleRate: u32) -> f32 { + if u32(CutoffFrequency) >= SampleRate { + return MAX_FILTER_FREQUENCY + } + return 1.0 - math.pow_f32(1.0 - 2.0 * CutoffFrequency / f32(SampleRate), 2.0) +} + +//------------------------------------------------------------------------- +// Description: Audio stream categories +// +// Other - All other streams (default) +// ForegroundOnlyMedia - (deprecated for Win10) Music, Streaming audio +// BackgroundCapableMedia - (deprecated for Win10) Video with audio +// Communications - VOIP, chat, phone call +// Alerts - Alarm, Ring tones +// SoundEffects - Sound effects, clicks, dings +// GameEffects - Game sound effects +// GameMedia - Background audio for games +// GameChat - In game player chat +// Speech - Speech recognition +// Media - Music, Streaming audio +// Movie - Video with audio +// FarFieldSpeech - Capture of far field speech +// UniformSpeech - Uniform, device agnostic speech processing +// VoiceTyping - Dictation, typing by voice +// +AUDIO_STREAM_CATEGORY :: enum i32 { + Other = 0, + //ForegroundOnlyMedia = 1, + //BackgroundCapableMedia = 2, + Communications = 3, + Alerts = 4, + SoundEffects = 5, + GameEffects = 6, + GameMedia = 7, + GameChat = 8, + Speech = 9, + Movie = 10, + Media = 11, + FarFieldSpeech = 12, + UniformSpeech = 13, + VoiceTyping = 14, +} diff --git a/vendor/windows/XAudio2/xaudio2fx.odin b/vendor/windows/XAudio2/xaudio2fx.odin new file mode 100644 index 000000000..1449ed4ea --- /dev/null +++ b/vendor/windows/XAudio2/xaudio2fx.odin @@ -0,0 +1,282 @@ +#+build windows + +package windows_xaudio2 + +import "core:math" + +foreign import xa2 "system:xaudio2.lib" + +/************************************************************************** + * + * Effect creation functions. + * + * On Xbox the application can link with the debug library to use the debug + * functionality. + * + **************************************************************************/ + +@(default_calling_convention="system") +foreign xa2 { + CreateAudioVolumeMeter :: proc(ppApo: ^^IUnknown) -> HRESULT --- + CreateAudioReverb :: proc(ppApo: ^^IUnknown) -> HRESULT --- +} + +/************************************************************************** + * + * Volume meter parameters. + * The volume meter supports f32 audio formats and must be used in-place. + * + **************************************************************************/ + +// VOLUMEMETER_LEVELS: Receives results from GetEffectParameters(). +// The user is responsible for allocating pPeakLevels, pRMSLevels, and initializing ChannelCount accordingly. +// The volume meter does not support SetEffectParameters(). +VOLUMEMETER_LEVELS :: struct #packed { + pPeakLevels: [^]f32 `fmt:"v,ChannelCount"`, // Peak levels table: receives maximum absolute level for each channel over a processing pass, may be NULL if pRMSLevls != NULL, otherwise must have at least ChannelCount elements. + pRMSLevels: [^]f32 `fmt:"v,ChannelCount"`, // Root mean square levels table: receives RMS level for each channel over a processing pass, may be NULL if pPeakLevels != NULL, otherwise must have at least ChannelCount elements. + ChannelCount: u32, // Number of channels being processed by the volume meter APO +} + +/************************************************************************** + * + * Reverb parameters. + * The reverb supports only f32 audio with the following channel configurations: + * Input: Mono Output: Mono + * Input: Mono Output: 5.1 + * Input: Stereo Output: Stereo + * Input: Stereo Output: 5.1 + * The framerate must be within [20000, 48000] Hz. + * + * When using mono input, delay filters associated with the right channel are not executed. + * In this case, parameters such as PositionRight and PositionMatrixRight have no effect. + * This also means the reverb uses less CPU when hosted in a mono submix. + * + **************************************************************************/ + +REVERB_MIN_FRAMERATE :: 20000 +REVERB_MAX_FRAMERATE :: 48000 + +// REVERB_PARAMETERS: Native parameter set for the reverb effect + +REVERB_PARAMETERS :: struct #packed { + // ratio of wet (processed) signal to dry (original) signal + WetDryMix: f32, // [0, 100] (percentage) + // Delay times + ReflectionsDelay: u32, // [0, 300] in ms + ReverbDelay: byte, // [0, 85] in ms + RearDelay: byte, // 7.1: [0, 20] in ms, all other: [0, 5] in ms + SideDelay: byte, // 7.1: [0, 5] in ms, all other: not used, but still validated + // Indexed parameters + PositionLeft: byte, // [0, 30] no units + PositionRight: byte, // [0, 30] no units, ignored when configured to mono + PositionMatrixLeft: byte, // [0, 30] no units + PositionMatrixRight: byte, // [0, 30] no units, ignored when configured to mono + EarlyDiffusion: byte, // [0, 15] no units + LateDiffusion: byte, // [0, 15] no units + LowEQGain: byte, // [0, 12] no units + LowEQCutoff: byte, // [0, 9] no units + HighEQGain: byte, // [0, 8] no units + HighEQCutoff: byte, // [0, 14] no units + // Direct parameters + RoomFilterFreq: f32, // [20, 20000] in Hz + RoomFilterMain: f32, // [-100, 0] in dB + RoomFilterHF: f32, // [-100, 0] in dB + ReflectionsGain: f32, // [-100, 20] in dB + ReverbGain: f32, // [-100, 20] in dB + DecayTime: f32, // [0.1, inf] in seconds + Density: f32, // [0, 100] (percentage) + RoomSize: f32, // [1, 100] in feet + // component control + DisableLateField: b32, // TRUE to disable late field reflections +} + +// Maximum, minimum and default values for the parameters above +REVERB_MIN_WET_DRY_MIX :: 0.0 +REVERB_MIN_REFLECTIONS_DELAY :: 0 +REVERB_MIN_REVERB_DELAY :: 0 +REVERB_MIN_REAR_DELAY :: 0 +REVERB_MIN_7POINT1_SIDE_DELAY :: 0 +REVERB_MIN_7POINT1_REAR_DELAY :: 0 +REVERB_MIN_POSITION :: 0 +REVERB_MIN_DIFFUSION :: 0 +REVERB_MIN_LOW_EQ_GAIN :: 0 +REVERB_MIN_LOW_EQ_CUTOFF :: 0 +REVERB_MIN_HIGH_EQ_GAIN :: 0 +REVERB_MIN_HIGH_EQ_CUTOFF :: 0 +REVERB_MIN_ROOM_FILTER_FREQ :: 20.0 +REVERB_MIN_ROOM_FILTER_MAIN :: -100.0 +REVERB_MIN_ROOM_FILTER_HF :: -100.0 +REVERB_MIN_REFLECTIONS_GAIN :: -100.0 +REVERB_MIN_REVERB_GAIN :: -100.0 +REVERB_MIN_DECAY_TIME :: 0.1 +REVERB_MIN_DENSITY :: 0.0 +REVERB_MIN_ROOM_SIZE :: 0.0 + +REVERB_MAX_WET_DRY_MIX :: 100.0 +REVERB_MAX_REFLECTIONS_DELAY :: 300 +REVERB_MAX_REVERB_DELAY :: 85 +REVERB_MAX_REAR_DELAY :: 5 +REVERB_MAX_7POINT1_SIDE_DELAY :: 5 +REVERB_MAX_7POINT1_REAR_DELAY :: 20 +REVERB_MAX_POSITION :: 30 +REVERB_MAX_DIFFUSION :: 15 +REVERB_MAX_LOW_EQ_GAIN :: 12 +REVERB_MAX_LOW_EQ_CUTOFF :: 9 +REVERB_MAX_HIGH_EQ_GAIN :: 8 +REVERB_MAX_HIGH_EQ_CUTOFF :: 14 +REVERB_MAX_ROOM_FILTER_FREQ :: 20000.0 +REVERB_MAX_ROOM_FILTER_MAIN :: 0.0 +REVERB_MAX_ROOM_FILTER_HF :: 0.0 +REVERB_MAX_REFLECTIONS_GAIN :: 20.0 +REVERB_MAX_REVERB_GAIN :: 20.0 +REVERB_MAX_DENSITY :: 100.0 +REVERB_MAX_ROOM_SIZE :: 100.0 + +REVERB_DEFAULT_WET_DRY_MIX :: 100.0 +REVERB_DEFAULT_REFLECTIONS_DELAY :: 5 +REVERB_DEFAULT_REVERB_DELAY :: 5 +REVERB_DEFAULT_REAR_DELAY :: 5 +REVERB_DEFAULT_7POINT1_SIDE_DELAY :: 5 +REVERB_DEFAULT_7POINT1_REAR_DELAY :: 20 +REVERB_DEFAULT_POSITION :: 6 +REVERB_DEFAULT_POSITION_MATRIX :: 27 +REVERB_DEFAULT_EARLY_DIFFUSION :: 8 +REVERB_DEFAULT_LATE_DIFFUSION :: 8 +REVERB_DEFAULT_LOW_EQ_GAIN :: 8 +REVERB_DEFAULT_LOW_EQ_CUTOFF :: 4 +REVERB_DEFAULT_HIGH_EQ_GAIN :: 8 +REVERB_DEFAULT_HIGH_EQ_CUTOFF :: 4 +REVERB_DEFAULT_ROOM_FILTER_FREQ :: 5000.0 +REVERB_DEFAULT_ROOM_FILTER_MAIN :: 0.0 +REVERB_DEFAULT_ROOM_FILTER_HF :: 0.0 +REVERB_DEFAULT_REFLECTIONS_GAIN :: 0.0 +REVERB_DEFAULT_REVERB_GAIN :: 0.0 +REVERB_DEFAULT_DECAY_TIME :: 1.0 +REVERB_DEFAULT_DENSITY :: 100.0 +REVERB_DEFAULT_ROOM_SIZE :: 100.0 + +REVERB_DEFAULT_DISABLE_LATE_FIELD: b32 : false + +// REVERB_I3DL2_PARAMETERS: Parameter set compliant with the I3DL2 standard + +REVERB_I3DL2_PARAMETERS :: struct #packed { + // ratio of wet (processed) signal to dry (original) signal + WetDryMix: f32, // [0, 100] (percentage) + + // Standard I3DL2 parameters + Room: i32, // [-10000, 0] in mB (hundredths of decibels) + RoomHF: i32, // [-10000, 0] in mB (hundredths of decibels) + RoomRolloffFactor: f32, // [0.0, 10.0] + DecayTime: f32, // [0.1, 20.0] in seconds + DecayHFRatio: f32, // [0.1, 2.0] + Reflections: i32, // [-10000, 1000] in mB (hundredths of decibels) + ReflectionsDelay: f32, // [0.0, 0.3] in seconds + Reverb: i32, // [-10000, 2000] in mB (hundredths of decibels) + ReverbDelay: f32, // [0.0, 0.1] in seconds + Diffusion: f32, // [0.0, 100.0] (percentage) + Density: f32, // [0.0, 100.0] (percentage) + HFReference: f32, // [20.0, 20000.0] in Hz +} + +/************************************************************************** + * + * Standard I3DL2 reverb presets (100% wet). + * + **************************************************************************/ + +I3DL2_PRESET_DEFAULT := REVERB_I3DL2_PARAMETERS{100.0,-10000, 0,0.0, 1.00,0.50,-10000,0.020,-10000,0.040,100.0,100.0,5000.0} +I3DL2_PRESET_GENERIC := REVERB_I3DL2_PARAMETERS{100.0, -1000, -100,0.0, 1.49,0.83, -2602,0.007, 200,0.011,100.0,100.0,5000.0} +I3DL2_PRESET_PADDEDCELL := REVERB_I3DL2_PARAMETERS{100.0, -1000,-6000,0.0, 0.17,0.10, -1204,0.001, 207,0.002,100.0,100.0,5000.0} +I3DL2_PRESET_ROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000, -454,0.0, 0.40,0.83, -1646,0.002, 53,0.003,100.0,100.0,5000.0} +I3DL2_PRESET_BATHROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000,-1200,0.0, 1.49,0.54, -370,0.007, 1030,0.011,100.0, 60.0,5000.0} +I3DL2_PRESET_LIVINGROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000,-6000,0.0, 0.50,0.10, -1376,0.003, -1104,0.004,100.0,100.0,5000.0} +I3DL2_PRESET_STONEROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000, -300,0.0, 2.31,0.64, -711,0.012, 83,0.017,100.0,100.0,5000.0} +I3DL2_PRESET_AUDITORIUM := REVERB_I3DL2_PARAMETERS{100.0, -1000, -476,0.0, 4.32,0.59, -789,0.020, -289,0.030,100.0,100.0,5000.0} +I3DL2_PRESET_CONCERTHALL := REVERB_I3DL2_PARAMETERS{100.0, -1000, -500,0.0, 3.92,0.70, -1230,0.020, -2,0.029,100.0,100.0,5000.0} +I3DL2_PRESET_CAVE := REVERB_I3DL2_PARAMETERS{100.0, -1000, 0,0.0, 2.91,1.30, -602,0.015, -302,0.022,100.0,100.0,5000.0} +I3DL2_PRESET_ARENA := REVERB_I3DL2_PARAMETERS{100.0, -1000, -698,0.0, 7.24,0.33, -1166,0.020, 16,0.030,100.0,100.0,5000.0} +I3DL2_PRESET_HANGAR := REVERB_I3DL2_PARAMETERS{100.0, -1000,-1000,0.0,10.05,0.23, -602,0.020, 198,0.030,100.0,100.0,5000.0} +I3DL2_PRESET_CARPETEDHALLWAY := REVERB_I3DL2_PARAMETERS{100.0, -1000,-4000,0.0, 0.30,0.10, -1831,0.002, -1630,0.030,100.0,100.0,5000.0} +I3DL2_PRESET_HALLWAY := REVERB_I3DL2_PARAMETERS{100.0, -1000, -300,0.0, 1.49,0.59, -1219,0.007, 441,0.011,100.0,100.0,5000.0} +I3DL2_PRESET_STONECORRIDOR := REVERB_I3DL2_PARAMETERS{100.0, -1000, -237,0.0, 2.70,0.79, -1214,0.013, 395,0.020,100.0,100.0,5000.0} +I3DL2_PRESET_ALLEY := REVERB_I3DL2_PARAMETERS{100.0, -1000, -270,0.0, 1.49,0.86, -1204,0.007, -4,0.011,100.0,100.0,5000.0} +I3DL2_PRESET_FOREST := REVERB_I3DL2_PARAMETERS{100.0, -1000,-3300,0.0, 1.49,0.54, -2560,0.162, -613,0.088, 79.0,100.0,5000.0} +I3DL2_PRESET_CITY := REVERB_I3DL2_PARAMETERS{100.0, -1000, -800,0.0, 1.49,0.67, -2273,0.007, -2217,0.011, 50.0,100.0,5000.0} +I3DL2_PRESET_MOUNTAINS := REVERB_I3DL2_PARAMETERS{100.0, -1000,-2500,0.0, 1.49,0.21, -2780,0.300, -2014,0.100, 27.0,100.0,5000.0} +I3DL2_PRESET_QUARRY := REVERB_I3DL2_PARAMETERS{100.0, -1000,-1000,0.0, 1.49,0.83,-10000,0.061, 500,0.025,100.0,100.0,5000.0} +I3DL2_PRESET_PLAIN := REVERB_I3DL2_PARAMETERS{100.0, -1000,-2000,0.0, 1.49,0.50, -2466,0.179, -2514,0.100, 21.0,100.0,5000.0} +I3DL2_PRESET_PARKINGLOT := REVERB_I3DL2_PARAMETERS{100.0, -1000, 0,0.0, 1.65,1.50, -1363,0.008, -1153,0.012,100.0,100.0,5000.0} +I3DL2_PRESET_SEWERPIPE := REVERB_I3DL2_PARAMETERS{100.0, -1000,-1000,0.0, 2.81,0.14, 429,0.014, 648,0.021, 80.0, 60.0,5000.0} +I3DL2_PRESET_UNDERWATER := REVERB_I3DL2_PARAMETERS{100.0, -1000,-4000,0.0, 1.49,0.10, -449,0.007, 1700,0.011,100.0,100.0,5000.0} +I3DL2_PRESET_SMALLROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000, -600,0.0, 1.10,0.83, -400,0.005, 500,0.010,100.0,100.0,5000.0} +I3DL2_PRESET_MEDIUMROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000, -600,0.0, 1.30,0.83, -1000,0.010, -200,0.020,100.0,100.0,5000.0} +I3DL2_PRESET_LARGEROOM := REVERB_I3DL2_PARAMETERS{100.0, -1000, -600,0.0, 1.50,0.83, -1600,0.020, -1000,0.040,100.0,100.0,5000.0} +I3DL2_PRESET_MEDIUMHALL := REVERB_I3DL2_PARAMETERS{100.0, -1000, -600,0.0, 1.80,0.70, -1300,0.015, -800,0.030,100.0,100.0,5000.0} +I3DL2_PRESET_LARGEHALL := REVERB_I3DL2_PARAMETERS{100.0, -1000, -600,0.0, 1.80,0.70, -2000,0.030, -1400,0.060,100.0,100.0,5000.0} +I3DL2_PRESET_PLATE := REVERB_I3DL2_PARAMETERS{100.0, -1000, -200,0.0, 1.30,0.90, 0,0.002, 0,0.010,100.0, 75.0,5000.0} + +// ReverbConvertI3DL2ToNative: Utility function to map from I3DL2 to native parameters + +ReverbConvertI3DL2ToNative :: proc "contextless" (pI3DL2: ^REVERB_I3DL2_PARAMETERS, pNative: ^REVERB_PARAMETERS, sevenDotOneReverb: b32 = true) { + reflectionsDelay: f32 + reverbDelay: f32 + + // RoomRolloffFactor is ignored + + // These parameters have no equivalent in I3DL2 + if sevenDotOneReverb { + pNative.RearDelay = REVERB_DEFAULT_7POINT1_REAR_DELAY // 20 + } else { + pNative.RearDelay = REVERB_DEFAULT_REAR_DELAY // 5 + } + pNative.SideDelay = REVERB_DEFAULT_7POINT1_SIDE_DELAY // 5 + pNative.PositionLeft = REVERB_DEFAULT_POSITION // 6 + pNative.PositionRight = REVERB_DEFAULT_POSITION // 6 + pNative.PositionMatrixLeft = REVERB_DEFAULT_POSITION_MATRIX // 27 + pNative.PositionMatrixRight = REVERB_DEFAULT_POSITION_MATRIX // 27 + pNative.RoomSize = REVERB_DEFAULT_ROOM_SIZE // 100 + pNative.LowEQCutoff = 4 + pNative.HighEQCutoff = 6 + + // The rest of the I3DL2 parameters map to the native property set + pNative.RoomFilterMain = f32(pI3DL2.Room) / 100.0 + pNative.RoomFilterHF = f32(pI3DL2.RoomHF) / 100.0 + + if pI3DL2.DecayHFRatio >= 1.0 { + index := i32(-4.0 * math.log10_f32(pI3DL2.DecayHFRatio)) + if index < -8 { index = -8 } + pNative.LowEQGain = byte((index < 0) ? index + 8 : 8) + pNative.HighEQGain = 8 + pNative.DecayTime = pI3DL2.DecayTime * pI3DL2.DecayHFRatio + } else { + index := i32(4.0 * math.log10_f32(pI3DL2.DecayHFRatio)) + if index < -8 { index = -8 } + pNative.LowEQGain = 8 + pNative.HighEQGain = byte((index < 0) ? index + 8 : 8) + pNative.DecayTime = pI3DL2.DecayTime + } + + reflectionsDelay = pI3DL2.ReflectionsDelay * 1000.0 + if reflectionsDelay >= REVERB_MAX_REFLECTIONS_DELAY { // 300 + reflectionsDelay = f32(REVERB_MAX_REFLECTIONS_DELAY - 1) + } else if reflectionsDelay <= 1 { + reflectionsDelay = 1 + } + pNative.ReflectionsDelay = u32(reflectionsDelay) + + reverbDelay = pI3DL2.ReverbDelay * 1000.0 + if reverbDelay >= REVERB_MAX_REVERB_DELAY { // 85 + reverbDelay = f32(REVERB_MAX_REVERB_DELAY - 1) + } + pNative.ReverbDelay = byte(reverbDelay) + + pNative.ReflectionsGain = f32(pI3DL2.Reflections) / 100.0 + pNative.ReverbGain = f32(pI3DL2.Reverb) / 100.0 + pNative.EarlyDiffusion = byte(15.0 * pI3DL2.Diffusion / 100.0) + pNative.LateDiffusion = pNative.EarlyDiffusion + pNative.Density = pI3DL2.Density + pNative.RoomFilterFreq = pI3DL2.HFReference + + pNative.WetDryMix = pI3DL2.WetDryMix + pNative.DisableLateField = false +} diff --git a/vendor/x11/xlib/xlib_procs.odin b/vendor/x11/xlib/xlib_procs.odin index 2a8d6832b..2cd4e0f83 100644 --- a/vendor/x11/xlib/xlib_procs.odin +++ b/vendor/x11/xlib/xlib_procs.odin @@ -234,7 +234,7 @@ foreign xlib { display: ^Display, window: Window, attr_mask: WindowAttributeMask, - attr: XWindowAttributes, + attr: ^XWindowAttributes, ) --- SetWindowBackground :: proc( display: ^Display,