diff --git a/core/c/libc/errno.odin b/core/c/libc/errno.odin index 843b2f1b6..de429a6ec 100644 --- a/core/c/libc/errno.odin +++ b/core/c/libc/errno.odin @@ -98,6 +98,14 @@ when ODIN_OS == .Haiku { ERANGE :: B_POSIX_ERROR_BASE + 17 } +when ODIN_OS == .JS { + _ :: libc + _get_errno :: proc "c" () -> ^int { + @(static) errno: int + return &errno + } +} + // Odin has no way to make an identifier "errno" behave as a function call to // read the value, or to produce an lvalue such that you can assign a different // error value to errno. To work around this, just expose it as a function like diff --git a/core/c/libc/stdio.odin b/core/c/libc/stdio.odin index 019389b0d..a94a53696 100644 --- a/core/c/libc/stdio.odin +++ b/core/c/libc/stdio.odin @@ -89,6 +89,30 @@ when ODIN_OS == .Linux { } } +when ODIN_OS == .JS { + fpos_t :: struct #raw_union { _: [16]char, _: longlong, _: double, } + + _IOFBF :: 0 + _IOLBF :: 1 + _IONBF :: 2 + + BUFSIZ :: 1024 + + EOF :: int(-1) + + FOPEN_MAX :: 1000 + + FILENAME_MAX :: 4096 + + L_tmpnam :: 20 + + SEEK_SET :: 0 + SEEK_CUR :: 1 + SEEK_END :: 2 + + TMP_MAX :: 308915776 +} + when ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD { fpos_t :: distinct i64 diff --git a/core/c/libc/stdlib.odin b/core/c/libc/stdlib.odin index 08c6fa6f0..98280e44b 100644 --- a/core/c/libc/stdlib.odin +++ b/core/c/libc/stdlib.odin @@ -10,6 +10,9 @@ when ODIN_OS == .Windows { foreign import libc "system:c" } +@(require) +import "base:runtime" + when ODIN_OS == .Windows { RAND_MAX :: 0x7fff @@ -145,6 +148,10 @@ aligned_alloc :: #force_inline proc "c" (alignment, size: size_t) -> rawptr { _aligned_malloc :: proc(size, alignment: size_t) -> rawptr --- } return _aligned_malloc(size=size, alignment=alignment) + } else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 { + context = runtime.default_context() + data, _ := runtime.mem_alloc_bytes(auto_cast size, auto_cast alignment) + return raw_data(data) } else { foreign libc { aligned_alloc :: proc(alignment, size: size_t) -> rawptr --- @@ -160,6 +167,9 @@ aligned_free :: #force_inline proc "c" (ptr: rawptr) { _aligned_free :: proc(ptr: rawptr) --- } _aligned_free(ptr) + } else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 { + context = runtime.default_context() + runtime.mem_free(ptr) } else { free(ptr) } diff --git a/core/c/libc/string.odin b/core/c/libc/string.odin index cde9c7e6b..4ec4f3a7a 100644 --- a/core/c/libc/string.odin +++ b/core/c/libc/string.odin @@ -12,6 +12,7 @@ when ODIN_OS == .Windows { foreign import libc "system:c" } +@(default_calling_convention="c") foreign libc { // 7.24.2 Copying functions memcpy :: proc(s1, s2: rawptr, n: size_t) -> rawptr --- diff --git a/core/c/libc/time.odin b/core/c/libc/time.odin index 48def707e..6828793ec 100644 --- a/core/c/libc/time.odin +++ b/core/c/libc/time.odin @@ -45,7 +45,7 @@ when ODIN_OS == .Windows { } } -when ODIN_OS == .Linux || ODIN_OS == .FreeBSD || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD || ODIN_OS == .Haiku { +when ODIN_OS == .Linux || ODIN_OS == .FreeBSD || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD || ODIN_OS == .Haiku || ODIN_OS == .JS { @(default_calling_convention="c") foreign libc { // 7.27.2 Time manipulation functions diff --git a/core/c/libc/wctype.odin b/core/c/libc/wctype.odin index a41fe7fac..b96410b4c 100644 --- a/core/c/libc/wctype.odin +++ b/core/c/libc/wctype.odin @@ -14,7 +14,7 @@ when ODIN_OS == .Windows { wctrans_t :: distinct wchar_t wctype_t :: distinct ushort -} else when ODIN_OS == .Linux { +} else when ODIN_OS == .Linux || ODIN_OS == .JS { wctrans_t :: distinct intptr_t wctype_t :: distinct ulong diff --git a/core/os/os_js.odin b/core/os/os_js.odin index 8bf1d988b..348554728 100644 --- a/core/os/os_js.odin +++ b/core/os/os_js.odin @@ -1,8 +1,6 @@ #+build js package os -import "base:runtime" - foreign import "odin_env" @(require_results) diff --git a/core/sys/wasm/README.md b/core/sys/wasm/README.md new file mode 100644 index 000000000..1aaeaa429 --- /dev/null +++ b/core/sys/wasm/README.md @@ -0,0 +1,15 @@ +# WASM on the Web + +This directory is for use when targeting the `js_wasm32` target and the packages that rely on it. + +The `js_wasm32` target assumes that the WASM output will be ran within a web browser rather than a standalone VM. In the VM cases, either `wasi_wasm32` or `freestanding_wasm32` should be used accordingly. + +## Example for `js_wasm32` + +```html + + + +``` diff --git a/vendor/wasm/js/dom.odin b/core/sys/wasm/js/dom.odin similarity index 99% rename from vendor/wasm/js/dom.odin rename to core/sys/wasm/js/dom.odin index 28dd32fed..ffc58a9a3 100644 --- a/vendor/wasm/js/dom.odin +++ b/core/sys/wasm/js/dom.odin @@ -90,4 +90,4 @@ window_get_scroll :: proc "contextless" () -> (x, y: f64) { scroll: [2]f64 _window_get_scroll(&scroll) return scroll.x, scroll.y -} +} \ No newline at end of file diff --git a/vendor/wasm/js/dom_all_targets.odin b/core/sys/wasm/js/dom_all_targets.odin similarity index 100% rename from vendor/wasm/js/dom_all_targets.odin rename to core/sys/wasm/js/dom_all_targets.odin diff --git a/vendor/wasm/js/events.odin b/core/sys/wasm/js/events.odin similarity index 85% rename from vendor/wasm/js/events.odin rename to core/sys/wasm/js/events.odin index 258776fff..905b3eba9 100644 --- a/vendor/wasm/js/events.odin +++ b/core/sys/wasm/js/events.odin @@ -35,7 +35,7 @@ Event_Kind :: enum u32 { Submit, Blur, Change, - HashChange, + Hash_Change, Select, Animation_Start, @@ -82,6 +82,9 @@ Event_Kind :: enum u32 { Context_Menu, + Gamepad_Connected, + Gamepad_Disconnected, + Custom, } @@ -117,7 +120,7 @@ event_kind_string := [Event_Kind]string{ .Submit = "submit", .Blur = "blur", .Change = "change", - .HashChange = "hashchange", + .Hash_Change = "hashchange", .Select = "select", .Animation_Start = "animationstart", @@ -164,6 +167,9 @@ event_kind_string := [Event_Kind]string{ .Context_Menu = "contextmenu", + .Gamepad_Connected = "gamepadconnected", + .Gamepad_Disconnected = "gamepaddisconnected", + .Custom = "?custom?", } @@ -180,9 +186,15 @@ Key_Location :: enum u8 { Numpad = 3, } -KEYBOARD_MAX_KEY_SIZE :: 16 +KEYBOARD_MAX_KEY_SIZE :: 16 KEYBOARD_MAX_CODE_SIZE :: 16 +GAMEPAD_MAX_ID_SIZE :: 64 +GAMEPAD_MAX_MAPPING_SIZE :: 64 + +GAMEPAD_MAX_BUTTONS :: 64 +GAMEPAD_MAX_AXES :: 16 + Event_Target_Kind :: enum u32 { Element = 0, Document = 1, @@ -203,6 +215,30 @@ Event_Option :: enum u8 { } Event_Options :: distinct bit_set[Event_Option; u8] +Gamepad_Button :: struct { + value: f64, + pressed: bool, + touched: bool, +} + +Gamepad_State :: struct { + id: string, + mapping: string, + index: int, + connected: bool, + timestamp: f64, + + button_count: int, + axis_count: int, + buttons: [GAMEPAD_MAX_BUTTONS]Gamepad_Button `fmt:"v,button_count"`, + axes: [GAMEPAD_MAX_AXES]f64 `fmt:"v,axes_count"`, + + _id_len: int `fmt:"-"`, + _mapping_len: int `fmt:"-"`, + _id_buf: [GAMEPAD_MAX_ID_SIZE]byte `fmt:"-"`, + _mapping_buf: [GAMEPAD_MAX_MAPPING_SIZE]byte `fmt:"-"`, +} + Event :: struct { kind: Event_Kind, target_kind: Event_Target_Kind, @@ -260,6 +296,8 @@ Event :: struct { button: i16, buttons: bit_set[0..<16; u16], }, + + gamepad: Gamepad_State, }, @@ -336,7 +374,18 @@ remove_custom_event_listener :: proc(id: string, name: string, user_data: rawptr return _remove_event_listener(id, name, user_data, callback) } -import "core:fmt" +get_gamepad_state :: proc "contextless" (index: int, s: ^Gamepad_State) -> bool { + @(default_calling_convention="contextless") + foreign dom_lib { + @(link_name="get_gamepad_state") + _get_gamepad_state :: proc(index: int, s: ^Gamepad_State) -> bool --- + } + + if s == nil { + return false + } + return _get_gamepad_state(index, s) +} @(export, link_name="odin_dom_do_event_callback") @@ -355,9 +404,13 @@ do_event_callback :: proc(user_data: rawptr, callback: proc(e: Event)) { init_event_raw(&event) - if event.kind == .Key_Up || event.kind == .Key_Down || event.kind == .Key_Press { + #partial switch event.kind { + case .Key_Up, .Key_Down, .Key_Press: event.key.key = string(event.key._key_buf[:event.key._key_len]) event.key.code = string(event.key._code_buf[:event.key._code_len]) + case .Gamepad_Connected, .Gamepad_Disconnected: + event.gamepad.id = string(event.gamepad._id_buf[:event.gamepad._id_len]) + event.gamepad.mapping = string(event.gamepad._mapping_buf[:event.gamepad._mapping_len]) } callback(event) diff --git a/vendor/wasm/js/events_all_targets.odin b/core/sys/wasm/js/events_all_targets.odin similarity index 99% rename from vendor/wasm/js/events_all_targets.odin rename to core/sys/wasm/js/events_all_targets.odin index ccf39015d..b7e01ca10 100644 --- a/vendor/wasm/js/events_all_targets.odin +++ b/core/sys/wasm/js/events_all_targets.odin @@ -284,5 +284,4 @@ add_custom_event_listener :: proc(id: string, name: string, user_data: rawptr, c } remove_custom_event_listener :: proc(id: string, name: string, user_data: rawptr, callback: proc(e: Event)) -> bool { panic("vendor:wasm/js not supported on non JS targets") -} - +} \ No newline at end of file diff --git a/vendor/wasm/js/general.odin b/core/sys/wasm/js/general.odin similarity index 100% rename from vendor/wasm/js/general.odin rename to core/sys/wasm/js/general.odin diff --git a/vendor/wasm/js/memory_all_targets.odin b/core/sys/wasm/js/memory_all_targets.odin similarity index 100% rename from vendor/wasm/js/memory_all_targets.odin rename to core/sys/wasm/js/memory_all_targets.odin diff --git a/vendor/wasm/js/memory_js.odin b/core/sys/wasm/js/memory_js.odin similarity index 100% rename from vendor/wasm/js/memory_js.odin rename to core/sys/wasm/js/memory_js.odin diff --git a/vendor/wasm/js/runtime.js b/core/sys/wasm/js/odin.js similarity index 95% rename from vendor/wasm/js/runtime.js rename to core/sys/wasm/js/odin.js index afd9f6dff..bf002da74 100644 --- a/vendor/wasm/js/runtime.js +++ b/core/sys/wasm/js/odin.js @@ -1533,15 +1533,49 @@ function odinSetupDefaultImports(wasmMemoryInterface, consoleElement, memory, ev wmi.storeU8(off(1), !!e.repeat); - wmi.storeI32(off(W), e.key.length) - wmi.storeI32(off(W), e.code.length) + wmi.storeInt(off(W, W), e.key.length) + wmi.storeInt(off(W, W), e.code.length) wmi.storeString(off(16, 1), e.key); wmi.storeString(off(16, 1), e.code); } else if (e.type === 'scroll') { - wmi.storeF64(off(8), window.scrollX); - wmi.storeF64(off(8), window.scrollY); + wmi.storeF64(off(8, 8), window.scrollX); + wmi.storeF64(off(8, 8), window.scrollY); } else if (e.type === 'visibilitychange') { wmi.storeU8(off(1), !document.hidden); + } else if (e instanceof GamepadEvent) { + const idPtr = off(W*2, W); + const mappingPtr = off(W*2, W); + + wmi.storeI32(off(W, W), e.gamepad.index); + wmi.storeU8(off(1), !!e.gamepad.connected); + wmi.storeF64(off(8, 8), e.gamepad.timestamp); + + wmi.storeInt(off(W, W), e.gamepad.buttons.length); + wmi.storeInt(off(W, W), e.gamepad.axes.length); + + for (let i = 0; i < 64; i++) { + if (i < e.gamepad.buttons.length) { + let b = e.gamepad.buttons[i]; + wmi.storeF64(off(8, 8), b.value); + wmi.storeU8(off(1), !!b.pressed); + wmi.storeU8(off(1), !!b.touched); + } else { + off(16, 8); + } + } + for (let i = 0; i < 16; i++) { + if (i < e.gamepad.axes.length) { + let a = e.gamepad.axes[i]; + wmi.storeF64(off(8, 8), a); + } else { + off(8, 8); + } + } + + wmi.storeInt(off(W, W), e.gamepad.id.length) + wmi.storeInt(off(W, W), e.gamepad.mapping.length) + wmi.storeString(off(64, 1), e.gamepad.id); + wmi.storeString(off(64, 1), e.gamepad.mapping); } }, @@ -1646,6 +1680,76 @@ function odinSetupDefaultImports(wasmMemoryInterface, consoleElement, memory, ev return false; }, + get_gamepad_state: (gamepad_id, ep) => { + let index = gamepad_id; + let gps = navigator.getGamepads(); + if (0 <= index && index < gps.length) { + let gamepad = gps[index]; + if (!gamepad) { + return false; + } + + const W = wasmMemoryInterface.intSize; + let offset = ep; + let off = (amount, alignment) => { + if (alignment === undefined) { + alignment = Math.min(amount, W); + } + if (offset % alignment != 0) { + offset += alignment - (offset%alignment); + } + let x = offset; + offset += amount; + return x; + }; + + let align = (alignment) => { + const modulo = offset & (alignment-1); + if (modulo != 0) { + offset += alignment - modulo + } + }; + + let wmi = wasmMemoryInterface; + + const idPtr = off(W*2, W); + const mappingPtr = off(W*2, W); + + wmi.storeI32(off(W), gamepad.index); + wmi.storeU8(off(1), !!gamepad.connected); + wmi.storeF64(off(8), gamepad.timestamp); + + wmi.storeInt(off(W), gamepad.buttons.length); + wmi.storeInt(off(W), gamepad.axes.length); + + for (let i = 0; i < 64; i++) { + if (i < gamepad.buttons.length) { + let b = gamepad.buttons[i]; + wmi.storeF64(off(8, 8), b.value); + wmi.storeU8(off(1), !!b.pressed); + wmi.storeU8(off(1), !!b.touched); + } else { + off(16, 8); + } + } + for (let i = 0; i < 16; i++) { + if (i < gamepad.axes.length) { + wmi.storeF64(off(8, 8), gamepad.axes[i]); + } else { + off(8, 8); + } + } + + wmi.storeInt(off(W, W), gamepad.id.length) + wmi.storeInt(off(W, W), gamepad.mapping.length) + wmi.storeString(off(64, 1), gamepad.id); + wmi.storeString(off(64, 1), gamepad.mapping); + + return true; + } + return false; + }, + get_element_value_f64: (id_ptr, id_len) => { let id = wasmMemoryInterface.loadString(id_ptr, id_len); let element = getElement(id); diff --git a/core/time/rfc3339.odin b/core/time/rfc3339.odin index e4c6565d6..20e8ea0bb 100644 --- a/core/time/rfc3339.odin +++ b/core/time/rfc3339.odin @@ -187,4 +187,110 @@ scan_digits :: proc(s: string, sep: string, count: int) -> (res: int, ok: bool) found_sep |= rune(s[count]) == v } return res, found_sep -} \ No newline at end of file +} + +/* +Serialize the timestamp as a RFC 3339 string. + +The boolean `ok` is false if the `time` is not a valid datetime, or if allocating the result string fails. + +**Inputs**: +- `utc_offset`: offset in minutes wrt UTC (ie. the timezone) +- `include_nanos`: whether to include nanoseconds in the result. +*/ +time_to_rfc3339 :: proc(time: Time, utc_offset : int = 0, include_nanos := true, allocator := context.allocator) -> (res: string, ok: bool) { + utc_offset := utc_offset + + // convert to datetime + datetime := time_to_datetime(time) or_return + + if datetime.year < 0 || datetime.year >= 10_000 { return "", false } + + temp_string := [36]u8{} + offset : uint = 0 + + print_as_fixed_int :: proc(dst: []u8, offset: ^uint, width: i8, i: i64) { + i := i + width := width + for digit_idx in 0.. (res: i64, n_digits: i8) { + res = n + n_digits = 9 + for res % 10 == 0 { + res = res / 10 + n_digits -= 1 + } + return + } + + // pre-epoch times: turn, say, -400ms to +600ms for display + nanos := time._nsec % 1_000_000_000 + if nanos < 0 { + nanos += 1_000_000_000 + } + + if nanos != 0 && include_nanos { + temp_string[offset] = '.' + offset += 1 + + // remove trailing zeroes + nanos_nonzero, n_digits := strip_trailing_zeroes_nanos(nanos) + assert(nanos_nonzero != 0) + + // write digits, right-to-left + for digit_idx : i8 = n_digits-1; digit_idx >= 0; digit_idx -= 1 { + digit := u8(nanos_nonzero % 10) + temp_string[offset + uint(digit_idx)] = '0' + u8(digit) + nanos_nonzero /= 10 + } + offset += uint(n_digits) + } + + if utc_offset == 0 { + temp_string[offset] = 'Z' + offset += 1 + } else { + temp_string[offset] = utc_offset > 0 ? '+' : '-' + offset += 1 + utc_offset = abs(utc_offset) + print_as_fixed_int(temp_string[:], &offset, 2, i64(utc_offset / 60)) + temp_string[offset] = ':' + offset += 1 + print_as_fixed_int(temp_string[:], &offset, 2, i64(utc_offset % 60)) + } + + res_as_slice, res_alloc := make_slice([]u8, len=offset, allocator = allocator) + if res_alloc != nil { + return "", false + } + + copy(res_as_slice, temp_string[:offset]) + + return string(res_as_slice), true +} diff --git a/tests/core/time/test_core_time.odin b/tests/core/time/test_core_time.odin index 51955b1c4..424111aa3 100644 --- a/tests/core/time/test_core_time.odin +++ b/tests/core/time/test_core_time.odin @@ -196,6 +196,36 @@ test_parse_rfc3339_string :: proc(t: ^testing.T) { } } +@test +test_print_rfc3339 :: proc(t: ^testing.T) { + TestCase :: struct { + printed: string, + time: i64, + utc_offset: int, + } + + tests :: [?]TestCase { + {"1985-04-12T23:20:50.52Z", 482196050520000000, 0}, + {"1985-04-12T23:20:50.52001905Z", 482196050520019050, 0}, + {"1996-12-19T16:39:57-08:00", 851013597000000000, -480}, + {"1996-12-20T00:39:57Z", 851042397000000000, 0}, + {"1937-01-01T12:00:27.87+00:20", -1041335972130000000, +20}, + } + + for test in tests { + timestamp := time.Time { _nsec = test.time } + printed_timestamp, ok := time.time_to_rfc3339(time=timestamp, utc_offset=test.utc_offset) + defer delete_string(printed_timestamp) + + testing.expect(t, ok, "expected printing to work fine") + + testing.expectf( + t, printed_timestamp == test.printed, + "expected is %w, printed is %w", test.printed, printed_timestamp, + ) + } +} + @test test_parse_iso8601_string :: proc(t: ^testing.T) { for test in iso8601_tests { @@ -318,4 +348,4 @@ date_component_roundtrip_test :: proc(t: ^testing.T, moment: dt.DateTime) { "Expected %4d-%2d-%2d %2d:%2d:%2d, got %4d-%2d-%2d %2d:%2d:%2d", moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, YYYY, MM, DD, hh, mm, ss, ) -} \ No newline at end of file +} diff --git a/vendor/libc/stdlib.odin b/vendor/libc/stdlib.odin index f898de619..54590c1c9 100644 --- a/vendor/libc/stdlib.odin +++ b/vendor/libc/stdlib.odin @@ -37,7 +37,7 @@ realloc :: proc "c" (ptr: rawptr, new_size: uint) -> rawptr { // Note that realloc does not actually care about alignment and is allowed to just align it to something // else than the original allocation. ptr, err := runtime.non_zero_mem_resize(ptr, -1, int(new_size)) - assert(err != nil, "realloc failure") + assert(err == nil, "realloc failure") return raw_data(ptr) } diff --git a/vendor/stb/image/stb_image.odin b/vendor/stb/image/stb_image.odin index 85d612354..0c2ad7d05 100644 --- a/vendor/stb/image/stb_image.odin +++ b/vendor/stb/image/stb_image.odin @@ -20,7 +20,6 @@ when LIB != "" { when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 { foreign import stbi "../lib/stb_image_wasm.o" - foreign import stbi { LIB } } else when LIB != "" { foreign import stbi { LIB } } else { diff --git a/vendor/stb/image/stb_image_resize.odin b/vendor/stb/image/stb_image_resize.odin index a37c2e243..241a93eb0 100644 --- a/vendor/stb/image/stb_image_resize.odin +++ b/vendor/stb/image/stb_image_resize.odin @@ -1,6 +1,6 @@ package stb_image -import c "core:c/libc" +import "core:c" @(private) RESIZE_LIB :: ( diff --git a/vendor/stb/image/stb_image_write.odin b/vendor/stb/image/stb_image_write.odin index a0c0b57a0..e86fa2b95 100644 --- a/vendor/stb/image/stb_image_write.odin +++ b/vendor/stb/image/stb_image_write.odin @@ -1,6 +1,6 @@ package stb_image -import c "core:c/libc" +import "core:c" @(private) WRITE_LIB :: ( diff --git a/vendor/wasm/README.md b/vendor/wasm/README.md index d0b0a2f6f..1aaeaa429 100644 --- a/vendor/wasm/README.md +++ b/vendor/wasm/README.md @@ -6,9 +6,9 @@ The `js_wasm32` target assumes that the WASM output will be ran within a web bro ## Example for `js_wasm32` -```js - - +```html + +