From 8a1f8fbd28e95ae0415e29afee2ed60b480b3ec1 Mon Sep 17 00:00:00 2001 From: Peter Li Date: Fri, 18 Sep 2026 11:29:02 -0700 Subject: [PATCH] new header parser and generator --- LICENSE | 22 ++ README.md | 55 +++- build.zig | 4 +- include/OPENGL-SOURCES.md | 5 +- include/uhdk.h | 543 ++++++++++++++++++++++++++------------ include/uhdk_native.h | 33 --- include/uhdk_opengl.h | 19 -- include/uhdk_profile.h | 28 -- src/Application.zig | 7 +- src/Event.zig | 2 +- src/EventPayload.zig | 60 +++++ src/api/app.zig | 29 +- src/arenas.zig | 520 ++++++++++++++++++++++++++++++++++++ src/callbacks.zig | 8 +- src/core_tests.zig | 69 ++++- src/macos/native.m | 50 ++-- src/native.h | 49 +++- src/root.zig | 1 + src/runtime.zig | 14 +- src/windows/native.c | 34 +-- 20 files changed, 1224 insertions(+), 328 deletions(-) create mode 100644 LICENSE delete mode 100644 include/uhdk_native.h delete mode 100644 include/uhdk_opengl.h delete mode 100644 include/uhdk_profile.h create mode 100644 src/EventPayload.zig create mode 100644 src/arenas.zig diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8599b0f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +// MIT License +// +// Copyright (c) 2022 Peter Li +// +// 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/README.md b/README.md index ee15b11..27addf1 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ exe.root_module.addImport("uhdk", uhdk.module("uhdk")); ``` `@import("uhdk")` provides translated public C declarations, including the bundled -OpenGL headers, and links the library and required system frameworks. The +OpenGL and experimental native-handle declarations, and links the library and required system frameworks. The `uhdk` artifact is also available through `uhdk.artifact("uhdk")` for C consumers or custom bindings. Set `.shared = true` to build a dynamic library; applications must arrange its runtime location/rpath. @@ -49,10 +49,24 @@ must arrange its runtime location/rpath. ## C interface - [uhdk.h](include/uhdk.h): desktop API, lifecycle, ownership, and threading contracts. -- [uhdk_opengl.h](include/uhdk_opengl.h): GLAD 2 OpenGL 3.3 core declarations and per-context loader table. -- [uhdk_native.h](include/uhdk_native.h): optional platform interoperability. - [OPENGL-SOURCES.md](include/OPENGL-SOURCES.md): GLAD generation recipe and Khronos header provenance and licenses. +All UHDK declarations live in `uhdk.h`. Before its first inclusion, define +`UHDK_ENABLE_OPENGL` to include the bundled GLAD 2 OpenGL 3.3 core declarations +and per-context loader table, and/or `UHDK_ENABLE_NATIVE` for the experimental +native-window handle API: + +```c +#define UHDK_ENABLE_OPENGL +#define UHDK_ENABLE_NATIVE +#include "uhdk.h" +``` + +Presence enables each flag; leave it undefined to disable it. These flags select +header declarations, not library backends. UHDK's own context-management API is +always declared. The supplied Zig module enables both sections. The vendored GL +headers remain unmodified dependencies rather than being pasted into `uhdk.h`. + The public interface uses pointer-and-length text, `char` booleans, and `float` coordinates. The native bridge is private; consumers do not need Objective-C. @@ -72,9 +86,42 @@ are not implemented. Native UTF-16 path bytes are kept separately from the WTF-8 paths passed to Zig I/O. If a native path cannot be exported losslessly as a shell URI, URI export returns `UNSUPPORTED`; native-path export still preserves it. +## Borrowed event payloads + +Events and request results carry `const void *data` plus `data_size` instead of +public unions. Check the event type (or successful request kind), then read the +corresponding named payload struct listed in `uhdk.h`. Check `data_size` before +reading fields; unknown types can be ignored. Payload-free events and unsuccessful +request results have NULL data and zero size. Failure diagnostics remain available +in the request envelope. + +```c +if (event->type == UHDK_EVENT_KEY && + event->data_size >= sizeof(uhdk_key_event)) { + const uhdk_key_event *key = (const uhdk_key_event *)event->data; + /* Read key->logical_key, key->pressed, etc. here. */ +} +``` + +One library-owned scratch arena holds the returned batch, its payload structs, +and nested spans. The next valid application-thread poll resets it, even if that +poll is empty or returns an error. Invalid arguments and wrong-thread calls do +not reset it. App destruction also invalidates everything. Rendering does not +advance this lifetime. Copy data you need to keep; copying an envelope alone +copies only its pointers. Retain file/menu references when needed beyond a poll, +but even retained handles cannot outlive the app. + +UHDK is pre-release: API and ABI compatibility with earlier revisions is not +guaranteed. Set creation descriptors to `UHDK_ABI_VERSION`. Event envelopes have +a fixed array stride within each returned batch. + +The scratch arena is maintained as UHDK-owned code in +[src/arenas.zig](src/arenas.zig), including its supporting storage and tests. +It originated in Ribtech p2; the MIT license and attribution are retained in that file. + ## Optional profiling -`uhdk.h` includes `uhdk_profile.h`: scope, frame, thread-name and named allocation/ +`uhdk.h` provides scope, frame, thread-name and named allocation/ free macros. They compile away without evaluating arguments unless the consumer defines `UHDK_PROFILE_BACKEND` to a quoted adapter header before including UHDK. An adapter implements the macros documented in that header. Names require static diff --git a/build.zig b/build.zig index 640faa7..3e4e36e 100644 --- a/build.zig +++ b/build.zig @@ -27,10 +27,12 @@ pub fn build(b: *std.Build) void { const unit_run = b.addRunArtifact(unit); b.step("test", "Run core library tests").dependOn(&unit_run.step); const public_c = b.addTranslateC(.{ - .root_source_file = b.path("include/uhdk_opengl.h"), + .root_source_file = b.path("include/uhdk.h"), .target = target, .optimize = optimize, }); + public_c.defineCMacro("UHDK_ENABLE_OPENGL", "1"); + public_c.defineCMacro("UHDK_ENABLE_NATIVE", "1"); public_c.addIncludePath(b.path("include")); if (shared) public_c.defineCMacro("UHDK_SHARED", "1"); const public_module = b.addModule("uhdk", .{ diff --git a/include/OPENGL-SOURCES.md b/include/OPENGL-SOURCES.md index f3ab94b..5f15c93 100644 --- a/include/OPENGL-SOURCES.md +++ b/include/OPENGL-SOURCES.md @@ -2,7 +2,8 @@ ## GLAD 2.0.8 -`uhdk_opengl.h` includes the generated `glad/gl.h`: OpenGL 3.3 core, no +`uhdk.h` with `UHDK_ENABLE_OPENGL` includes the generated `glad/gl.h`: +OpenGL 3.3 core, no extensions, header-only output, explicit per-context tables (`--mx`), and no platform loader. `src/glad.c` compiles its implementation into libuhdk. Applications load a `GladGLContext` through their adapter for `uhdk_gl_context_get_proc`. @@ -27,7 +28,7 @@ No generator or Python installation is required to build Uhdk. ## Original Khronos headers Retained unmodified for consumers choosing their own loader. Do not combine -`GL/glcorearb.h` with `glad/gl.h` or `uhdk_opengl.h` in a translation unit. +`GL/glcorearb.h` with `glad/gl.h` or `UHDK_ENABLE_OPENGL` in a translation unit. License notices are preserved in each file. - `include/GL/glcorearb.h` diff --git a/include/uhdk.h b/include/uhdk.h index a2c1b09..0abd94c 100644 --- a/include/uhdk.h +++ b/include/uhdk.h @@ -9,6 +9,13 @@ * supplies types only; this header requires no libc function calls. * Native OS frameworks may themselves depend on system runtimes. * + * Header configuration (define before the first include): + * - UHDK_ENABLE_OPENGL: include bundled GLAD types and per-context loader API. + * - UHDK_ENABLE_NATIVE: declare the experimental borrowed native-handle API. + * - UHDK_PROFILE_BACKEND: quoted consumer instrumentation adapter header. + * Presence enables each feature flag; leave it undefined to disable it. + * These select declarations only, not library backend/build features. + * * Naming: * - Types: uhdk_type; functions: uhdk_object_verb; constants: UHDK_CATEGORY_VALUE. * - create/destroy allocate and dispose unique objects; retain/release manage @@ -27,8 +34,10 @@ * - NULL is allowed for a span's pointer only when its length is zero. * - Input spans/descriptors are copied before return unless documented otherwise. * - No allocator ownership crosses the ABI. Release objects through Uhdk. - * - Event spans are borrowed until the next poll or app destruction. Copy spans - * or retain files/menus to keep them. Each poll returns an immutable batch. + * - Event batches and spans are borrowed until the next valid poll on the + * application thread, or app destruction (whichever comes first). Copy spans + * or retain files/menus to keep them. Every such poll resets storage, including empty/error polls. + * Rendering does not reset storage. Each poll returns an immutable batch. * - create/run/destroy belong to OS main. run invokes one application entry on * a second thread. All other calls belong to that application thread except * app_wake, which is thread-safe. Native work is queued to the main thread. @@ -44,11 +53,33 @@ * except documented buffer-size queries. * - The application owns rendering, document state, file encoding and layout. * Uhdk provides optional OpenGL 3.3 core context management. Other renderers - * may attach through experimental uhdk_native.h; drawing remains app-owned. + * may attach through the experimental native-handle API; drawing remains app-owned. */ #include -#include "uhdk_profile.h" +/* Optional, consumer-owned instrumentation; no exported UHDK ABI or dependency. + * Define UHDK_PROFILE_BACKEND to a quoted adapter header before including uhdk.h. + * That header implements the macros below. Without it, arguments are not evaluated. + * Names (zone, thread, pool) must be string literals or have static lifetime. + * C zones: UHDK_PROFILE_BEGIN(token, "name"); ... UHDK_PROFILE_END(token); + * End on the same thread, in reverse nesting order, including early exits. + * C++ may use UHDK_PROFILE_SCOPE("name") for automatic scope exit. + * Memory: report successful non-null allocations, then free BEFORE releasing + * storage. A successful resize is free + alloc; failed allocations emit nothing. + * A pointer must use the same pool name for allocation and free. These hooks do + * not allocate/free memory themselves, intercept malloc, or transfer ownership. + */ +#ifdef UHDK_PROFILE_BACKEND +#include UHDK_PROFILE_BACKEND +#else +#define UHDK_PROFILE_BEGIN(token, name) ((void)0) +#define UHDK_PROFILE_END(token) ((void)0) +#define UHDK_PROFILE_SCOPE(name) ((void)0) +#define UHDK_PROFILE_FRAME() ((void)0) +#define UHDK_PROFILE_THREAD(name) ((void)0) +#define UHDK_PROFILE_ALLOC(ptr, size, pool) ((void)0) +#define UHDK_PROFILE_FREE(ptr, pool) ((void)0) +#endif #ifndef UHDK_API #if defined(_WIN32) && defined(UHDK_SHARED) @@ -185,7 +216,7 @@ typedef struct uhdk_event_list { } uhdk_event_list; #define UHDK_WAIT_FOREVER UINT64_MAX -typedef void(UHDK_CALL *uhdk_app_main_fn)(uhdk_app *, void *user); +typedef void(UHDK_CALL *uhdk_app_main_fn)(uhdk_app *app, void *user); typedef struct uhdk_app_desc { uint32_t struct_size; uint32_t abi_version; @@ -196,27 +227,34 @@ typedef struct uhdk_app_desc { } uhdk_app_desc; UHDK_API uint32_t UHDK_CALL uhdk_get_abi_version(void); -UHDK_API uhdk_status UHDK_CALL uhdk_app_create(const uhdk_app_desc *, uhdk_app **out); -UHDK_API uhdk_status UHDK_CALL uhdk_app_run(uhdk_app *, uhdk_app_main_fn, void *user); /* Once. */ +UHDK_API uhdk_status UHDK_CALL uhdk_app_create(const uhdk_app_desc *desc, uhdk_app **out); +UHDK_API uhdk_status UHDK_CALL uhdk_app_run( + uhdk_app *app, + uhdk_app_main_fn main_fn, + void *user +); /* Once. */ /* Zero timeout does not wait; finite timeouts use monotonic nanoseconds. * STOPPED is delivered once; later polls return UNAVAILABLE. */ -UHDK_API uhdk_status UHDK_CALL uhdk_app_poll_events(uhdk_app *, uint64_t timeout_ns, - uhdk_event_list *out); -UHDK_API void UHDK_CALL uhdk_app_quit(uhdk_app *); /* Accept/request shutdown. */ -UHDK_API void UHDK_CALL uhdk_app_wake(uhdk_app *); /* Thread-safe, coalesced WAKE event. */ +UHDK_API uhdk_status UHDK_CALL uhdk_app_poll_events( + uhdk_app *app, + uint64_t timeout_ns, + uhdk_event_list *out +); +UHDK_API void UHDK_CALL uhdk_app_quit(uhdk_app *app); /* Accept/request shutdown. */ +UHDK_API void UHDK_CALL uhdk_app_wake(uhdk_app *app); /* Thread-safe, coalesced WAKE event. */ /* Monotonic nanoseconds since app creation, in the same clock as event.time_ns. */ -UHDK_API uint64_t UHDK_CALL uhdk_app_get_time_ns(uhdk_app *); +UHDK_API uint64_t UHDK_CALL uhdk_app_get_time_ns(uhdk_app *app); /* One pending deadline per app. Replaces any previous deadline; 0 cancels it. * A past deadline queues WAKE at the next dispatch. On expiry the deadline is * cleared before delivering WAKE. Explicit wakes may coalesce with expiry but * do not cancel a future deadline. No event is delivered inline. App code maintains * multiple timers by scheduling its earliest deadline. No real-time guarantee. */ -UHDK_API uhdk_status UHDK_CALL uhdk_app_set_wake_deadline(uhdk_app *, uint64_t deadline_ns); -UHDK_API void UHDK_CALL uhdk_app_destroy(uhdk_app *); /* After run; releases child objects. */ -UHDK_API uhdk_backend UHDK_CALL uhdk_app_get_backend(uhdk_app *); -UHDK_API uhdk_bool UHDK_CALL uhdk_app_has_capability(uhdk_app *, uhdk_capability); -UHDK_API uhdk_status UHDK_CALL uhdk_request_cancel(uhdk_app *, uhdk_request_id); +UHDK_API uhdk_status UHDK_CALL uhdk_app_set_wake_deadline(uhdk_app *app, uint64_t deadline_ns); +UHDK_API void UHDK_CALL uhdk_app_destroy(uhdk_app *app); /* After run; releases child objects. */ +UHDK_API uhdk_backend UHDK_CALL uhdk_app_get_backend(uhdk_app *app); +UHDK_API uhdk_bool UHDK_CALL uhdk_app_has_capability(uhdk_app *app, uhdk_capability capability); +UHDK_API uhdk_status UHDK_CALL uhdk_request_cancel(uhdk_app *app, uhdk_request_id request_id); /* Application-thread CPU execution time in nanoseconds, not elapsed wall time. * Bracket user update/input processing with begin_tick/end_tick, AFTER polling @@ -250,9 +288,9 @@ typedef struct uhdk_app_stats { float frames_per_second; } uhdk_app_stats; -UHDK_API uhdk_status UHDK_CALL uhdk_app_begin_tick(uhdk_app *); -UHDK_API uhdk_status UHDK_CALL uhdk_app_end_tick(uhdk_app *); -UHDK_API uhdk_status UHDK_CALL uhdk_app_get_stats(uhdk_app *, uhdk_app_stats *out); +UHDK_API uhdk_status UHDK_CALL uhdk_app_begin_tick(uhdk_app *app); +UHDK_API uhdk_status UHDK_CALL uhdk_app_end_tick(uhdk_app *app); +UHDK_API uhdk_status UHDK_CALL uhdk_app_get_stats(uhdk_app *app, uhdk_app_stats *out); /* Logical coordinates: top-left origin, x right, y down. Window geometry uses * content-area units. Framebuffer dimensions are pixels; use reported sizes, @@ -294,25 +332,46 @@ typedef struct uhdk_window_metrics { uhdk_bool visible; } uhdk_window_metrics; -UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async(uhdk_app *, const uhdk_window_desc *, - uhdk_request_id *out); -UHDK_API void UHDK_CALL uhdk_window_destroy(uhdk_window *); -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_title_async(uhdk_window *, uhdk_text, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_size_async(uhdk_window *, uhdk_size, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_state_async(uhdk_window *, uhdk_window_state, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_visible_async(uhdk_window *, uhdk_bool, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL -uhdk_window_request_focus_async(uhdk_window *, uhdk_request_id *out); /* OS may decline. */ -UHDK_API uhdk_status UHDK_CALL uhdk_window_get_metrics(uhdk_window *, uhdk_window_metrics *out); -UHDK_API void UHDK_CALL uhdk_window_invalidate(uhdk_window *); /* Coalesced redraw event. */ +UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async( + uhdk_app *app, + const uhdk_window_desc *desc, + uhdk_request_id *out +); +UHDK_API void UHDK_CALL uhdk_window_destroy(uhdk_window *window); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_title_async( + uhdk_window *window, + uhdk_text title, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_size_async( + uhdk_window *window, + uhdk_size size, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_state_async( + uhdk_window *window, + uhdk_window_state state, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_visible_async( + uhdk_window *window, + uhdk_bool visible, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_window_request_focus_async( + uhdk_window *window, + uhdk_request_id *out +); /* OS may decline. */ +UHDK_API uhdk_status UHDK_CALL uhdk_window_get_metrics( + uhdk_window *window, + uhdk_window_metrics *out +); +UHDK_API void UHDK_CALL uhdk_window_invalidate(uhdk_window *window); /* Coalesced redraw event. */ /* Optional OpenGL context management (application/render thread). - * Include for bundled Khronos GL types, constants and PFNGL* - * function-pointer types. This header alone does not introduce GL names. + * Define UHDK_ENABLE_OPENGL before including this header for bundled Khronos + * GL types, constants, and PFNGL* function-pointer types. Without that flag, + * no GL names are introduced. * * Creation requires UHDK_WINDOW_OPENGL and guarantees desktop OpenGL >= 3.3, * core profile, with GLSL >= 3.30. macOS selects its 4.1 core profile because @@ -341,28 +400,39 @@ UHDK_API void UHDK_CALL uhdk_window_invalidate(uhdk_window *); /* Coalesced redr * type before calling. GL uses stdcall on Win32; Uhdk exports still use C ABI. */ typedef void(UHDK_GL_CALL *uhdk_gl_proc)(void); -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_create_async(uhdk_window *, uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_begin_frame(uhdk_gl_context *); -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_end_frame(uhdk_gl_context *); -UHDK_API void UHDK_CALL uhdk_gl_context_destroy(uhdk_gl_context *); -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_make_current(uhdk_gl_context *); -UHDK_API uhdk_status UHDK_CALL uhdk_gl_clear_current(uhdk_app *); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_create_async( + uhdk_window *window, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_begin_frame(uhdk_gl_context *context); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_end_frame(uhdk_gl_context *context); +UHDK_API void UHDK_CALL uhdk_gl_context_destroy(uhdk_gl_context *context); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_make_current(uhdk_gl_context *context); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_clear_current(uhdk_app *app); /* Actual negotiated version, not the requested minimum. */ -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_version(uhdk_gl_context *, uint32_t *out_major, - uint32_t *out_minor); -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_swap_buffers(uhdk_gl_context *); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_version( + uhdk_gl_context *context, + uint32_t *out_major, + uint32_t *out_minor +); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_swap_buffers(uhdk_gl_context *context); /* Explicitly set 0 (unsynchronized) or 1 (vsync); other values are invalid. * No initial interval is guaranteed. Unsupported control returns UNSUPPORTED; * compositor/driver policy may override presentation timing. */ -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_set_swap_interval(uhdk_gl_context *, - int32_t interval); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_set_swap_interval( + uhdk_gl_context *context, + int32_t interval +); /* Requires this context current. Accepts pointer+length ASCII GL symbol names. * Missing symbols return NOT_FOUND. Pointers are valid until context destruction * and must only be used with that context current. A non-NULL pointer alone does * not prove extension/version support; check GL version/extension availability. * Loads both platform-library exports and driver-provided GL entry points. */ -UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_proc(uhdk_gl_context *, uhdk_text name, - uhdk_gl_proc *out); +UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_proc( + uhdk_gl_context *context, + uhdk_text name, + uhdk_gl_proc *out +); /* Keyboard events are not text. Physical key = USB HID keyboard-page usage; * 0 means unknown. Logical key = Unicode scalar, or UHDK_KEY_* below. @@ -448,12 +518,18 @@ enum { UHDK_POINTER_NORMAL = 0, UHDK_POINTER_CAPTURE = 1 }; -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_cursor_shape_async(uhdk_window *, uhdk_cursor_shape, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_cursor_shape_async( + uhdk_window *window, + uhdk_cursor_shape shape, + uhdk_request_id *out +); /* Capture delivers selection-drag events outside the window. Focus loss or OS * cancellation restores NORMAL and emits POINTER_MODE_CHANGED. */ -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_pointer_mode_async(uhdk_window *, uhdk_pointer_mode, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_pointer_mode_async( + uhdk_window *window, + uhdk_pointer_mode mode, + uhdk_request_id *out +); /* Text offsets/lengths are UTF-8 byte counts, always at scalar boundaries. * surrounding_text is a bounded snapshot; document_offset locates it in the @@ -476,11 +552,19 @@ typedef struct uhdk_text_state { uint64_t selection_len; uhdk_rect caret; /* Window-local logical coordinates. */ } uhdk_text_state; -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_input_async(uhdk_window *, uhdk_bool enabled, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_state(uhdk_window *, const uhdk_text_state *); -UHDK_API uhdk_status UHDK_CALL uhdk_window_cancel_composition_async(uhdk_window *, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_input_async( + uhdk_window *window, + uhdk_bool enabled, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_state( + uhdk_window *window, + const uhdk_text_state *state +); +UHDK_API uhdk_status UHDK_CALL uhdk_window_cancel_composition_async( + uhdk_window *window, + uhdk_request_id *out +); /* File references preserve native names, including non-UTF-8 POSIX paths. * Display names are UTF-8. URI/native-path export is explicit and owned by the @@ -500,15 +584,25 @@ enum { UHDK_FILE_URI = 1, UHDK_FILE_NATIVE_PATH = 2 }; -UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_native_path(uhdk_app *, uhdk_bytes path, - uhdk_file **out); -UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_uri(uhdk_app *, uhdk_text uri, - uhdk_file **out); -UHDK_API void UHDK_CALL uhdk_file_retain(uhdk_file *); -UHDK_API void UHDK_CALL uhdk_file_release(uhdk_file *); -UHDK_API uhdk_status UHDK_CALL uhdk_file_copy_representation(uhdk_file *, uhdk_file_representation, - uint8_t *buffer, uint64_t capacity, - uint64_t *out_len); +UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_native_path( + uhdk_app *app, + uhdk_bytes path, + uhdk_file **out +); +UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_uri( + uhdk_app *app, + uhdk_text uri, + uhdk_file **out +); +UHDK_API void UHDK_CALL uhdk_file_retain(uhdk_file *file); +UHDK_API void UHDK_CALL uhdk_file_release(uhdk_file *file); +UHDK_API uhdk_status UHDK_CALL uhdk_file_copy_representation( + uhdk_file *file, + uhdk_file_representation representation, + uint8_t *buffer, + uint64_t capacity, + uint64_t *out_len +); typedef uint32_t uhdk_file_type; enum { @@ -534,11 +628,17 @@ typedef struct uhdk_file_info { * New POSIX files use mode 0600. No power-loss durability or conflict detection promise. * No silent truncate fallback. Cancellation before rename leaves the target intact. */ -UHDK_API uhdk_status UHDK_CALL uhdk_file_read_all_async(uhdk_file *, uint64_t max_bytes, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_file_write_all_async(uhdk_file *, uhdk_bytes, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_file_get_info_async(uhdk_file *, uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_file_read_all_async( + uhdk_file *file, + uint64_t max_bytes, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_file_write_all_async( + uhdk_file *file, + uhdk_bytes bytes, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_file_get_info_async(uhdk_file *file, uhdk_request_id *out); typedef uint32_t uhdk_dialog_kind; enum { @@ -564,29 +664,40 @@ typedef struct uhdk_file_dialog_desc { const uhdk_file_filter *filters; uint64_t filters_len; } uhdk_file_dialog_desc; -UHDK_API uhdk_status UHDK_CALL uhdk_file_dialog_show_async(uhdk_app *, - const uhdk_file_dialog_desc *, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_file_dialog_show_async( + uhdk_app *app, + const uhdk_file_dialog_desc *desc, + uhdk_request_id *out +); /* Plain-text clipboard only. Writes copy UTF-8 before returning; empty text is * valid. Reads negotiate native text formats internally and complete with text - * borrowed until the next event poll. NOT_FOUND means no text format. + * borrowed for the event batch lifetime. NOT_FOUND means no text format. * max_bytes bounds the UTF-8 result; excess returns BUFFER_TOO_SMALL, never a * truncated string. A read remains async even when a backend can read instantly. */ -UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_write_text_async(uhdk_app *, uhdk_text, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_read_text_async(uhdk_app *, uint64_t max_bytes, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_write_text_async( + uhdk_app *app, + uhdk_text text, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_read_text_async( + uhdk_app *app, + uint64_t max_bytes, + uhdk_request_id *out +); /* Incoming file drops only. When enabled, Uhdk negotiates copy-only file drops * and emits DROP_FILES with borrowed file references plus the drop position. - * Retain references to use after the next poll. Directory references are allowed; + * Retain references to use beyond the event batch lifetime. Directory references are allowed; * the application decides whether to open them. No source deletion, outgoing * drag, data offers, or application-visible drop-completion transaction. */ -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_drop_enabled_async(uhdk_window *, uhdk_bool, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_drop_enabled_async( + uhdk_window *window, + uhdk_bool enabled, + uhdk_request_id *out +); /* Menus are immutable snapshots. Command IDs are application-assigned, nonzero * and unique within a menu tree. Shortcuts use logical keys + modifiers. @@ -623,26 +734,49 @@ typedef struct uhdk_menu_item { uhdk_menu *submenu; /* Retained by the created parent menu. */ } uhdk_menu_item; -UHDK_API uhdk_status UHDK_CALL uhdk_menu_create(uhdk_app *, const uhdk_menu_item *, uint64_t len, - uhdk_menu **out); -UHDK_API uhdk_status UHDK_CALL uhdk_menu_set_item_flags_async(uhdk_menu *, uint64_t command_id, - uhdk_menu_flags, - uhdk_request_id *out); -UHDK_API void UHDK_CALL uhdk_menu_retain(uhdk_menu *); -UHDK_API void UHDK_CALL uhdk_menu_release(uhdk_menu *); -UHDK_API uhdk_status UHDK_CALL uhdk_app_set_menu_async(uhdk_app *, uhdk_menu *, - uhdk_request_id *out); /* NULL clears. */ -UHDK_API uhdk_status UHDK_CALL uhdk_window_set_menu_async(uhdk_window *, uhdk_menu *, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_menu_popup_async(uhdk_menu *, uhdk_window *, uhdk_point, - uhdk_request_id *out); -UHDK_API uhdk_status UHDK_CALL uhdk_app_set_icon_async(uhdk_app *, const uhdk_image *, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_menu_create( + uhdk_app *app, + const uhdk_menu_item *items, + uint64_t len, + uhdk_menu **out +); +UHDK_API uhdk_status UHDK_CALL uhdk_menu_set_item_flags_async( + uhdk_menu *menu, + uint64_t command_id, + uhdk_menu_flags flags, + uhdk_request_id *out +); +UHDK_API void UHDK_CALL uhdk_menu_retain(uhdk_menu *menu); +UHDK_API void UHDK_CALL uhdk_menu_release(uhdk_menu *menu); +UHDK_API uhdk_status UHDK_CALL uhdk_app_set_menu_async( + uhdk_app *app, + uhdk_menu *menu, + uhdk_request_id *out +); /* NULL clears. */ +UHDK_API uhdk_status UHDK_CALL uhdk_window_set_menu_async( + uhdk_window *window, + uhdk_menu *menu, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_menu_popup_async( + uhdk_menu *menu, + uhdk_window *window, + uhdk_point position, + uhdk_request_id *out +); +UHDK_API uhdk_status UHDK_CALL uhdk_app_set_icon_async( + uhdk_app *app, + const uhdk_image *image, + uhdk_request_id *out +); /* Successful shell completion means handed off to the OS, not that a target * application finished loading. URLs are not shell command strings. */ -UHDK_API uhdk_status UHDK_CALL uhdk_shell_open_url_async(uhdk_app *, uhdk_text url, - uhdk_request_id *out); +UHDK_API uhdk_status UHDK_CALL uhdk_shell_open_url_async( + uhdk_app *app, + uhdk_text url, + uhdk_request_id *out +); typedef uint32_t uhdk_change_flags; enum { @@ -663,7 +797,7 @@ enum { * as remove/create; no rename pairing is promised. RESCAN_REQUIRED means inspect * current state. Pending notifications are suppressed after destroy; already borrowed batches remain valid. */ UHDK_API uhdk_status UHDK_CALL uhdk_watch_create_async(uhdk_file *root, uhdk_request_id *out); -UHDK_API void UHDK_CALL uhdk_watch_destroy(uhdk_watch *); +UHDK_API void UHDK_CALL uhdk_watch_destroy(uhdk_watch *watch); typedef uint32_t uhdk_appearance; enum { @@ -678,7 +812,7 @@ typedef struct uhdk_preferences { uhdk_bool reduced_motion; uhdk_bool high_contrast; } uhdk_preferences; -UHDK_API uhdk_status UHDK_CALL uhdk_app_get_preferences(uhdk_app *, uhdk_preferences *out); +UHDK_API uhdk_status UHDK_CALL uhdk_app_get_preferences(uhdk_app *app, uhdk_preferences *out); /* Event payloads. Window is NULL for app-wide events. time_ns is monotonic time * relative to app creation, not wall time. Do not persist event pointers. @@ -753,86 +887,119 @@ enum { UHDK_EDIT_TAB }; +/* Event data is a pointer to the named payload type selected by type. + * All payloads are immutable and naturally aligned. data_size is the payload + * size in bytes; check it before accessing fields introduced by newer versions. + * Payload-free events have NULL data and data_size == 0. Unknown types may be + * ignored. Event envelopes have a fixed stride within each returned batch. + */ +typedef struct uhdk_key_event { + uint32_t physical_key; + uhdk_key logical_key; + uhdk_modifier_flags modifiers; + uhdk_bool pressed; + uhdk_bool repeat; +} uhdk_key_event; + +typedef struct uhdk_pointer_event { + uhdk_point position; + uhdk_point delta; /* delta uses logical units. */ + uhdk_pointer_button button; + uhdk_modifier_flags modifiers; + uhdk_bool pressed; + uint32_t click_count; +} uhdk_pointer_event; + +typedef struct uhdk_scroll_event { + uhdk_point position; + uhdk_point delta; /* Positive scroll = toward right/bottom. */ + uhdk_scroll_unit unit; + uhdk_scroll_phase phase; + uhdk_modifier_flags modifiers; + uhdk_bool momentum; +} uhdk_scroll_event; + +typedef struct uhdk_text_event { + uhdk_text text; + uint64_t revision; + uhdk_bool has_replacement; + uint32_t reserved; + uint64_t replacement_start; + uint64_t replacement_len; + uint64_t selection_start; + uint64_t selection_len; /* Within preedit text. */ +} uhdk_text_event; + +typedef struct uhdk_edit_event { + uhdk_edit_command command; + uhdk_bool extend_selection; +} uhdk_edit_event; + +typedef struct uhdk_drop_files_event { + uhdk_file_list files; + uhdk_point position; +} uhdk_drop_files_event; + +typedef struct uhdk_menu_event { + uint64_t command_id; +} uhdk_menu_event; + +typedef struct uhdk_watch_event { + uhdk_watch *watch; + uhdk_change_flags flags; + uhdk_status status; /* Non-OK: watch failure; rescan/recreate as needed. */ + uhdk_file *file; /* Nullable when only the watched root is known. */ +} uhdk_watch_event; + +typedef struct uhdk_activation_event { + uhdk_bool active; +} uhdk_activation_event; +typedef struct uhdk_pointer_mode_event { + uhdk_pointer_mode mode; +} uhdk_pointer_mode_event; +typedef struct uhdk_window_result { + uhdk_window *window; +} uhdk_window_result; +typedef struct uhdk_gl_context_result { + uhdk_gl_context *gl_context; +} uhdk_gl_context_result; +typedef struct uhdk_watch_result { + uhdk_watch *watch; +} uhdk_watch_result; + +/* Request data mapping (only valid on UHDK_STATUS_OK): + * FILE_DIALOG: uhdk_file_list; CLIPBOARD_TEXT: uhdk_text; + * WINDOW: uhdk_window_result; GL_CONTEXT: uhdk_gl_context_result; + * WATCH: uhdk_watch_result; FILE_READ: uhdk_bytes; FILE_INFO: uhdk_file_info. + * SHELL, COMMAND, FILE_WRITE, MENU_POPUP and failures have NULL/0 data. + */ typedef struct uhdk_request_result { uhdk_request_id id; uhdk_request_kind kind; uhdk_status status; uhdk_text diagnostic; /* Optional; for humans, never parse for logic. */ - union { - uhdk_file_list files; /* FILE_DIALOG */ - uhdk_text text; /* CLIPBOARD_TEXT */ - uhdk_window *window; - uhdk_gl_context *gl_context; - uhdk_watch *watch; - uhdk_bytes bytes; - uhdk_file_info info; - } value; /* Only valid on UHDK_STATUS_OK; SHELL/COMMAND/FILE_WRITE/MENU_POPUP have no payload. */ + const void *data; + uint64_t data_size; } uhdk_request_result; +/* Event data mapping: + * APP_ACTIVATION: uhdk_activation_event; OPEN_FILES: uhdk_file_list; + * WINDOW_METRICS: uhdk_window_metrics; PREFERENCES_CHANGED: uhdk_preferences; + * KEY: uhdk_key_event; POINTER_ENTER/LEAVE/MOVE/BUTTON: uhdk_pointer_event; + * POINTER_MODE_CHANGED: uhdk_pointer_mode_event; SCROLL: uhdk_scroll_event; + * TEXT_COMMIT/PREEDIT: uhdk_text_event; EDIT_COMMAND: uhdk_edit_event; + * DROP_FILES: uhdk_drop_files_event; MENU_COMMAND: uhdk_menu_event; + * WATCH: uhdk_watch_event; REQUEST_DONE: uhdk_request_result. + * All other events have NULL/0 data. + */ struct uhdk_event { uint32_t struct_size; uhdk_event_type type; uint64_t time_ns; uint64_t input_id; /* Correlates raw key and native edit/text; zero for non-input. */ uhdk_window *window; - union { - uhdk_bool active; - uhdk_file_list files; - uhdk_window_metrics metrics; - uhdk_preferences preferences; - struct { - uint32_t physical_key; - uhdk_key logical_key; - uhdk_modifier_flags modifiers; - uhdk_bool pressed; - uhdk_bool repeat; - } key; - struct { - uhdk_point position; - uhdk_point delta; /* delta uses logical units. */ - uhdk_pointer_button button; - uhdk_modifier_flags modifiers; - uhdk_bool pressed; - uint32_t click_count; - } pointer; - uhdk_pointer_mode pointer_mode; - struct { - uhdk_point position; - uhdk_point delta; /* Positive scroll = toward right/bottom. */ - uhdk_scroll_unit unit; - uhdk_scroll_phase phase; - uhdk_modifier_flags modifiers; - uhdk_bool momentum; - } scroll; - struct { - uhdk_text text; - uint64_t revision; - uhdk_bool has_replacement; - uint32_t reserved; - uint64_t replacement_start; - uint64_t replacement_len; - uint64_t selection_start; - uint64_t selection_len; /* Within preedit text. */ - } text; - struct { - uhdk_edit_command command; - uhdk_bool extend_selection; - } edit; - struct { - uhdk_file_list files; - uhdk_point position; - } drop_files; - struct { - uint64_t command_id; - } menu; - struct { - uhdk_watch *watch; - uhdk_change_flags flags; - uhdk_status status; /* Non-OK: watch failure; rescan/recreate as needed. */ - uhdk_file *file; /* Nullable when only the watched root is known. */ - } watch; - uhdk_request_result request; - } data; + const void *data; + uint64_t data_size; }; /* Lifecycle policy: close/quit requests are advisory. Destroy the window or call @@ -852,7 +1019,43 @@ struct uhdk_event { * semantic trees, notifications, persistent access bookmarks and streaming data. */ +#ifdef UHDK_ENABLE_NATIVE +/* Experimental platform escape hatch; native handles may require OS headers. */ +/* Platform escape hatch. Returned handles are borrowed and must not be released. + * macOS: primary=NSWindow*, secondary=NSView*; Windows: primary=HWND. + * Wayland: primary=wl_display*, secondary=wl_surface*. + * X11: primary=Display*, value=XID. Lifetime ends with the Uhdk window. + * With UHDK_WINDOW_OPENGL, Uhdk owns drawable setup; do not replace its native + * view/surface or pixel format. Otherwise the external renderer owns setup. + * Borrowing a handle does not change platform threading rules: AppKit window + * and view operations still belong on the macOS main thread. + */ +typedef struct uhdk_native_window { + uint32_t struct_size; + uhdk_backend backend; + void *primary; + void *secondary; + uint64_t value; +} uhdk_native_window; +UHDK_API uhdk_status UHDK_CALL uhdk_window_get_native(uhdk_window *window, uhdk_native_window *out); +#endif /* UHDK_ENABLE_NATIVE */ + #ifdef __cplusplus } /* extern "C" */ #endif +#ifdef UHDK_ENABLE_OPENGL +/* GLAD 2: OpenGL 3.3 core, no extensions, explicit per-context function table. + * Do not combine with platform GL headers or GL/glcorearb.h. + * The loader implementation is linked into libuhdk; consumers must not define + * GLAD_GL_IMPLEMENTATION when linking that library. + * Load a GladGLContext via gladLoadGLContextUserPtr and an adapter calling + * uhdk_gl_context_get_proc, inside a begin_frame/end_frame bracket. Keep one + * table per context and use it only while that context is current. + */ +#if defined(UHDK_SHARED) +#define GLAD_API_CALL_EXPORT +#endif +#include "glad/gl.h" +#endif /* UHDK_ENABLE_OPENGL */ + #endif /* UHDK_H_INCLUDED */ diff --git a/include/uhdk_native.h b/include/uhdk_native.h deleted file mode 100644 index a94db3b..0000000 --- a/include/uhdk_native.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef UHDK_NATIVE_H_INCLUDED -#define UHDK_NATIVE_H_INCLUDED - -/* Optional experimental platform escape hatch; outside the v0.1 core contract. - * Native handles may require OS headers. Not needed for Uhdk OpenGL rendering. - */ -#include "uhdk.h" -#ifdef __cplusplus -extern "C" { -#endif - -/* Platform escape hatch. Returned handles are borrowed and must not be released. - * macOS: primary=NSWindow*, secondary=NSView*; Windows: primary=HWND. - * Wayland: primary=wl_display*, secondary=wl_surface*. - * X11: primary=Display*, value=XID. Lifetime ends with the Uhdk window. - * With UHDK_WINDOW_OPENGL, Uhdk owns drawable setup; do not replace its native - * view/surface or pixel format. Otherwise the external renderer owns setup. - * Borrowing a handle does not change platform threading rules: AppKit window - * and view operations still belong on the macOS main thread. - */ -typedef struct uhdk_native_window { - uint32_t struct_size; - uhdk_backend backend; - void *primary; - void *secondary; - uint64_t value; -} uhdk_native_window; -UHDK_API uhdk_status UHDK_CALL uhdk_window_get_native(uhdk_window *, uhdk_native_window *out); - -#ifdef __cplusplus -} -#endif -#endif /* UHDK_NATIVE_H_INCLUDED */ diff --git a/include/uhdk_opengl.h b/include/uhdk_opengl.h deleted file mode 100644 index 12578c7..0000000 --- a/include/uhdk_opengl.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef UHDK_OPENGL_H_INCLUDED -#define UHDK_OPENGL_H_INCLUDED - -#include "uhdk.h" - -/* GLAD 2: OpenGL 3.3 core, no extensions, explicit per-context function table. - * Do not combine with platform GL headers or GL/glcorearb.h. - * The loader implementation is linked into libuhdk; consumers must not define - * GLAD_GL_IMPLEMENTATION when linking that library. - * Load a GladGLContext via gladLoadGLContextUserPtr and an adapter calling - * uhdk_gl_context_get_proc, inside a begin_frame/end_frame bracket. Keep one - * table per context and use it only while that context is current. - */ -#if defined(UHDK_SHARED) -#define GLAD_API_CALL_EXPORT -#endif -#include "glad/gl.h" - -#endif /* UHDK_OPENGL_H_INCLUDED */ diff --git a/include/uhdk_profile.h b/include/uhdk_profile.h deleted file mode 100644 index bcd9f4b..0000000 --- a/include/uhdk_profile.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef UHDK_PROFILE_H_INCLUDED -#define UHDK_PROFILE_H_INCLUDED - -/* Optional, consumer-owned instrumentation; no exported UHDK ABI or dependency. - * Define UHDK_PROFILE_BACKEND to a quoted adapter header before including uhdk.h. - * That header implements the macros below. Without it, arguments are not evaluated. - * Names (zone, thread, pool) must be string literals or have static lifetime. - * C zones: UHDK_PROFILE_BEGIN(token, "name"); ... UHDK_PROFILE_END(token); - * End on the same thread, in reverse nesting order, including early exits. - * C++ may use UHDK_PROFILE_SCOPE("name") for automatic scope exit. - * Memory: report successful non-null allocations, then free BEFORE releasing - * storage. A successful resize is free + alloc; failed allocations emit nothing. - * A pointer must use the same pool name for allocation and free. These hooks do - * not allocate/free memory themselves, intercept malloc, or transfer ownership. - */ -#ifdef UHDK_PROFILE_BACKEND -#include UHDK_PROFILE_BACKEND -#else -#define UHDK_PROFILE_BEGIN(token, name) ((void)0) -#define UHDK_PROFILE_END(token) ((void)0) -#define UHDK_PROFILE_SCOPE(name) ((void)0) -#define UHDK_PROFILE_FRAME() ((void)0) -#define UHDK_PROFILE_THREAD(name) ((void)0) -#define UHDK_PROFILE_ALLOC(ptr, size, pool) ((void)0) -#define UHDK_PROFILE_FREE(ptr, pool) ((void)0) -#endif - -#endif diff --git a/src/Application.zig b/src/Application.zig index 9ff2ea3..51a66d4 100644 --- a/src/Application.zig +++ b/src/Application.zig @@ -6,6 +6,7 @@ const abi = @import("abi.zig"); const Event = @import("Event.zig"); const Request = @import("Request.zig"); const Resource = @import("Resource.zig"); +const arenas = @import("arenas.zig"); const Application = @This(); gpa: std.mem.Allocator, @@ -24,7 +25,7 @@ io_tail: ?*Request = null, pending_head: ?*Event = null, pending_tail: ?*Event = null, published: ?*Event = null, -events: std.ArrayList(c.uhdk_event) = .empty, +scratch: arenas.BumpArena, name: []u8, epoch: u64, deadline: u64 = 0, @@ -47,7 +48,9 @@ pub fn create(gpa: std.mem.Allocator, io: std.Io, name: []const u8) !*Applicatio const self = try gpa.create(Application); errdefer gpa.destroy(self); const owned_name = try gpa.dupe(u8, name); - self.* = .{ .gpa = gpa, .io = io, .name = owned_name, .epoch = runtime.now(io) }; + errdefer gpa.free(owned_name); + const scratch = try arenas.BumpArena.init(gpa, io); + self.* = .{ .gpa = gpa, .io = io, .name = owned_name, .scratch = scratch, .epoch = runtime.now(io) }; return self; } pub fn lock(self: *Application) void { diff --git a/src/Event.zig b/src/Event.zig index 6445e1d..d74698c 100644 --- a/src/Event.zig +++ b/src/Event.zig @@ -8,5 +8,5 @@ const Event = @This(); next: ?*Event = null, arena: abi.Arena, gpa: std.mem.Allocator, -event: c.uhdk_event = std.mem.zeroes(c.uhdk_event), +event: c.z_event = std.mem.zeroes(c.z_event), pins: std.ArrayList(*Resource) = .empty, diff --git a/src/EventPayload.zig b/src/EventPayload.zig new file mode 100644 index 0000000..3c3e4d0 --- /dev/null +++ b/src/EventPayload.zig @@ -0,0 +1,60 @@ +//! Convert private producer unions to immutable, arena-owned public payloads. +const std = @import("std"); +const c = @import("native"); +const abi = @import("abi.zig"); +const Data = struct { ptr: ?*const anyopaque = null, size: u64 = 0 }; + +fn store(comptime T: type, allocator: std.mem.Allocator, value: T) !Data { + const ptr = try allocator.create(T); + ptr.* = value; + return .{ .ptr = ptr, .size = @sizeOf(T) }; +} + +fn files(allocator: std.mem.Allocator, value: c.uhdk_file_list) !c.uhdk_file_list { + if (value.len == 0) return .{ .ptr = null, .len = 0 }; + const ptr = try allocator.dupe(?*c.uhdk_file, value.ptr[0..@intCast(value.len)]); + return .{ .ptr = ptr.ptr, .len = ptr.len }; +} + +fn result(allocator: std.mem.Allocator, source: c.z_request_result) !c.uhdk_request_result { + const data: Data = if (source.status != abi.OK) .{} else switch (source.kind) { + c.UHDK_REQUEST_FILE_DIALOG => try store(c.uhdk_file_list, allocator, try files(allocator, source.value.files)), + c.UHDK_REQUEST_CLIPBOARD_TEXT => try store(c.uhdk_text, allocator, try abi.copyText(allocator, source.value.text)), + c.UHDK_REQUEST_FILE_READ => try store(c.uhdk_bytes, allocator, try abi.copyBytes(allocator, source.value.bytes)), + c.UHDK_REQUEST_FILE_INFO => try store(c.uhdk_file_info, allocator, source.value.info), + c.UHDK_REQUEST_WINDOW => try store(c.uhdk_window_result, allocator, .{ .window = source.value.window }), + c.UHDK_REQUEST_GL_CONTEXT => try store(c.uhdk_gl_context_result, allocator, .{ .gl_context = source.value.gl_context }), + c.UHDK_REQUEST_WATCH => try store(c.uhdk_watch_result, allocator, .{ .watch = source.value.watch }), + else => .{}, + }; + return .{ .id = source.id, .kind = source.kind, .status = source.status, .diagnostic = try abi.copyText(allocator, source.diagnostic), .data = data.ptr, .data_size = data.size }; +} + +pub fn pack(allocator: std.mem.Allocator, source: c.z_event) !c.uhdk_event { + const data: Data = switch (source.type) { + c.UHDK_EVENT_APP_ACTIVATION => try store(c.uhdk_activation_event, allocator, .{ .active = source.data.active }), + c.UHDK_EVENT_OPEN_FILES => try store(c.uhdk_file_list, allocator, try files(allocator, source.data.files)), + c.UHDK_EVENT_WINDOW_METRICS => try store(c.uhdk_window_metrics, allocator, source.data.metrics), + c.UHDK_EVENT_PREFERENCES_CHANGED => try store(c.uhdk_preferences, allocator, source.data.preferences), + c.UHDK_EVENT_KEY => try store(c.uhdk_key_event, allocator, source.data.key), + c.UHDK_EVENT_POINTER_ENTER, c.UHDK_EVENT_POINTER_LEAVE, c.UHDK_EVENT_POINTER_MOVE, c.UHDK_EVENT_POINTER_BUTTON => try store(c.uhdk_pointer_event, allocator, source.data.pointer), + c.UHDK_EVENT_POINTER_MODE_CHANGED => try store(c.uhdk_pointer_mode_event, allocator, .{ .mode = source.data.pointer_mode }), + c.UHDK_EVENT_SCROLL => try store(c.uhdk_scroll_event, allocator, source.data.scroll), + c.UHDK_EVENT_TEXT_COMMIT, c.UHDK_EVENT_TEXT_PREEDIT => data: { + var text = source.data.text; + text.text = try abi.copyText(allocator, text.text); + break :data try store(c.uhdk_text_event, allocator, text); + }, + c.UHDK_EVENT_EDIT_COMMAND => try store(c.uhdk_edit_event, allocator, source.data.edit), + c.UHDK_EVENT_DROP_FILES => data: { + var drop = source.data.drop_files; + drop.files = try files(allocator, drop.files); + break :data try store(c.uhdk_drop_files_event, allocator, drop); + }, + c.UHDK_EVENT_MENU_COMMAND => try store(c.uhdk_menu_event, allocator, source.data.menu), + c.UHDK_EVENT_WATCH => try store(c.uhdk_watch_event, allocator, source.data.watch), + c.UHDK_EVENT_REQUEST_DONE => try store(c.uhdk_request_result, allocator, try result(allocator, source.data.request)), + else => .{}, + }; + return .{ .struct_size = @sizeOf(c.uhdk_event), .type = source.type, .time_ns = source.time_ns, .input_id = source.input_id, .window = source.window, .data = data.ptr, .data_size = data.size }; +} diff --git a/src/api/app.zig b/src/api/app.zig index 03490d4..a770bd5 100644 --- a/src/api/app.zig +++ b/src/api/app.zig @@ -100,7 +100,7 @@ pub export fn uhdk_app_poll_events( defer poll_zone.end(); runtime.destroyEvents(application.published); application.published = null; - application.events.clearRetainingCapacity(); + application.scratch.reset(); out.* = .{ .ptr = null, .len = 0 }; application.lock(); defer application.unlock(); @@ -151,19 +151,30 @@ pub export fn uhdk_app_poll_events( while (cursor) |node| : (cursor = node.next) { count += 1; } - application.events.ensureTotalCapacity(application.gpa, count) catch return abi.OOM; + const allocator = application.scratch.allocator(); + const events = allocator.alloc(c.uhdk_event, count) catch return abi.OOM; + var len: usize = 0; + cursor = application.pending_head; + // Do not consume nodes until the entire batch has been converted. On OOM, + // the queue (and all terminal request completions) remains retryable. + while (cursor) |node| : (cursor = node.next) { + const event = node.event; + if (event.window != null and !runtime.fromObjectHandle(event.window).alive) continue; + if (event.type == c.UHDK_EVENT_WATCH and !runtime.fromObjectHandle(event.data.watch.watch).alive) continue; + events[len] = @import("../EventPayload.zig").pack(allocator, event) catch return abi.OOM; + len += 1; + } application.published = application.pending_head; application.pending_head = null; application.pending_tail = null; cursor = application.published; while (cursor) |node| : (cursor = node.next) { - const event = node.event; - if (event.window != null and !runtime.fromObjectHandle(event.window).alive) continue; - if (event.type == c.UHDK_EVENT_WATCH and !runtime.fromObjectHandle(event.data.watch.watch).alive) continue; - application.events.appendAssumeCapacity(event); - if (event.type == c.UHDK_EVENT_STOPPED) application.stopped_seen = true; + // Payload bytes now live in scratch. Keep only resource pins and nodes + // until the next poll; their private producer spans are no longer read. + _ = node.arena.reset(.free_all); + if (node.event.type == c.UHDK_EVENT_STOPPED) application.stopped_seen = true; } - out.* = .{ .ptr = application.events.items.ptr, .len = application.events.items.len }; + out.* = .{ .ptr = if (len == 0) null else events.ptr, .len = len }; application.stats.event_count +|= out.*.len; return abi.OK; } @@ -259,7 +270,7 @@ pub export fn uhdk_app_destroy(handle: ?*c.uhdk_app) void { std.debug.assert(c.z_native_is_main_thread() != 0 and !application.running); runtime.destroyEvents(application.published); runtime.destroyEvents(application.pending_head); - application.events.deinit(application.gpa); + application.scratch.deinit(); var cursor = application.objects; while (cursor) |object| { cursor = object.next; diff --git a/src/arenas.zig b/src/arenas.zig new file mode 100644 index 0000000..872d30a --- /dev/null +++ b/src/arenas.zig @@ -0,0 +1,520 @@ +//! UHDK-owned scratch arena and supporting paged storage. +//! Derived from RibtechEngine lib/p2 at 39697f9afba2933f92361586bf8802c22dfbe709. +//! Maintained as part of UHDK; this is not a vendored dependency. + +const std = @import("std"); + +pub const PoolStatistics = struct { + statName: []const u8, + instanceCount: usize, + memoryReserved: usize, + memoryUsed: usize, +}; + +// mutex based, fast bump-only allocator +pub const BumpArena = struct { + const PageSize = 8192 * 2; + const SmallAllocPages = PagedVectorAdvanced([PageSize]u8, 1); + + smallBumpOffset: usize = 0, + pageIndex: usize = 0, + + allocatedBytes: usize = 0, + allocCount: usize = 0, + + backing: std.mem.Allocator, + io_context: std.Io, + small: SmallAllocPages, + large: std.heap.ArenaAllocator, + mutex: std.Io.Mutex, + + pub var vtable: std.mem.Allocator.VTable = .{ + .alloc = alloc, + .free = free, + .resize = resize, + .remap = std.mem.Allocator.noRemap, + }; + + pub fn usage(self: *@This()) usize { + return self.allocatedBytes; + } + + pub fn getStatistics(self: *@This()) PoolStatistics { + return .{ + .statName = "BumpArena", + .instanceCount = self.allocCount, + .memoryReserved = self.small.pages.items.len * PageSize + + self.small.pages.capacity * @sizeOf(usize) + + self.large.queryCapacity(), + .memoryUsed = self.allocatedBytes, + }; + } + + pub fn init(backing: std.mem.Allocator, io_context: std.Io) !@This() { + return .{ + .backing = backing, + .io_context = io_context, + .small = try SmallAllocPages.init(backing), + .large = std.heap.ArenaAllocator.init(backing), + .mutex = .init, + }; + } + + pub fn io(self: *const @This()) std.Io { + return self.io_context; + } + + pub fn deinit(self: *@This()) void { + self.mutex.lockUncancelable(self.io()); + defer self.mutex.unlock(self.io()); + self.small.deinit(self.backing); + self.large.deinit(); + } + + pub fn alloc(ctx: *anyopaque, len: usize, ptr_align: std.mem.Alignment, ret_addr: usize) ?[*]u8 { + const self: *@This() = @ptrCast(@alignCast(ctx)); + self.mutex.lockUncancelable(self.io()); + defer self.mutex.unlock(self.io()); + + const alignment = ptr_align.toByteUnits(); + const result: [*]u8 = if (len > 4096 or alignment > 8) result: { + break :result self.largeAllocator().rawAlloc(len, ptr_align, ret_addr) orelse return null; + } else result: { + var offset = std.mem.alignForward(usize, self.smallBumpOffset, alignment); + if (offset + len > PageSize) { + // The first page exists at init; materialize its logical entry + // before advancing. append is transactional on allocation failure. + if (self.small.pages.items[0].len == 0) self.small.pages.items[0].len = 1; + self.small.append(self.backing, undefined) catch return null; + self.pageIndex += 1; + offset = 0; + } + const memory = self.small.getMutable(self.pageIndex)[offset .. offset + len]; + self.smallBumpOffset = offset + len; + break :result memory.ptr; + }; + self.allocatedBytes += len; + self.allocCount += 1; + return result; + } + + inline fn largeAllocator(self: *@This()) std.mem.Allocator { + return self.large.allocator(); + } + + pub fn free(ctx: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, ret_addr: usize) void { + // it's a bump allocator, it does nothing on free. + _ = ctx; + _ = buf; + _ = buf_align; + _ = ret_addr; + } + + // do not support resize in place. + pub fn resize(ctx: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, new_len: usize, ret_addr: usize) bool { + _ = ctx; + _ = buf; + _ = buf_align; + _ = new_len; + _ = ret_addr; + + return false; + } + + pub fn allocator(self: *@This()) std.mem.Allocator { + return .{ .ptr = self, .vtable = &vtable }; + } + + pub fn reset(self: *@This()) void { + self.mutex.lockUncancelable(self.io()); + defer self.mutex.unlock(self.io()); + + _ = self.large.reset(.free_all); + self.small.reset(self.backing); + self.smallBumpOffset = 0; + self.pageIndex = 0; + self.allocatedBytes = 0; + self.allocCount = 0; + } +}; + +test "bump arena initialization" { + var arena = try BumpArena.init(std.testing.allocator, std.testing.io); + defer arena.deinit(); +} + +test "reset clears usage" { + var arena = try BumpArena.init(std.testing.allocator, std.testing.io); + defer arena.deinit(); + + const allocator = arena.allocator(); + const mem = allocator.alloc(u8, 128) catch unreachable; + defer _ = mem; + + try std.testing.expect(arena.usage() >= 128); + arena.reset(); + try std.testing.expectEqual(@as(usize, 0), arena.usage()); +} + +// it's like an normal vector/ arraylist except it never invalidates old pointers. +// and never causes re-allocations. (excepting in the control block, which is just a vector of things) +// +// It is implemented with a control block which which contains a list of pages. +// these pages contain the actual memory of the vector. +// +// it has a growth factor, which can be configured. +// +// use cases: +// +// - this is highly performant for cases where +// - YOU WANT generally fast insertion time +// - YOU WANT fast random access time +// - YOU WANT cache-coherent and efficient linear iteration +// - YOU WANT to retain pointers across operations +// - YOU WANT to avoid reallocations (eg. if the data set is remarkably big) +// - YOU KNOW that you have linear growth factors and exponential growth is overkill. +// +// great for: +// - event logging. +// - output buffering. +// - recording timelines. + +pub fn PagedVectorAdvanced(comptime T: type, comptime growth: usize) type { + return struct { + const Page = struct { + data: [growth]T align(8) = undefined, + len: u32 = 0, + }; + + pages: std.ArrayListUnmanaged(*Page) = .empty, + currentPageId: usize = 0, + head: *T, + + pub fn init(allocator: std.mem.Allocator) !@This() { + var rv = @This(){ + .head = undefined, + }; + + const page = try allocator.create(Page); + errdefer allocator.destroy(page); + page.* = .{}; + try rv.pages.append(allocator, page); + rv.head = &rv.pages.items[0].data[0]; + return rv; + } + + pub fn capacity(self: @This()) usize { + return self.pages.items.len * growth; + } + + pub fn len(self: @This()) usize { + return (self.currentPageId) * growth + self.currentPage().len; + } + + pub fn getStatistics(self: *const @This()) PoolStatistics { + return .{ + .statName = "PagedVector", + .instanceCount = self.len(), + .memoryReserved = self.pages.items.len * @sizeOf(Page) + + self.pages.capacity * @sizeOf(*Page), + .memoryUsed = self.len() * @sizeOf(T), + }; + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + for (self.pages.items) |page| { + allocator.destroy(page); + } + + self.pages.deinit(allocator); + } + + fn maybeExpand(self: *@This(), allocator: std.mem.Allocator) !void { + if (self.currentPageId == self.pages.items.len - 1) { + const page = try allocator.create(Page); + errdefer allocator.destroy(page); + page.* = .{}; + try self.pages.append(allocator, page); + self.currentPageId = self.pages.items.len - 1; + } else { + self.currentPageId += 1; + } + } + + // Retain pages; no live element survives reset. + pub fn reset(self: *@This(), allocator: std.mem.Allocator) void { + _ = allocator; + for (self.pages.items) |page| page.len = 0; + self.currentPageId = 0; + self.head = &self.pages.items[0].data[0]; + } + + pub fn append(self: *@This(), allocator: std.mem.Allocator, value: T) !void { + if (self.currentPage().len == growth) try self.maybeExpand(allocator); + const page = self.currentPage(); + self.head = &page.data[page.len]; + self.head.* = value; + page.len += 1; + } + + pub fn appendSlice(self: *@This(), allocator: std.mem.Allocator, slice: []const T) !void { + for (slice) |value| { + try self.append(allocator, value); + } + } + + pub fn getMutable(self: *@This(), index: usize) *T { + const pageIndex = std.math.divFloor(usize, index, growth) catch unreachable; + return &self.pages.items[pageIndex].data[index % growth]; + } + + pub fn get(self: @This(), index: usize) *const T { + const pageIndex = std.math.divFloor(usize, index, growth) catch unreachable; + return &self.pages.items[pageIndex].data[index % growth]; + } + + // swaps the element at the index with the element currently pointed to by head + // then pops back the head, shrinking length by 1 + // + // Efficient removal of an element at an index but does not preserve order. + pub fn swapRemove(self: *@This(), index: usize) void { + const ptr = self.getMutable(index); + ptr.* = self.head.*; + _ = try self.pop(); + } + + fn currentPage(self: @This()) *Page { + return self.pages.items[self.currentPageId]; + } + + // // retrieves a value, shrinking length by 1 + pub fn pop(self: *@This()) !T { + const rv = self.head.*; + var page = self.currentPage(); + page.len -= 1; + if (page.len < 1) { + page.len = 0; + self.currentPageId -= 1; + } + + self.head = &self.currentPage().data[self.currentPage().len - 1]; + return rv; + } + }; +} + +pub fn PagedVectorUnmanaged(comptime T: type) type { + return PagedVectorAdvanced(T, 1024); +} + +pub fn PagedVector(comptime T: type) type { + return struct { + const VectorType = PagedVectorAdvanced(T, 1024); + + vector: VectorType, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) !@This() { + return .{ + .vector = try VectorType.init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *@This()) void { + self.vector.deinit(self.allocator); + } + + pub fn appendSlice(self: *@This(), slice: []const T) !void { + try self.vector.appendSlice(self.allocator, slice); + } + + pub fn append(self: *@This(), value: T) !void { + try self.vector.append(self.allocator, value); + } + + pub fn appendAndGet(self: *@This(), value: T) !*T { + const index = self.len(); + try self.append(value); + return self.getMutable(index); + } + + pub fn get(self: *const @This(), index: usize) *const T { + return self.vector.get(index); + } + + pub fn getMutable(self: *@This(), index: usize) *T { + return self.vector.getMutable(index); + } + + pub fn len(self: @This()) usize { + return self.vector.len(); + } + + pub fn capacity(self: @This()) usize { + return self.vector.capacity(); + } + + pub fn getStatistics(self: *const @This()) PoolStatistics { + return self.vector.getStatistics(); + } + + pub fn swapRemove(self: *@This(), index: usize) void { + self.vector.swapRemove(index); + } + + pub fn reset(self: *@This()) void { + self.vector.reset(self.allocator); + } + + // // retrieves a value, shrinking length by 1 + pub fn pop(self: *@This()) !T { + return try self.vector.pop(); + } + }; +} + +test "simple loading multiple pages." { + const TestStruct = struct { + lmao: usize, + }; + + const allocator = std.testing.allocator; + var vector = try PagedVectorAdvanced(TestStruct, 1024).init(std.testing.allocator); + defer vector.deinit(allocator); + + for (0..128) |j| { + try vector.append(allocator, .{ .lmao = j }); + } + + const ptr = vector.get(0); + const mutable = vector.getMutable(127); + + for (0..4096) |i| { + try vector.append(allocator, .{ .lmao = i }); + } + + // testing that pointers are not invalidated + std.debug.assert(ptr == vector.get(0)); + std.debug.assert(mutable == vector.getMutable(127)); + + std.debug.assert(vector.getMutable(4092) == vector.getMutable(4092)); + + // 4096 + 128 = 4224 = 4 pages + 128 extra entries + std.debug.assert(vector.pages.items.len == 5); + std.debug.assert(vector.capacity() == 1024 * 5); + std.debug.assert(vector.len() == 128 + 1024 * 4); + + // 4092 - 3072 = offset of 1020 in the third page. + const ptr4092 = vector.getMutable(4092); + std.debug.assert(vector.getMutable(4092) == &vector.pages.items[3].data[1020]); + + // lets try adding 1024 elements and see if pointers are still preserved. + for (0..1024) |_| { + try vector.append(allocator, .{ .lmao = 0 }); + } + + std.debug.assert(vector.getMutable(4092) == ptr4092); + std.debug.assert(vector.get(0).lmao == 0); + std.debug.assert(vector.get(1).lmao == 1); + std.debug.assert(vector.get(2).lmao == 2); + + // testing the pop() function + std.debug.assert(vector.len() == 128 + 1024 * 5); + _ = try vector.pop(); + _ = try vector.pop(); + _ = try vector.pop(); + + // testing the swapRemove Functionality + std.debug.assert(vector.len() == 128 + 1024 * 5 - 3); + vector.swapRemove(1); + std.debug.assert(vector.len() == 128 + 1024 * 5 - 4); + + for (0..1024) |_| { + vector.swapRemove(0); + } + std.debug.assert(vector.len() == 128 + 1024 * 5 - 4 - 1024); +} + +test "managed version" { + const allocator = std.testing.allocator; + + var vec = try PagedVector(struct { lmao: u64 }).init(allocator); + defer vec.deinit(); + + for (0..8192) |i| { + try vec.append(.{ .lmao = i }); + } + + std.debug.assert(vec.vector.pages.items.len == 8); +} + +test "reset retains pages and clears length" { + const allocator = std.testing.allocator; + + var vec = try PagedVectorAdvanced(u32, 4).init(allocator); + defer vec.deinit(allocator); + + for (0..10) |i| { + try vec.append(allocator, @intCast(i)); + } + + try std.testing.expect(vec.pages.items.len > 1); + vec.reset(allocator); + + try std.testing.expectEqual(@as(usize, 3), vec.pages.items.len); + try std.testing.expectEqual(@as(usize, 0), vec.len()); + + try vec.append(allocator, 42); + try std.testing.expectEqual(@as(u32, 42), vec.get(0).*); +} + +fn exercise(gpa: std.mem.Allocator) !void { + var arena = try BumpArena.init(gpa, std.testing.io); + defer arena.deinit(); + const a = arena.allocator(); + const first = try a.alloc(u8, 4096); + @memset(first, 17); + _ = try a.alloc(u8, 4096); + _ = try a.alloc(u8, 4096); + _ = try a.alloc(u8, 4093); + // Alignment rounding takes this allocation across the first page boundary. + const aligned = try a.create(u64); + aligned.* = 0x12345678; + try std.testing.expectEqual(@as(usize, 0), @intFromPtr(aligned) % @alignOf(u64)); + const OverAligned = struct { value: u8 align(256) }; + const over = try a.create(OverAligned); + over.* = .{ .value = 42 }; + try std.testing.expectEqual(@as(usize, 0), @intFromPtr(over) % 256); + const large = try a.alloc(u8, 20000); + @memset(large, 29); + for (first) |v| try std.testing.expectEqual(@as(u8, 17), v); + try std.testing.expectEqual(@as(u64, 0x12345678), aligned.*); + arena.reset(); + try std.testing.expectEqual(@as(usize, 0), arena.usage()); + const reused = try a.alloc(u8, 4096); + try std.testing.expectEqual(@intFromPtr(first.ptr), @intFromPtr(reused.ptr)); + for (0..12) |_| _ = try a.alloc(u8, 4096); + arena.reset(); + for (0..12) |_| _ = try a.alloc(u8, 4096); +} + +test "bump arena alignment page rollover reuse and all allocation failures" { + try std.testing.checkAllAllocationFailures(std.testing.allocator, exercise, .{}); +} + +test "bump arena failed page allocation can be retried" { + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); + var arena = try BumpArena.init(failing.allocator(), std.testing.io); + defer arena.deinit(); + const a = arena.allocator(); + for (0..4) |_| _ = try a.alloc(u8, 4096); + const before = arena.usage(); + failing.fail_index = failing.alloc_index; + try std.testing.expectError(error.OutOfMemory, a.alloc(u8, 4096)); + try std.testing.expectEqual(before, arena.usage()); + failing.fail_index = std.math.maxInt(usize); + const memory = try a.alloc(u8, 4096); + @memset(memory, 7); + try std.testing.expectEqual(before + 4096, arena.usage()); +} diff --git a/src/callbacks.zig b/src/callbacks.zig index caa8d83..1d485db 100644 --- a/src/callbacks.zig +++ b/src/callbacks.zig @@ -4,7 +4,7 @@ const c = @import("native"); const abi = @import("abi.zig"); const runtime = @import("runtime.zig"); -export fn z_emit(handle: ?*c.uhdk_app, event: [*c]const c.uhdk_event) void { +export fn z_emit(handle: ?*c.uhdk_app, event: [*c]const c.z_event) void { runtime.emit(handle, event); } @@ -12,7 +12,7 @@ export fn z_complete( handle: ?*c.uhdk_app, id: u64, status: abi.Status, - result: [*c]const c.uhdk_request_result, + result: [*c]const c.z_request_result, ) void { runtime.complete(handle, id, status, result); } @@ -38,7 +38,7 @@ export fn z_metrics(handle: ?*c.uhdk_window, value: [*c]const c.uhdk_window_metr object.app.lock(); object.payload.window.metrics = value.*; object.app.unlock(); - var event = std.mem.zeroes(c.uhdk_event); + var event = std.mem.zeroes(c.z_event); event.type = c.UHDK_EVENT_WINDOW_METRICS; event.window = handle; event.data.metrics = value.*; @@ -50,7 +50,7 @@ export fn z_preferences(handle: ?*c.uhdk_app, value: [*c]const c.uhdk_preference application.lock(); application.preferences = value.*; application.unlock(); - var event = std.mem.zeroes(c.uhdk_event); + var event = std.mem.zeroes(c.z_event); event.type = c.UHDK_EVENT_PREFERENCES_CHANGED; event.data.preferences = value.*; c.z_emit(handle, &event); diff --git a/src/core_tests.zig b/src/core_tests.zig index cc01641..0c924f5 100644 --- a/src/core_tests.zig +++ b/src/core_tests.zig @@ -7,7 +7,7 @@ const abi = @import("abi.zig"); fn destroyApplication(application: *Application) void { runtime.destroyEvents(application.pending_head); runtime.destroyEvents(application.published); - application.events.deinit(application.gpa); + application.scratch.deinit(); const gpa = application.gpa; gpa.free(application.name); application.* = undefined; @@ -37,7 +37,7 @@ test "publication copies text and preserves batches across ordinary calls" { const application = try Application.create(std.testing.allocator, std.testing.io, "events"); defer destroyApplication(application); var source = [_]u8{ 'h', 'i' }; - var event = std.mem.zeroes(c.uhdk_event); + var event = std.mem.zeroes(c.z_event); event.type = c.UHDK_EVENT_TEXT_COMMIT; event.data.text.text = .{ .ptr = &source, .len = source.len }; runtime.emit(runtime.toAppHandle(application), &event); @@ -99,3 +99,68 @@ test "tick statistics exclude blocked time" { try std.testing.expectEqual(@as(u64, 1), stats.tick_count); try std.testing.expect(stats.tick_time_ns < wall_time / 2); } + +test "public payload scratch lifetime empty polls and resource pins" { + const app_api = @import("api/app.zig"); + const application = try Application.create(std.testing.allocator, std.testing.io, "payloads"); + defer destroyApplication(application); + application.running = true; + application.worker_id = std.Thread.getCurrentId(); + const handle = runtime.toAppHandle(application); + const file = try runtime.createObject(application, c.Z_KIND_FILE); + defer runtime.release(file); + var handles = [_]?*c.uhdk_file{@ptrCast(file)}; + var event = std.mem.zeroes(c.z_event); + event.type = c.UHDK_EVENT_OPEN_FILES; + event.data.files = .{ .ptr = &handles, .len = 1 }; + runtime.emit(handle, &event); + try std.testing.expectEqual(@as(usize, 2), file.refs); + var batch: c.uhdk_event_list = undefined; + try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_poll_events(handle, 0, &batch)); + try std.testing.expectEqual(@as(u64, 1), batch.len); + try std.testing.expectEqual(@as(u64, @sizeOf(c.uhdk_file_list)), batch.ptr[0].data_size); + const payload: *const c.uhdk_file_list = @ptrCast(@alignCast(batch.ptr[0].data.?)); + try std.testing.expectEqual(@intFromPtr(file), @intFromPtr(payload.ptr[0].?)); + try std.testing.expectEqual(@as(usize, 2), file.refs); + // Invalid calls must not reset a borrow. Valid empty polls must release it. + try std.testing.expectEqual(@as(abi.Status, abi.INVALID), app_api.uhdk_app_poll_events(handle, 0, null)); + try std.testing.expectEqual(@as(u64, 1), payload.len); + try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_poll_events(handle, 0, &batch)); + try std.testing.expectEqual(@as(u64, 0), batch.len); + try std.testing.expectEqual(@as(usize, 1), file.refs); + try std.testing.expectEqual(@as(usize, 0), application.scratch.usage()); +} + +test "poll allocation failure keeps queued terminal completion retryable" { + const app_api = @import("api/app.zig"); + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); + const application = try Application.create(failing.allocator(), std.testing.io, "retry"); + defer destroyApplication(application); + application.running = true; + application.worker_id = std.Thread.getCurrentId(); + const handle = runtime.toAppHandle(application); + var bytes = [_]u8{42} ** 20000; + var event = std.mem.zeroes(c.z_event); + event.type = c.UHDK_EVENT_REQUEST_DONE; + event.data.request = .{ .id = 123, .kind = c.UHDK_REQUEST_FILE_READ, .status = abi.OK, .diagnostic = .{ .ptr = null, .len = 0 }, .value = .{ .bytes = .{ .ptr = &bytes, .len = bytes.len } } }; + runtime.emit(handle, &event); + @memset(&bytes, 0); + failing.fail_index = failing.alloc_index; + var batch: c.uhdk_event_list = undefined; + try std.testing.expectEqual(@as(abi.Status, abi.OOM), app_api.uhdk_app_poll_events(handle, 0, &batch)); + try std.testing.expectEqual(@as(u64, 0), batch.len); + try std.testing.expect(application.pending_head != null); + failing.fail_index = std.math.maxInt(usize); + try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_poll_events(handle, 0, &batch)); + try std.testing.expectEqual(@as(u64, 1), batch.len); + const result: *const c.uhdk_request_result = @ptrCast(@alignCast(batch.ptr[0].data.?)); + try std.testing.expectEqual(@as(u64, 123), result.id); + const data: *const c.uhdk_bytes = @ptrCast(@alignCast(result.data.?)); + try std.testing.expectEqual(@as(u64, bytes.len), data.len); + for (data.ptr[0..@intCast(data.len)]) |b| try std.testing.expectEqual(@as(u8, 42), b); + try std.testing.expect(application.pending_head == null); + // Producer payload storage has been freed; the public copy is in scratch. + try std.testing.expectEqual(@as(usize, 0), application.published.?.arena.queryCapacity()); + try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_poll_events(handle, 0, &batch)); + try std.testing.expectEqual(@as(u64, 0), batch.len); +} diff --git a/src/macos/native.m b/src/macos/native.m index bfb2a4e..f40d779 100644 --- a/src/macos/native.m +++ b/src/macos/native.m @@ -75,7 +75,7 @@ static uhdk_modifier_flags mods(NSEventModifierFlags f) { @property(nonatomic) uint64_t currentInput; @property(nonatomic) uint64_t lastEdit; @property(nonatomic) uhdk_rect caret; -- (void)emit:(uhdk_event *)event; +- (void)emit:(z_event *)event; - (void)cancelMarked; - (void)applyState:(const uhdk_text_state *)state; @end @@ -197,14 +197,14 @@ static uhdk_key logical(NSEvent *e) { (void)event; return YES; } -- (void)emit:(uhdk_event *)e { +- (void)emit:(z_event *)e { e->window = self.owner.handle; e->input_id = self.currentInput; z_emit(host.app, e); } - (void)drawRect:(NSRect)rect { (void)rect; - uhdk_event e = {.type = UHDK_EVENT_WINDOW_REDRAW}; + z_event e = {.type = UHDK_EVENT_WINDOW_REDRAW}; [self emit:&e]; } - (void)setFrameSize:(NSSize)size { @@ -236,7 +236,7 @@ static uhdk_key logical(NSEvent *e) { } - (void)key:(NSEvent *)event pressed:(BOOL)pressed { self.currentInput = ++self.nextInput; - uhdk_event e = {.type = UHDK_EVENT_KEY}; + z_event e = {.type = UHDK_EVENT_KEY}; e.data.key.physical_key = physical(event.keyCode); e.data.key.logical_key = logical(event); e.data.key.modifiers = mods(event.modifierFlags); @@ -277,7 +277,7 @@ static uhdk_key logical(NSEvent *e) { break; } self.currentInput = ++self.nextInput; - uhdk_event e = {.type = UHDK_EVENT_KEY}; + z_event e = {.type = UHDK_EVENT_KEY}; e.data.key.physical_key = physical(event.keyCode); e.data.key.modifiers = newMods; e.data.key.pressed = @@ -291,7 +291,7 @@ static uhdk_key logical(NSEvent *e) { } - (void)pointer:(NSEvent *)event type:(uint32_t)type pressed:(BOOL)pressed { NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - uhdk_event e = {.type = type}; + z_event e = {.type = type}; e.data.pointer.position = (uhdk_point){p.x, p.y}; e.data.pointer.delta = (uhdk_point){event.deltaX, event.deltaY}; e.data.pointer.modifiers = mods(event.modifierFlags); @@ -341,7 +341,7 @@ static uhdk_key logical(NSEvent *e) { } - (void)scrollWheel:(NSEvent *)event { NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - uhdk_event e = {.type = UHDK_EVENT_SCROLL}; + z_event e = {.type = UHDK_EVENT_SCROLL}; e.data.scroll.position = (uhdk_point){p.x, p.y}; e.data.scroll.delta = (uhdk_point){-event.scrollingDeltaX, -event.scrollingDeltaY}; e.data.scroll.unit = @@ -383,7 +383,7 @@ static uhdk_key logical(NSEvent *e) { selection:(NSRange)sel explicit:(BOOL)explicitRange { [self editID]; - uhdk_event e = {.type = type}; + z_event e = {.type = type}; e.data.text.text = utf8(s); e.data.text.revision = self.revision; e.data.text.has_replacement = explicitRange; @@ -418,7 +418,7 @@ static uhdk_key logical(NSEvent *e) { [self.textBuffer replaceCharactersInRange:replace withString:s]; self.selection = NSMakeRange(replace.location + s.length, 0); if (hadMarked) { - uhdk_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END}; + z_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END}; [self emit:&e]; } if (ownInput) @@ -463,7 +463,7 @@ static uhdk_key logical(NSEvent *e) { self.selection = self.originalRange; self.marked = NSMakeRange(NSNotFound, 0); [self editID]; - uhdk_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END}; + z_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END}; [self emit:&e]; } [self.inputContext discardMarkedText]; @@ -542,7 +542,7 @@ static uhdk_key logical(NSEvent *e) { return; } [self editID]; - uhdk_event e = {.type = UHDK_EVENT_EDIT_COMMAND}; + z_event e = {.type = UHDK_EVENT_EDIT_COMMAND}; e.data.edit.command = command.unsignedIntValue; e.data.edit.extend_selection = extend; [self emit:&e]; @@ -608,7 +608,7 @@ static uhdk_key logical(NSEvent *e) { files[n++] = f; } NSPoint point = [self convertPoint:sender.draggingLocation fromView:nil]; - uhdk_event e = {.type = UHDK_EVENT_DROP_FILES}; + z_event e = {.type = UHDK_EVENT_DROP_FILES}; e.data.drop_files.files = (uhdk_file_list){files, n}; e.data.drop_files.position = (uhdk_point){point.x, point.y}; [self emit:&e]; @@ -640,12 +640,12 @@ static uhdk_key logical(NSEvent *e) { : self.window.isZoomed ? UHDK_WINDOW_MAXIMIZED : UHDK_WINDOW_NORMAL; z_metrics(self.handle, &m); - uhdk_event e = {.type = UHDK_EVENT_WINDOW_REDRAW, .window = self.handle}; + z_event e = {.type = UHDK_EVENT_WINDOW_REDRAW, .window = self.handle}; z_emit(host.app, &e); } - (BOOL)windowShouldClose:(NSWindow *)sender { (void)sender; - uhdk_event e = {.type = UHDK_EVENT_WINDOW_CLOSE_REQUESTED, .window = self.handle}; + z_event e = {.type = UHDK_EVENT_WINDOW_CLOSE_REQUESTED, .window = self.handle}; z_emit(host.app, &e); return NO; } @@ -668,7 +668,7 @@ static uhdk_key logical(NSEvent *e) { [self.view cancelMarked]; if (self.view.capture) { self.view.capture = NO; - uhdk_event e = {.type = UHDK_EVENT_POINTER_MODE_CHANGED, .window = self.handle}; + z_event e = {.type = UHDK_EVENT_POINTER_MODE_CHANGED, .window = self.handle}; e.data.pointer_mode = UHDK_POINTER_NORMAL; z_emit(host.app, &e); } @@ -709,7 +709,7 @@ static uhdk_key logical(NSEvent *e) { w = candidate; break; } - uhdk_event e = {.type = UHDK_EVENT_MENU_COMMAND, .window = w.handle}; + z_event e = {.type = UHDK_EVENT_MENU_COMMAND, .window = w.handle}; e.data.menu.command_id = self.command; if (self.role == UHDK_MENU_ROLE_QUIT) { e.type = UHDK_EVENT_QUIT_REQUESTED; @@ -808,7 +808,7 @@ static NSMenu *makeMenu(uhdk_menu *menu, ZWindow *window) { @implementation ZHost - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { (void)sender; - uhdk_event e = {.type = UHDK_EVENT_QUIT_REQUESTED}; + z_event e = {.type = UHDK_EVENT_QUIT_REQUESTED}; z_emit(self.app, &e); return NSTerminateCancel; } @@ -818,13 +818,13 @@ static NSMenu *makeMenu(uhdk_menu *menu, ZWindow *window) { } - (void)applicationDidBecomeActive:(NSNotification *)note { (void)note; - uhdk_event e = {.type = UHDK_EVENT_APP_ACTIVATION}; + z_event e = {.type = UHDK_EVENT_APP_ACTIVATION}; e.data.active = YES; z_emit(self.app, &e); } - (void)applicationDidResignActive:(NSNotification *)note { (void)note; - uhdk_event e = {.type = UHDK_EVENT_APP_ACTIVATION}; + z_event e = {.type = UHDK_EVENT_APP_ACTIVATION}; e.data.active = NO; z_emit(self.app, &e); } @@ -838,7 +838,7 @@ static NSMenu *makeMenu(uhdk_menu *menu, ZWindow *window) { if (f) files[n++] = f; } - uhdk_event e = {.type = UHDK_EVENT_OPEN_FILES}; + z_event e = {.type = UHDK_EVENT_OPEN_FILES}; e.data.files = (uhdk_file_list){files, n}; z_emit(self.app, &e); for (NSUInteger i = 0; i < n; i++) @@ -967,7 +967,7 @@ static void watchEvents(ConstFSEventStreamRef stream, void *info, size_t count, [[path stringByDeletingLastPathComponent] isEqualToString:watch.path]); if (!match && !unknown && !invalid && ![path isEqualToString:watch.observed]) continue; - uhdk_event e = {.type = UHDK_EVENT_WATCH}; + z_event e = {.type = UHDK_EVENT_WATCH}; e.data.watch.watch = watch.handle; e.data.watch.status = UHDK_STATUS_OK; e.data.watch.flags = UHDK_CHANGE_RESCAN_REQUIRED; @@ -1091,7 +1091,7 @@ static uhdk_status showPanel(uhdk_app *app, const uhdk_file_dialog_desc *d, uint if (f) files[n++] = f; } - uhdk_request_result result = {0}; + z_request_result result = {0}; result.value.files = (uhdk_file_list){files, n}; z_complete(app, request, n == urls.count ? UHDK_STATUS_OK : UHDK_STATUS_OUT_OF_MEMORY, &result); @@ -1260,7 +1260,7 @@ int z_native_is_main_thread(void) { return NSThread.isMainThread; } uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t request, - uhdk_request_result *result) { + z_request_result *result) { @autoreleasepool { @try { ZWindow *w = cmd->target && z_object_kind(cmd->target) == Z_KIND_WINDOW @@ -1429,7 +1429,7 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque case Z_POINTER: w.view.capture = cmd->number != 0; { - uhdk_event e = {.type = UHDK_EVENT_POINTER_MODE_CHANGED, .window = w.handle}; + z_event e = {.type = UHDK_EVENT_POINTER_MODE_CHANGED, .window = w.handle}; e.data.pointer_mode = (uint32_t)cmd->number; z_emit(app, &e); } @@ -1561,7 +1561,7 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque break; } case Z_REDRAW: { - uhdk_event e = {.type = UHDK_EVENT_WINDOW_REDRAW, .window = w.handle}; + z_event e = {.type = UHDK_EVENT_WINDOW_REDRAW, .window = w.handle}; z_emit(app, &e); break; } diff --git a/src/native.h b/src/native.h index 389b035..4252c00 100644 --- a/src/native.h +++ b/src/native.h @@ -1,8 +1,49 @@ #ifndef UHDK_PRIVATE_NATIVE_H #define UHDK_PRIVATE_NATIVE_H +#define UHDK_ENABLE_NATIVE #include "uhdk.h" -#include "uhdk_native.h" #include +/* Private producer storage. Converted to public pointer payloads at poll. */ +typedef struct z_request_result { + uhdk_request_id id; + uhdk_request_kind kind; + uhdk_status status; + uhdk_text diagnostic; /* Optional; for humans, never parse for logic. */ + union { + uhdk_file_list files; /* FILE_DIALOG */ + uhdk_text text; /* CLIPBOARD_TEXT */ + uhdk_window *window; + uhdk_gl_context *gl_context; + uhdk_watch *watch; + uhdk_bytes bytes; + uhdk_file_info info; + } value; /* Only valid on UHDK_STATUS_OK; SHELL/COMMAND/FILE_WRITE/MENU_POPUP have no payload. */ +} z_request_result; + +typedef struct z_event { + uint32_t struct_size; + uhdk_event_type type; + uint64_t time_ns; + uint64_t input_id; /* Correlates raw key and native edit/text; zero for non-input. */ + uhdk_window *window; + union { + uhdk_bool active; + uhdk_file_list files; + uhdk_window_metrics metrics; + uhdk_preferences preferences; + uhdk_key_event key; + uhdk_pointer_event pointer; + uhdk_pointer_mode pointer_mode; + uhdk_scroll_event scroll; + uhdk_text_event text; + uhdk_edit_event edit; + uhdk_drop_files_event drop_files; + uhdk_menu_event menu; + uhdk_watch_event watch; + z_request_result request; + } data; +} z_event; + enum { Z_EVENT_SIZE = sizeof(uhdk_event), Z_EVENT_ALIGNMENT = _Alignof(uhdk_event), @@ -67,7 +108,7 @@ void z_native_wake(void); void z_native_again(void); void z_native_stop(void); void z_native_finish(void); -uhdk_status z_native_execute(uhdk_app *, const z_command *, uint64_t, uhdk_request_result *); +uhdk_status z_native_execute(uhdk_app *, const z_command *, uint64_t, z_request_result *); void z_native_cancel(uint64_t); void z_native_destroy(uint32_t kind, void *native); void *z_native_pool_push(void); @@ -79,8 +120,8 @@ void z_native_handles(void *, uhdk_native_window *); uhdk_status z_native_file_init(uhdk_file *, uhdk_bytes path, uhdk_text uri, void *grant); /* Zig callbacks, always return before native temporary spans expire. */ void z_tick(uhdk_app *); -void z_emit(uhdk_app *, const uhdk_event *); -void z_complete(uhdk_app *, uint64_t, uhdk_status, const uhdk_request_result *); +void z_emit(uhdk_app *, const z_event *); +void z_complete(uhdk_app *, uint64_t, uhdk_status, const z_request_result *); void *z_object_native(void *); void z_object_set_native(void *, void *); uint32_t z_object_kind(void *); diff --git a/src/root.zig b/src/root.zig index 3de47f0..c59a0a1 100644 --- a/src/root.zig +++ b/src/root.zig @@ -17,6 +17,7 @@ test { std.testing.refAllDecls(@import("abi.zig")); std.testing.refAllDecls(@import("FileOperation.zig")); _ = @import("core_tests.zig"); + _ = @import("arenas.zig"); } fn checkExports(comptime module: type) void { diff --git a/src/runtime.zig b/src/runtime.zig index cfa8fc0..78f8597 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -111,7 +111,7 @@ pub fn destroyEvents(head: ?*Event) void { } pub fn publishLocked(application: *Application, node: *Event) void { - node.event.struct_size = @sizeOf(c.uhdk_event); + node.event.struct_size = @sizeOf(c.z_event); node.event.time_ns = now(application.io) - application.epoch; node.next = null; if (application.pending_tail) |tail| tail.next = node else application.pending_head = node; @@ -153,7 +153,7 @@ pub fn copyPayload(node: *Event) !void { } pub fn simpleEvent(application: *Application, typ: u32) void { - var event = std.mem.zeroes(c.uhdk_event); + var event = std.mem.zeroes(c.z_event); event.type = typ; c.z_emit(toAppHandle(application), &event); } @@ -256,7 +256,7 @@ pub fn finish( application: *Application, command: *Request, status: abi.Status, - value: ?*const c.uhdk_request_result, + value: ?*const c.z_request_result, ) void { if (command.id == 0) { abandon(command); @@ -478,7 +478,7 @@ pub fn fileStep(application: *Application, command: *Request) bool { finish(application, command, fileError(err), null); return true; }; - var value = std.mem.zeroes(c.uhdk_request_result); + var value = std.mem.zeroes(c.z_request_result); switch (result) { .pending => return false, .written => {}, @@ -554,7 +554,7 @@ pub fn setMenuFlags(object: *Resource, id: u64, flags: u32) bool { return false; } -pub fn emit(handle: ?*c.uhdk_app, event: [*c]const c.uhdk_event) void { +pub fn emit(handle: ?*c.uhdk_app, event: [*c]const c.z_event) void { const application = fromAppHandle(handle); const node = application.gpa.create(Event) catch { application.lock(); @@ -599,7 +599,7 @@ pub fn complete( handle: ?*c.uhdk_app, id: u64, status: abi.Status, - result: [*c]const c.uhdk_request_result, + result: [*c]const c.z_request_result, ) void { const application = fromAppHandle(handle); application.lock(); @@ -647,7 +647,7 @@ pub fn tick(handle: ?*c.uhdk_app) void { application.io_tail = command; continue; } - var result = std.mem.zeroes(c.uhdk_request_result); + var result = std.mem.zeroes(c.z_request_result); if (command.data.op == c.Z_MENU_FLAGS) { application.lock(); const found = setMenuFlags(fromObjectHandle(command.data.target), command.data.number, @intCast(command.data.limit)); diff --git a/src/windows/native.c b/src/windows/native.c index a089a38..f294611 100644 --- a/src/windows/native.c +++ b/src/windows/native.c @@ -148,7 +148,7 @@ static Window *windowOf(void *handle) { return handle ? z_object_native(handle) : NULL; } static void emit(Window *w, uint32_t type) { - uhdk_event e = {0}; + z_event e = {0}; e.type = type; e.window = w ? w->handle : NULL; z_emit(host.app, &e); @@ -209,7 +209,7 @@ static void textEvent(Window *w, uint32_t type, const WCHAR *text, int length, i char *s = utf8(text, length, &len); if (!s) return; - uhdk_event e = {0}; + z_event e = {0}; e.type = type; e.window = w->handle; e.input_id = w->input_id = ++host.input_id; @@ -228,7 +228,7 @@ static void textEvent(Window *w, uint32_t type, const WCHAR *text, int length, i profileFree(s); } static void editEvent(Window *w, uint32_t command) { - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_EDIT_COMMAND; e.window = w->handle; e.input_id = w->input_id = ++host.input_id; @@ -248,7 +248,7 @@ static void menuEvent(Window *w, const uhdk_menu_item *item) { editEvent(w, UHDK_EDIT_COPY + item->role - UHDK_MENU_ROLE_COPY); return; } - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_MENU_COMMAND; e.window = w ? w->handle : NULL; e.data.menu.command_id = item->command_id; @@ -445,7 +445,7 @@ static void dropFiles(Window *w, HDROP drop) { POINT point; DragQueryPoint(drop, &point); if (used) { - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_DROP_FILES; e.window = w->handle; e.data.drop_files.files = (uhdk_file_list){files, used}; @@ -511,7 +511,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { metrics(w); break; case WM_ACTIVATEAPP: { - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_APP_ACTIVATION; e.data.active = !!wp; z_emit(host.app, &e); @@ -561,7 +561,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { w->tracking = TRUE; emit(w, UHDK_EVENT_POINTER_ENTER); } - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_POINTER_MOVE; e.window = w->handle; e.data.pointer.position = @@ -593,7 +593,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { msg == WM_XBUTTONUP; if (!up) SetFocus(hwnd); - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_POINTER_BUTTON; e.window = w->handle; e.data.pointer.position = @@ -613,7 +613,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { return 0; } case WM_CAPTURECHANGED: { - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_POINTER_MODE_CHANGED; e.window = w->handle; e.data.pointer_mode = UHDK_POINTER_NORMAL; @@ -628,7 +628,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { case WM_MOUSEHWHEEL: { POINT p = {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}; ScreenToClient(hwnd, &p); - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_SCROLL; e.window = w->handle; e.data.scroll.position = (uhdk_point){p.x / w->scale, p.y / w->scale}; @@ -663,7 +663,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { return 0; } } - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_KEY; e.window = w->handle; e.input_id = w->input_id = ++host.input_id; @@ -1194,7 +1194,7 @@ static void showDialog(void) { } else status = files ? UHDK_STATUS_OS_ERROR : UHDK_STATUS_OUT_OF_MEMORY; } - uhdk_request_result result = {0}; + z_request_result result = {0}; result.value.files = (uhdk_file_list){files, count}; host.dialog = NULL; host.dialog_id = 0; @@ -1279,7 +1279,7 @@ static uhdk_status createWatch(uhdk_file *root, uhdk_watch *handle) { return UHDK_STATUS_OK; } static void watchEvent(Watch *watch, uint32_t flags, uhdk_status status) { - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_WATCH; e.data.watch.watch = watch->handle; e.data.watch.flags = flags; @@ -1402,7 +1402,7 @@ void z_native_run(void) { while (host.running) { int result = GetMessageW(&message, NULL, 0, 0); if (result <= 0) { - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_QUIT_REQUESTED; z_emit(host.app, &e); if (result < 0) @@ -1578,7 +1578,7 @@ static uhdk_status clipboardRead(uint64_t id, uint64_t limit) { profileFree(text); return UHDK_STATUS_BUFFER_TOO_SMALL; } - uhdk_request_result result = {0}; + z_request_result result = {0}; result.value.text = (uhdk_text){text, len}; z_complete(host.app, id, UHDK_STATUS_OK, &result); profileFree(text); @@ -1632,7 +1632,7 @@ static uhdk_status setIcon(const uhdk_image *image) { return UHDK_STATUS_OK; } uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t request, - uhdk_request_result *result) { + z_request_result *result) { (void)app; Window *w = cmd->target && z_object_kind(cmd->target) == Z_KIND_WINDOW ? windowOf(cmd->target) : NULL; @@ -1765,7 +1765,7 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque ReleaseCapture(); break; } - uhdk_event e = {0}; + z_event e = {0}; e.type = UHDK_EVENT_POINTER_MODE_CHANGED; e.window = w->handle; e.data.pointer_mode = (uint32_t)cmd->number;