new header parser and generator

This commit is contained in:
Peter Li 2026-09-18 11:29:02 -07:00
parent 97c4b41e12
commit 8a1f8fbd28
20 changed files with 1224 additions and 328 deletions

22
LICENSE Normal file
View File

@ -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.

View File

@ -41,7 +41,7 @@ exe.root_module.addImport("uhdk", uhdk.module("uhdk"));
``` ```
`@import("uhdk")` provides translated public C declarations, including the bundled `@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 `uhdk` artifact is also available through `uhdk.artifact("uhdk")` for C consumers
or custom bindings. Set `.shared = true` to build a dynamic library; applications or custom bindings. Set `.shared = true` to build a dynamic library; applications
must arrange its runtime location/rpath. must arrange its runtime location/rpath.
@ -49,10 +49,24 @@ must arrange its runtime location/rpath.
## C interface ## C interface
- [uhdk.h](include/uhdk.h): desktop API, lifecycle, ownership, and threading contracts. - [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. - [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` The public interface uses pointer-and-length text, `char` booleans, and `float`
coordinates. The native bridge is private; consumers do not need Objective-C. 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 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. 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 ## 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 free macros. They compile away without evaluating arguments unless the consumer
defines `UHDK_PROFILE_BACKEND` to a quoted adapter header before including UHDK. defines `UHDK_PROFILE_BACKEND` to a quoted adapter header before including UHDK.
An adapter implements the macros documented in that header. Names require static An adapter implements the macros documented in that header. Names require static

View File

@ -27,10 +27,12 @@ pub fn build(b: *std.Build) void {
const unit_run = b.addRunArtifact(unit); const unit_run = b.addRunArtifact(unit);
b.step("test", "Run core library tests").dependOn(&unit_run.step); b.step("test", "Run core library tests").dependOn(&unit_run.step);
const public_c = b.addTranslateC(.{ const public_c = b.addTranslateC(.{
.root_source_file = b.path("include/uhdk_opengl.h"), .root_source_file = b.path("include/uhdk.h"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
public_c.defineCMacro("UHDK_ENABLE_OPENGL", "1");
public_c.defineCMacro("UHDK_ENABLE_NATIVE", "1");
public_c.addIncludePath(b.path("include")); public_c.addIncludePath(b.path("include"));
if (shared) public_c.defineCMacro("UHDK_SHARED", "1"); if (shared) public_c.defineCMacro("UHDK_SHARED", "1");
const public_module = b.addModule("uhdk", .{ const public_module = b.addModule("uhdk", .{

View File

@ -2,7 +2,8 @@
## GLAD 2.0.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 extensions, header-only output, explicit per-context tables (`--mx`), and no
platform loader. `src/glad.c` compiles its implementation into libuhdk. Applications platform loader. `src/glad.c` compiles its implementation into libuhdk. Applications
load a `GladGLContext` through their adapter for `uhdk_gl_context_get_proc`. 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 ## Original Khronos headers
Retained unmodified for consumers choosing their own loader. Do not combine 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. License notices are preserved in each file.
- `include/GL/glcorearb.h` - `include/GL/glcorearb.h`

View File

@ -9,6 +9,13 @@
* <stdint.h> supplies types only; this header requires no libc function calls. * <stdint.h> supplies types only; this header requires no libc function calls.
* Native OS frameworks may themselves depend on system runtimes. * 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: * Naming:
* - Types: uhdk_type; functions: uhdk_object_verb; constants: UHDK_CATEGORY_VALUE. * - Types: uhdk_type; functions: uhdk_object_verb; constants: UHDK_CATEGORY_VALUE.
* - create/destroy allocate and dispose unique objects; retain/release manage * - 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. * - NULL is allowed for a span's pointer only when its length is zero.
* - Input spans/descriptors are copied before return unless documented otherwise. * - Input spans/descriptors are copied before return unless documented otherwise.
* - No allocator ownership crosses the ABI. Release objects through Uhdk. * - No allocator ownership crosses the ABI. Release objects through Uhdk.
* - Event spans are borrowed until the next poll or app destruction. Copy spans * - Event batches and spans are borrowed until the next valid poll on the
* or retain files/menus to keep them. Each poll returns an immutable batch. * 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 * - 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 * 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. * app_wake, which is thread-safe. Native work is queued to the main thread.
@ -44,11 +53,33 @@
* except documented buffer-size queries. * except documented buffer-size queries.
* - The application owns rendering, document state, file encoding and layout. * - The application owns rendering, document state, file encoding and layout.
* Uhdk provides optional OpenGL 3.3 core context management. Other renderers * 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 <stdint.h> #include <stdint.h>
#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 #ifndef UHDK_API
#if defined(_WIN32) && defined(UHDK_SHARED) #if defined(_WIN32) && defined(UHDK_SHARED)
@ -185,7 +216,7 @@ typedef struct uhdk_event_list {
} uhdk_event_list; } uhdk_event_list;
#define UHDK_WAIT_FOREVER UINT64_MAX #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 { typedef struct uhdk_app_desc {
uint32_t struct_size; uint32_t struct_size;
uint32_t abi_version; uint32_t abi_version;
@ -196,27 +227,34 @@ typedef struct uhdk_app_desc {
} uhdk_app_desc; } uhdk_app_desc;
UHDK_API uint32_t UHDK_CALL uhdk_get_abi_version(void); 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_create(const uhdk_app_desc *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_run(
uhdk_app *app,
uhdk_app_main_fn main_fn,
void *user
); /* Once. */
/* Zero timeout does not wait; finite timeouts use monotonic nanoseconds. /* Zero timeout does not wait; finite timeouts use monotonic nanoseconds.
* STOPPED is delivered once; later polls return UNAVAILABLE. */ * 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_API uhdk_status UHDK_CALL uhdk_app_poll_events(
uhdk_event_list *out); uhdk_app *app,
UHDK_API void UHDK_CALL uhdk_app_quit(uhdk_app *); /* Accept/request shutdown. */ uint64_t timeout_ns,
UHDK_API void UHDK_CALL uhdk_app_wake(uhdk_app *); /* Thread-safe, coalesced WAKE event. */ 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. */ /* 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. /* 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 * 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 * 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 * 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. * 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 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 *); /* After run; releases child objects. */ 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 *); 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 *, uhdk_capability); 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 *, uhdk_request_id); 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. /* Application-thread CPU execution time in nanoseconds, not elapsed wall time.
* Bracket user update/input processing with begin_tick/end_tick, AFTER polling * 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; float frames_per_second;
} uhdk_app_stats; } uhdk_app_stats;
UHDK_API uhdk_status UHDK_CALL uhdk_app_begin_tick(uhdk_app *); 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 *); 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 *, uhdk_app_stats *out); 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 /* Logical coordinates: top-left origin, x right, y down. Window geometry uses
* content-area units. Framebuffer dimensions are pixels; use reported sizes, * content-area units. Framebuffer dimensions are pixels; use reported sizes,
@ -294,25 +332,46 @@ typedef struct uhdk_window_metrics {
uhdk_bool visible; uhdk_bool visible;
} uhdk_window_metrics; } uhdk_window_metrics;
UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async(uhdk_app *, const uhdk_window_desc *, UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async(
uhdk_request_id *out); uhdk_app *app,
UHDK_API void UHDK_CALL uhdk_window_destroy(uhdk_window *); const uhdk_window_desc *desc,
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_title_async(uhdk_window *, uhdk_text, uhdk_request_id *out
uhdk_request_id *out); );
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_size_async(uhdk_window *, uhdk_size, UHDK_API void UHDK_CALL uhdk_window_destroy(uhdk_window *window);
uhdk_request_id *out); UHDK_API uhdk_status UHDK_CALL uhdk_window_set_title_async(
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_state_async(uhdk_window *, uhdk_window_state, uhdk_window *window,
uhdk_request_id *out); uhdk_text title,
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_visible_async(uhdk_window *, uhdk_bool, uhdk_request_id *out
uhdk_request_id *out); );
UHDK_API uhdk_status UHDK_CALL UHDK_API uhdk_status UHDK_CALL uhdk_window_set_size_async(
uhdk_window_request_focus_async(uhdk_window *, uhdk_request_id *out); /* OS may decline. */ uhdk_window *window,
UHDK_API uhdk_status UHDK_CALL uhdk_window_get_metrics(uhdk_window *, uhdk_window_metrics *out); uhdk_size size,
UHDK_API void UHDK_CALL uhdk_window_invalidate(uhdk_window *); /* Coalesced redraw event. */ 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). /* Optional OpenGL context management (application/render thread).
* Include <uhdk_opengl.h> for bundled Khronos GL types, constants and PFNGL* * Define UHDK_ENABLE_OPENGL before including this header for bundled Khronos
* function-pointer types. This header alone does not introduce GL names. * 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, * 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 * 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. */ * type before calling. GL uses stdcall on Win32; Uhdk exports still use C ABI. */
typedef void(UHDK_GL_CALL *uhdk_gl_proc)(void); 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_create_async(
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_begin_frame(uhdk_gl_context *); uhdk_window *window,
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_end_frame(uhdk_gl_context *); uhdk_request_id *out
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_context_begin_frame(uhdk_gl_context *context);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_clear_current(uhdk_app *); 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. */ /* 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, UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_version(
uint32_t *out_minor); uhdk_gl_context *context,
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_swap_buffers(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 *context);
/* Explicitly set 0 (unsynchronized) or 1 (vsync); other values are invalid. /* Explicitly set 0 (unsynchronized) or 1 (vsync); other values are invalid.
* No initial interval is guaranteed. Unsupported control returns UNSUPPORTED; * No initial interval is guaranteed. Unsupported control returns UNSUPPORTED;
* compositor/driver policy may override presentation timing. */ * compositor/driver policy may override presentation timing. */
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_set_swap_interval(uhdk_gl_context *, UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_set_swap_interval(
int32_t interval); uhdk_gl_context *context,
int32_t interval
);
/* Requires this context current. Accepts pointer+length ASCII GL symbol names. /* Requires this context current. Accepts pointer+length ASCII GL symbol names.
* Missing symbols return NOT_FOUND. Pointers are valid until context destruction * 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 * 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. * not prove extension/version support; check GL version/extension availability.
* Loads both platform-library exports and driver-provided GL entry points. */ * 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_API uhdk_status UHDK_CALL uhdk_gl_context_get_proc(
uhdk_gl_proc *out); uhdk_gl_context *context,
uhdk_text name,
uhdk_gl_proc *out
);
/* Keyboard events are not text. Physical key = USB HID keyboard-page usage; /* Keyboard events are not text. Physical key = USB HID keyboard-page usage;
* 0 means unknown. Logical key = Unicode scalar, or UHDK_KEY_* below. * 0 means unknown. Logical key = Unicode scalar, or UHDK_KEY_* below.
@ -448,12 +518,18 @@ enum {
UHDK_POINTER_NORMAL = 0, UHDK_POINTER_NORMAL = 0,
UHDK_POINTER_CAPTURE = 1 UHDK_POINTER_CAPTURE = 1
}; };
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_cursor_shape_async(uhdk_window *, uhdk_cursor_shape, UHDK_API uhdk_status UHDK_CALL uhdk_window_set_cursor_shape_async(
uhdk_request_id *out); uhdk_window *window,
uhdk_cursor_shape shape,
uhdk_request_id *out
);
/* Capture delivers selection-drag events outside the window. Focus loss or OS /* Capture delivers selection-drag events outside the window. Focus loss or OS
* cancellation restores NORMAL and emits POINTER_MODE_CHANGED. */ * 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_API uhdk_status UHDK_CALL uhdk_window_set_pointer_mode_async(
uhdk_request_id *out); uhdk_window *window,
uhdk_pointer_mode mode,
uhdk_request_id *out
);
/* Text offsets/lengths are UTF-8 byte counts, always at scalar boundaries. /* Text offsets/lengths are UTF-8 byte counts, always at scalar boundaries.
* surrounding_text is a bounded snapshot; document_offset locates it in the * 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; uint64_t selection_len;
uhdk_rect caret; /* Window-local logical coordinates. */ uhdk_rect caret; /* Window-local logical coordinates. */
} uhdk_text_state; } uhdk_text_state;
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_input_async(uhdk_window *, uhdk_bool enabled, UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_input_async(
uhdk_request_id *out); uhdk_window *window,
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_state(uhdk_window *, const uhdk_text_state *); uhdk_bool enabled,
UHDK_API uhdk_status UHDK_CALL uhdk_window_cancel_composition_async(uhdk_window *, uhdk_request_id *out
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. /* 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 * 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_URI = 1,
UHDK_FILE_NATIVE_PATH = 2 UHDK_FILE_NATIVE_PATH = 2
}; };
UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_native_path(uhdk_app *, uhdk_bytes path, UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_native_path(
uhdk_file **out); uhdk_app *app,
UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_uri(uhdk_app *, uhdk_text uri, uhdk_bytes path,
uhdk_file **out); 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_create_from_uri(
UHDK_API uhdk_status UHDK_CALL uhdk_file_copy_representation(uhdk_file *, uhdk_file_representation, uhdk_app *app,
uint8_t *buffer, uint64_t capacity, uhdk_text uri,
uint64_t *out_len); 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; typedef uint32_t uhdk_file_type;
enum { enum {
@ -534,11 +628,17 @@ typedef struct uhdk_file_info {
* New POSIX files use mode 0600. No power-loss durability or conflict detection promise. * 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. * 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_API uhdk_status UHDK_CALL uhdk_file_read_all_async(
uhdk_request_id *out); uhdk_file *file,
UHDK_API uhdk_status UHDK_CALL uhdk_file_write_all_async(uhdk_file *, uhdk_bytes, uint64_t max_bytes,
uhdk_request_id *out); 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_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; typedef uint32_t uhdk_dialog_kind;
enum { enum {
@ -564,29 +664,40 @@ typedef struct uhdk_file_dialog_desc {
const uhdk_file_filter *filters; const uhdk_file_filter *filters;
uint64_t filters_len; uint64_t filters_len;
} uhdk_file_dialog_desc; } uhdk_file_dialog_desc;
UHDK_API uhdk_status UHDK_CALL uhdk_file_dialog_show_async(uhdk_app *, UHDK_API uhdk_status UHDK_CALL uhdk_file_dialog_show_async(
const uhdk_file_dialog_desc *, uhdk_app *app,
uhdk_request_id *out); const uhdk_file_dialog_desc *desc,
uhdk_request_id *out
);
/* Plain-text clipboard only. Writes copy UTF-8 before returning; empty text is /* Plain-text clipboard only. Writes copy UTF-8 before returning; empty text is
* valid. Reads negotiate native text formats internally and complete with text * 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 * 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. * 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_API uhdk_status UHDK_CALL uhdk_clipboard_write_text_async(
uhdk_request_id *out); uhdk_app *app,
UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_read_text_async(uhdk_app *, uint64_t max_bytes, uhdk_text text,
uhdk_request_id *out); 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 /* Incoming file drops only. When enabled, Uhdk negotiates copy-only file drops
* and emits DROP_FILES with borrowed file references plus the drop position. * 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 * the application decides whether to open them. No source deletion, outgoing
* drag, data offers, or application-visible drop-completion transaction. * 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_API uhdk_status UHDK_CALL uhdk_window_set_drop_enabled_async(
uhdk_request_id *out); uhdk_window *window,
uhdk_bool enabled,
uhdk_request_id *out
);
/* Menus are immutable snapshots. Command IDs are application-assigned, nonzero /* Menus are immutable snapshots. Command IDs are application-assigned, nonzero
* and unique within a menu tree. Shortcuts use logical keys + modifiers. * 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 *submenu; /* Retained by the created parent menu. */
} uhdk_menu_item; } uhdk_menu_item;
UHDK_API uhdk_status UHDK_CALL uhdk_menu_create(uhdk_app *, const uhdk_menu_item *, uint64_t len, UHDK_API uhdk_status UHDK_CALL uhdk_menu_create(
uhdk_menu **out); uhdk_app *app,
UHDK_API uhdk_status UHDK_CALL uhdk_menu_set_item_flags_async(uhdk_menu *, uint64_t command_id, const uhdk_menu_item *items,
uhdk_menu_flags, uint64_t len,
uhdk_request_id *out); uhdk_menu **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_menu_set_item_flags_async(
UHDK_API uhdk_status UHDK_CALL uhdk_app_set_menu_async(uhdk_app *, uhdk_menu *, uhdk_menu *menu,
uhdk_request_id *out); /* NULL clears. */ uint64_t command_id,
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_menu_async(uhdk_window *, uhdk_menu *, uhdk_menu_flags flags,
uhdk_request_id *out); 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 void UHDK_CALL uhdk_menu_retain(uhdk_menu *menu);
UHDK_API uhdk_status UHDK_CALL uhdk_app_set_icon_async(uhdk_app *, const uhdk_image *, UHDK_API void UHDK_CALL uhdk_menu_release(uhdk_menu *menu);
uhdk_request_id *out); 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 /* Successful shell completion means handed off to the OS, not that a target
* application finished loading. URLs are not shell command strings. */ * 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_API uhdk_status UHDK_CALL uhdk_shell_open_url_async(
uhdk_request_id *out); uhdk_app *app,
uhdk_text url,
uhdk_request_id *out
);
typedef uint32_t uhdk_change_flags; typedef uint32_t uhdk_change_flags;
enum { enum {
@ -663,7 +797,7 @@ enum {
* as remove/create; no rename pairing is promised. RESCAN_REQUIRED means inspect * 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. */ * 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 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; typedef uint32_t uhdk_appearance;
enum { enum {
@ -678,7 +812,7 @@ typedef struct uhdk_preferences {
uhdk_bool reduced_motion; uhdk_bool reduced_motion;
uhdk_bool high_contrast; uhdk_bool high_contrast;
} uhdk_preferences; } 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 /* 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. * relative to app creation, not wall time. Do not persist event pointers.
@ -753,58 +887,39 @@ enum {
UHDK_EDIT_TAB UHDK_EDIT_TAB
}; };
typedef struct uhdk_request_result { /* Event data is a pointer to the named payload type selected by type.
uhdk_request_id id; * All payloads are immutable and naturally aligned. data_size is the payload
uhdk_request_kind kind; * size in bytes; check it before accessing fields introduced by newer versions.
uhdk_status status; * Payload-free events have NULL data and data_size == 0. Unknown types may be
uhdk_text diagnostic; /* Optional; for humans, never parse for logic. */ * ignored. Event envelopes have a fixed stride within each returned batch.
union { */
uhdk_file_list files; /* FILE_DIALOG */ typedef struct uhdk_key_event {
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. */
} uhdk_request_result;
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; uint32_t physical_key;
uhdk_key logical_key; uhdk_key logical_key;
uhdk_modifier_flags modifiers; uhdk_modifier_flags modifiers;
uhdk_bool pressed; uhdk_bool pressed;
uhdk_bool repeat; uhdk_bool repeat;
} key; } uhdk_key_event;
struct {
typedef struct uhdk_pointer_event {
uhdk_point position; uhdk_point position;
uhdk_point delta; /* delta uses logical units. */ uhdk_point delta; /* delta uses logical units. */
uhdk_pointer_button button; uhdk_pointer_button button;
uhdk_modifier_flags modifiers; uhdk_modifier_flags modifiers;
uhdk_bool pressed; uhdk_bool pressed;
uint32_t click_count; uint32_t click_count;
} pointer; } uhdk_pointer_event;
uhdk_pointer_mode pointer_mode;
struct { typedef struct uhdk_scroll_event {
uhdk_point position; uhdk_point position;
uhdk_point delta; /* Positive scroll = toward right/bottom. */ uhdk_point delta; /* Positive scroll = toward right/bottom. */
uhdk_scroll_unit unit; uhdk_scroll_unit unit;
uhdk_scroll_phase phase; uhdk_scroll_phase phase;
uhdk_modifier_flags modifiers; uhdk_modifier_flags modifiers;
uhdk_bool momentum; uhdk_bool momentum;
} scroll; } uhdk_scroll_event;
struct {
typedef struct uhdk_text_event {
uhdk_text text; uhdk_text text;
uint64_t revision; uint64_t revision;
uhdk_bool has_replacement; uhdk_bool has_replacement;
@ -813,26 +928,78 @@ struct uhdk_event {
uint64_t replacement_len; uint64_t replacement_len;
uint64_t selection_start; uint64_t selection_start;
uint64_t selection_len; /* Within preedit text. */ uint64_t selection_len; /* Within preedit text. */
} text; } uhdk_text_event;
struct {
typedef struct uhdk_edit_event {
uhdk_edit_command command; uhdk_edit_command command;
uhdk_bool extend_selection; uhdk_bool extend_selection;
} edit; } uhdk_edit_event;
struct {
typedef struct uhdk_drop_files_event {
uhdk_file_list files; uhdk_file_list files;
uhdk_point position; uhdk_point position;
} drop_files; } uhdk_drop_files_event;
struct {
typedef struct uhdk_menu_event {
uint64_t command_id; uint64_t command_id;
} menu; } uhdk_menu_event;
struct {
typedef struct uhdk_watch_event {
uhdk_watch *watch; uhdk_watch *watch;
uhdk_change_flags flags; uhdk_change_flags flags;
uhdk_status status; /* Non-OK: watch failure; rescan/recreate as needed. */ uhdk_status status; /* Non-OK: watch failure; rescan/recreate as needed. */
uhdk_file *file; /* Nullable when only the watched root is known. */ uhdk_file *file; /* Nullable when only the watched root is known. */
} watch; } uhdk_watch_event;
uhdk_request_result request;
} data; 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. */
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;
const void *data;
uint64_t data_size;
}; };
/* Lifecycle policy: close/quit requests are advisory. Destroy the window or call /* 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. * 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 #ifdef __cplusplus
} /* extern "C" */ } /* extern "C" */
#endif #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 */ #endif /* UHDK_H_INCLUDED */

View File

@ -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 */

View File

@ -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 */

View File

@ -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

View File

@ -6,6 +6,7 @@ const abi = @import("abi.zig");
const Event = @import("Event.zig"); const Event = @import("Event.zig");
const Request = @import("Request.zig"); const Request = @import("Request.zig");
const Resource = @import("Resource.zig"); const Resource = @import("Resource.zig");
const arenas = @import("arenas.zig");
const Application = @This(); const Application = @This();
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
@ -24,7 +25,7 @@ io_tail: ?*Request = null,
pending_head: ?*Event = null, pending_head: ?*Event = null,
pending_tail: ?*Event = null, pending_tail: ?*Event = null,
published: ?*Event = null, published: ?*Event = null,
events: std.ArrayList(c.uhdk_event) = .empty, scratch: arenas.BumpArena,
name: []u8, name: []u8,
epoch: u64, epoch: u64,
deadline: u64 = 0, 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); const self = try gpa.create(Application);
errdefer gpa.destroy(self); errdefer gpa.destroy(self);
const owned_name = try gpa.dupe(u8, name); 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; return self;
} }
pub fn lock(self: *Application) void { pub fn lock(self: *Application) void {

View File

@ -8,5 +8,5 @@ const Event = @This();
next: ?*Event = null, next: ?*Event = null,
arena: abi.Arena, arena: abi.Arena,
gpa: std.mem.Allocator, 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, pins: std.ArrayList(*Resource) = .empty,

60
src/EventPayload.zig Normal file
View File

@ -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 };
}

View File

@ -100,7 +100,7 @@ pub export fn uhdk_app_poll_events(
defer poll_zone.end(); defer poll_zone.end();
runtime.destroyEvents(application.published); runtime.destroyEvents(application.published);
application.published = null; application.published = null;
application.events.clearRetainingCapacity(); application.scratch.reset();
out.* = .{ .ptr = null, .len = 0 }; out.* = .{ .ptr = null, .len = 0 };
application.lock(); application.lock();
defer application.unlock(); defer application.unlock();
@ -151,19 +151,30 @@ pub export fn uhdk_app_poll_events(
while (cursor) |node| : (cursor = node.next) { while (cursor) |node| : (cursor = node.next) {
count += 1; 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.published = application.pending_head;
application.pending_head = null; application.pending_head = null;
application.pending_tail = null; application.pending_tail = null;
cursor = application.published; cursor = application.published;
while (cursor) |node| : (cursor = node.next) { while (cursor) |node| : (cursor = node.next) {
const event = node.event; // Payload bytes now live in scratch. Keep only resource pins and nodes
if (event.window != null and !runtime.fromObjectHandle(event.window).alive) continue; // until the next poll; their private producer spans are no longer read.
if (event.type == c.UHDK_EVENT_WATCH and !runtime.fromObjectHandle(event.data.watch.watch).alive) continue; _ = node.arena.reset(.free_all);
application.events.appendAssumeCapacity(event); if (node.event.type == c.UHDK_EVENT_STOPPED) application.stopped_seen = true;
if (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; application.stats.event_count +|= out.*.len;
return abi.OK; 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); std.debug.assert(c.z_native_is_main_thread() != 0 and !application.running);
runtime.destroyEvents(application.published); runtime.destroyEvents(application.published);
runtime.destroyEvents(application.pending_head); runtime.destroyEvents(application.pending_head);
application.events.deinit(application.gpa); application.scratch.deinit();
var cursor = application.objects; var cursor = application.objects;
while (cursor) |object| { while (cursor) |object| {
cursor = object.next; cursor = object.next;

520
src/arenas.zig Normal file
View File

@ -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());
}

View File

@ -4,7 +4,7 @@ const c = @import("native");
const abi = @import("abi.zig"); const abi = @import("abi.zig");
const runtime = @import("runtime.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); runtime.emit(handle, event);
} }
@ -12,7 +12,7 @@ export fn z_complete(
handle: ?*c.uhdk_app, handle: ?*c.uhdk_app,
id: u64, id: u64,
status: abi.Status, status: abi.Status,
result: [*c]const c.uhdk_request_result, result: [*c]const c.z_request_result,
) void { ) void {
runtime.complete(handle, id, status, result); 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.app.lock();
object.payload.window.metrics = value.*; object.payload.window.metrics = value.*;
object.app.unlock(); 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.type = c.UHDK_EVENT_WINDOW_METRICS;
event.window = handle; event.window = handle;
event.data.metrics = value.*; 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.lock();
application.preferences = value.*; application.preferences = value.*;
application.unlock(); 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.type = c.UHDK_EVENT_PREFERENCES_CHANGED;
event.data.preferences = value.*; event.data.preferences = value.*;
c.z_emit(handle, &event); c.z_emit(handle, &event);

View File

@ -7,7 +7,7 @@ const abi = @import("abi.zig");
fn destroyApplication(application: *Application) void { fn destroyApplication(application: *Application) void {
runtime.destroyEvents(application.pending_head); runtime.destroyEvents(application.pending_head);
runtime.destroyEvents(application.published); runtime.destroyEvents(application.published);
application.events.deinit(application.gpa); application.scratch.deinit();
const gpa = application.gpa; const gpa = application.gpa;
gpa.free(application.name); gpa.free(application.name);
application.* = undefined; 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"); const application = try Application.create(std.testing.allocator, std.testing.io, "events");
defer destroyApplication(application); defer destroyApplication(application);
var source = [_]u8{ 'h', 'i' }; 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.type = c.UHDK_EVENT_TEXT_COMMIT;
event.data.text.text = .{ .ptr = &source, .len = source.len }; event.data.text.text = .{ .ptr = &source, .len = source.len };
runtime.emit(runtime.toAppHandle(application), &event); 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.expectEqual(@as(u64, 1), stats.tick_count);
try std.testing.expect(stats.tick_time_ns < wall_time / 2); 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);
}

View File

@ -75,7 +75,7 @@ static uhdk_modifier_flags mods(NSEventModifierFlags f) {
@property(nonatomic) uint64_t currentInput; @property(nonatomic) uint64_t currentInput;
@property(nonatomic) uint64_t lastEdit; @property(nonatomic) uint64_t lastEdit;
@property(nonatomic) uhdk_rect caret; @property(nonatomic) uhdk_rect caret;
- (void)emit:(uhdk_event *)event; - (void)emit:(z_event *)event;
- (void)cancelMarked; - (void)cancelMarked;
- (void)applyState:(const uhdk_text_state *)state; - (void)applyState:(const uhdk_text_state *)state;
@end @end
@ -197,14 +197,14 @@ static uhdk_key logical(NSEvent *e) {
(void)event; (void)event;
return YES; return YES;
} }
- (void)emit:(uhdk_event *)e { - (void)emit:(z_event *)e {
e->window = self.owner.handle; e->window = self.owner.handle;
e->input_id = self.currentInput; e->input_id = self.currentInput;
z_emit(host.app, e); z_emit(host.app, e);
} }
- (void)drawRect:(NSRect)rect { - (void)drawRect:(NSRect)rect {
(void)rect; (void)rect;
uhdk_event e = {.type = UHDK_EVENT_WINDOW_REDRAW}; z_event e = {.type = UHDK_EVENT_WINDOW_REDRAW};
[self emit:&e]; [self emit:&e];
} }
- (void)setFrameSize:(NSSize)size { - (void)setFrameSize:(NSSize)size {
@ -236,7 +236,7 @@ static uhdk_key logical(NSEvent *e) {
} }
- (void)key:(NSEvent *)event pressed:(BOOL)pressed { - (void)key:(NSEvent *)event pressed:(BOOL)pressed {
self.currentInput = ++self.nextInput; 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.physical_key = physical(event.keyCode);
e.data.key.logical_key = logical(event); e.data.key.logical_key = logical(event);
e.data.key.modifiers = mods(event.modifierFlags); e.data.key.modifiers = mods(event.modifierFlags);
@ -277,7 +277,7 @@ static uhdk_key logical(NSEvent *e) {
break; break;
} }
self.currentInput = ++self.nextInput; 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.physical_key = physical(event.keyCode);
e.data.key.modifiers = newMods; e.data.key.modifiers = newMods;
e.data.key.pressed = 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 { - (void)pointer:(NSEvent *)event type:(uint32_t)type pressed:(BOOL)pressed {
NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; 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.position = (uhdk_point){p.x, p.y};
e.data.pointer.delta = (uhdk_point){event.deltaX, event.deltaY}; e.data.pointer.delta = (uhdk_point){event.deltaX, event.deltaY};
e.data.pointer.modifiers = mods(event.modifierFlags); e.data.pointer.modifiers = mods(event.modifierFlags);
@ -341,7 +341,7 @@ static uhdk_key logical(NSEvent *e) {
} }
- (void)scrollWheel:(NSEvent *)event { - (void)scrollWheel:(NSEvent *)event {
NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; 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.position = (uhdk_point){p.x, p.y};
e.data.scroll.delta = (uhdk_point){-event.scrollingDeltaX, -event.scrollingDeltaY}; e.data.scroll.delta = (uhdk_point){-event.scrollingDeltaX, -event.scrollingDeltaY};
e.data.scroll.unit = e.data.scroll.unit =
@ -383,7 +383,7 @@ static uhdk_key logical(NSEvent *e) {
selection:(NSRange)sel selection:(NSRange)sel
explicit:(BOOL)explicitRange { explicit:(BOOL)explicitRange {
[self editID]; [self editID];
uhdk_event e = {.type = type}; z_event e = {.type = type};
e.data.text.text = utf8(s); e.data.text.text = utf8(s);
e.data.text.revision = self.revision; e.data.text.revision = self.revision;
e.data.text.has_replacement = explicitRange; e.data.text.has_replacement = explicitRange;
@ -418,7 +418,7 @@ static uhdk_key logical(NSEvent *e) {
[self.textBuffer replaceCharactersInRange:replace withString:s]; [self.textBuffer replaceCharactersInRange:replace withString:s];
self.selection = NSMakeRange(replace.location + s.length, 0); self.selection = NSMakeRange(replace.location + s.length, 0);
if (hadMarked) { if (hadMarked) {
uhdk_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END}; z_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END};
[self emit:&e]; [self emit:&e];
} }
if (ownInput) if (ownInput)
@ -463,7 +463,7 @@ static uhdk_key logical(NSEvent *e) {
self.selection = self.originalRange; self.selection = self.originalRange;
self.marked = NSMakeRange(NSNotFound, 0); self.marked = NSMakeRange(NSNotFound, 0);
[self editID]; [self editID];
uhdk_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END}; z_event e = {.type = UHDK_EVENT_TEXT_COMPOSITION_END};
[self emit:&e]; [self emit:&e];
} }
[self.inputContext discardMarkedText]; [self.inputContext discardMarkedText];
@ -542,7 +542,7 @@ static uhdk_key logical(NSEvent *e) {
return; return;
} }
[self editID]; [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.command = command.unsignedIntValue;
e.data.edit.extend_selection = extend; e.data.edit.extend_selection = extend;
[self emit:&e]; [self emit:&e];
@ -608,7 +608,7 @@ static uhdk_key logical(NSEvent *e) {
files[n++] = f; files[n++] = f;
} }
NSPoint point = [self convertPoint:sender.draggingLocation fromView:nil]; 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.files = (uhdk_file_list){files, n};
e.data.drop_files.position = (uhdk_point){point.x, point.y}; e.data.drop_files.position = (uhdk_point){point.x, point.y};
[self emit:&e]; [self emit:&e];
@ -640,12 +640,12 @@ static uhdk_key logical(NSEvent *e) {
: self.window.isZoomed ? UHDK_WINDOW_MAXIMIZED : self.window.isZoomed ? UHDK_WINDOW_MAXIMIZED
: UHDK_WINDOW_NORMAL; : UHDK_WINDOW_NORMAL;
z_metrics(self.handle, &m); 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); z_emit(host.app, &e);
} }
- (BOOL)windowShouldClose:(NSWindow *)sender { - (BOOL)windowShouldClose:(NSWindow *)sender {
(void)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); z_emit(host.app, &e);
return NO; return NO;
} }
@ -668,7 +668,7 @@ static uhdk_key logical(NSEvent *e) {
[self.view cancelMarked]; [self.view cancelMarked];
if (self.view.capture) { if (self.view.capture) {
self.view.capture = NO; 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; e.data.pointer_mode = UHDK_POINTER_NORMAL;
z_emit(host.app, &e); z_emit(host.app, &e);
} }
@ -709,7 +709,7 @@ static uhdk_key logical(NSEvent *e) {
w = candidate; w = candidate;
break; 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; e.data.menu.command_id = self.command;
if (self.role == UHDK_MENU_ROLE_QUIT) { if (self.role == UHDK_MENU_ROLE_QUIT) {
e.type = UHDK_EVENT_QUIT_REQUESTED; e.type = UHDK_EVENT_QUIT_REQUESTED;
@ -808,7 +808,7 @@ static NSMenu *makeMenu(uhdk_menu *menu, ZWindow *window) {
@implementation ZHost @implementation ZHost
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
(void)sender; (void)sender;
uhdk_event e = {.type = UHDK_EVENT_QUIT_REQUESTED}; z_event e = {.type = UHDK_EVENT_QUIT_REQUESTED};
z_emit(self.app, &e); z_emit(self.app, &e);
return NSTerminateCancel; return NSTerminateCancel;
} }
@ -818,13 +818,13 @@ static NSMenu *makeMenu(uhdk_menu *menu, ZWindow *window) {
} }
- (void)applicationDidBecomeActive:(NSNotification *)note { - (void)applicationDidBecomeActive:(NSNotification *)note {
(void)note; (void)note;
uhdk_event e = {.type = UHDK_EVENT_APP_ACTIVATION}; z_event e = {.type = UHDK_EVENT_APP_ACTIVATION};
e.data.active = YES; e.data.active = YES;
z_emit(self.app, &e); z_emit(self.app, &e);
} }
- (void)applicationDidResignActive:(NSNotification *)note { - (void)applicationDidResignActive:(NSNotification *)note {
(void)note; (void)note;
uhdk_event e = {.type = UHDK_EVENT_APP_ACTIVATION}; z_event e = {.type = UHDK_EVENT_APP_ACTIVATION};
e.data.active = NO; e.data.active = NO;
z_emit(self.app, &e); z_emit(self.app, &e);
} }
@ -838,7 +838,7 @@ static NSMenu *makeMenu(uhdk_menu *menu, ZWindow *window) {
if (f) if (f)
files[n++] = 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}; e.data.files = (uhdk_file_list){files, n};
z_emit(self.app, &e); z_emit(self.app, &e);
for (NSUInteger i = 0; i < n; i++) 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]); [[path stringByDeletingLastPathComponent] isEqualToString:watch.path]);
if (!match && !unknown && !invalid && ![path isEqualToString:watch.observed]) if (!match && !unknown && !invalid && ![path isEqualToString:watch.observed])
continue; continue;
uhdk_event e = {.type = UHDK_EVENT_WATCH}; z_event e = {.type = UHDK_EVENT_WATCH};
e.data.watch.watch = watch.handle; e.data.watch.watch = watch.handle;
e.data.watch.status = UHDK_STATUS_OK; e.data.watch.status = UHDK_STATUS_OK;
e.data.watch.flags = UHDK_CHANGE_RESCAN_REQUIRED; 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) if (f)
files[n++] = f; files[n++] = f;
} }
uhdk_request_result result = {0}; z_request_result result = {0};
result.value.files = (uhdk_file_list){files, n}; result.value.files = (uhdk_file_list){files, n};
z_complete(app, request, n == urls.count ? UHDK_STATUS_OK : UHDK_STATUS_OUT_OF_MEMORY, z_complete(app, request, n == urls.count ? UHDK_STATUS_OK : UHDK_STATUS_OUT_OF_MEMORY,
&result); &result);
@ -1260,7 +1260,7 @@ int z_native_is_main_thread(void) {
return NSThread.isMainThread; return NSThread.isMainThread;
} }
uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t request, uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t request,
uhdk_request_result *result) { z_request_result *result) {
@autoreleasepool { @autoreleasepool {
@try { @try {
ZWindow *w = cmd->target && z_object_kind(cmd->target) == Z_KIND_WINDOW 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: case Z_POINTER:
w.view.capture = cmd->number != 0; 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; e.data.pointer_mode = (uint32_t)cmd->number;
z_emit(app, &e); z_emit(app, &e);
} }
@ -1561,7 +1561,7 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
break; break;
} }
case Z_REDRAW: { 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); z_emit(app, &e);
break; break;
} }

View File

@ -1,8 +1,49 @@
#ifndef UHDK_PRIVATE_NATIVE_H #ifndef UHDK_PRIVATE_NATIVE_H
#define UHDK_PRIVATE_NATIVE_H #define UHDK_PRIVATE_NATIVE_H
#define UHDK_ENABLE_NATIVE
#include "uhdk.h" #include "uhdk.h"
#include "uhdk_native.h"
#include <stddef.h> #include <stddef.h>
/* 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 { enum {
Z_EVENT_SIZE = sizeof(uhdk_event), Z_EVENT_SIZE = sizeof(uhdk_event),
Z_EVENT_ALIGNMENT = _Alignof(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_again(void);
void z_native_stop(void); void z_native_stop(void);
void z_native_finish(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_cancel(uint64_t);
void z_native_destroy(uint32_t kind, void *native); void z_native_destroy(uint32_t kind, void *native);
void *z_native_pool_push(void); 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); 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. */ /* Zig callbacks, always return before native temporary spans expire. */
void z_tick(uhdk_app *); void z_tick(uhdk_app *);
void z_emit(uhdk_app *, const uhdk_event *); void z_emit(uhdk_app *, const z_event *);
void z_complete(uhdk_app *, uint64_t, uhdk_status, const uhdk_request_result *); void z_complete(uhdk_app *, uint64_t, uhdk_status, const z_request_result *);
void *z_object_native(void *); void *z_object_native(void *);
void z_object_set_native(void *, void *); void z_object_set_native(void *, void *);
uint32_t z_object_kind(void *); uint32_t z_object_kind(void *);

View File

@ -17,6 +17,7 @@ test {
std.testing.refAllDecls(@import("abi.zig")); std.testing.refAllDecls(@import("abi.zig"));
std.testing.refAllDecls(@import("FileOperation.zig")); std.testing.refAllDecls(@import("FileOperation.zig"));
_ = @import("core_tests.zig"); _ = @import("core_tests.zig");
_ = @import("arenas.zig");
} }
fn checkExports(comptime module: type) void { fn checkExports(comptime module: type) void {

View File

@ -111,7 +111,7 @@ pub fn destroyEvents(head: ?*Event) void {
} }
pub fn publishLocked(application: *Application, node: *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.event.time_ns = now(application.io) - application.epoch;
node.next = null; node.next = null;
if (application.pending_tail) |tail| tail.next = node else application.pending_head = node; 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 { 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; event.type = typ;
c.z_emit(toAppHandle(application), &event); c.z_emit(toAppHandle(application), &event);
} }
@ -256,7 +256,7 @@ pub fn finish(
application: *Application, application: *Application,
command: *Request, command: *Request,
status: abi.Status, status: abi.Status,
value: ?*const c.uhdk_request_result, value: ?*const c.z_request_result,
) void { ) void {
if (command.id == 0) { if (command.id == 0) {
abandon(command); abandon(command);
@ -478,7 +478,7 @@ pub fn fileStep(application: *Application, command: *Request) bool {
finish(application, command, fileError(err), null); finish(application, command, fileError(err), null);
return true; return true;
}; };
var value = std.mem.zeroes(c.uhdk_request_result); var value = std.mem.zeroes(c.z_request_result);
switch (result) { switch (result) {
.pending => return false, .pending => return false,
.written => {}, .written => {},
@ -554,7 +554,7 @@ pub fn setMenuFlags(object: *Resource, id: u64, flags: u32) bool {
return false; 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 application = fromAppHandle(handle);
const node = application.gpa.create(Event) catch { const node = application.gpa.create(Event) catch {
application.lock(); application.lock();
@ -599,7 +599,7 @@ pub fn complete(
handle: ?*c.uhdk_app, handle: ?*c.uhdk_app,
id: u64, id: u64,
status: abi.Status, status: abi.Status,
result: [*c]const c.uhdk_request_result, result: [*c]const c.z_request_result,
) void { ) void {
const application = fromAppHandle(handle); const application = fromAppHandle(handle);
application.lock(); application.lock();
@ -647,7 +647,7 @@ pub fn tick(handle: ?*c.uhdk_app) void {
application.io_tail = command; application.io_tail = command;
continue; 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) { if (command.data.op == c.Z_MENU_FLAGS) {
application.lock(); application.lock();
const found = setMenuFlags(fromObjectHandle(command.data.target), command.data.number, @intCast(command.data.limit)); const found = setMenuFlags(fromObjectHandle(command.data.target), command.data.number, @intCast(command.data.limit));

View File

@ -148,7 +148,7 @@ static Window *windowOf(void *handle) {
return handle ? z_object_native(handle) : NULL; return handle ? z_object_native(handle) : NULL;
} }
static void emit(Window *w, uint32_t type) { static void emit(Window *w, uint32_t type) {
uhdk_event e = {0}; z_event e = {0};
e.type = type; e.type = type;
e.window = w ? w->handle : NULL; e.window = w ? w->handle : NULL;
z_emit(host.app, &e); 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); char *s = utf8(text, length, &len);
if (!s) if (!s)
return; return;
uhdk_event e = {0}; z_event e = {0};
e.type = type; e.type = type;
e.window = w->handle; e.window = w->handle;
e.input_id = w->input_id = ++host.input_id; 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); profileFree(s);
} }
static void editEvent(Window *w, uint32_t command) { static void editEvent(Window *w, uint32_t command) {
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_EDIT_COMMAND; e.type = UHDK_EVENT_EDIT_COMMAND;
e.window = w->handle; e.window = w->handle;
e.input_id = w->input_id = ++host.input_id; 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); editEvent(w, UHDK_EDIT_COPY + item->role - UHDK_MENU_ROLE_COPY);
return; return;
} }
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_MENU_COMMAND; e.type = UHDK_EVENT_MENU_COMMAND;
e.window = w ? w->handle : NULL; e.window = w ? w->handle : NULL;
e.data.menu.command_id = item->command_id; e.data.menu.command_id = item->command_id;
@ -445,7 +445,7 @@ static void dropFiles(Window *w, HDROP drop) {
POINT point; POINT point;
DragQueryPoint(drop, &point); DragQueryPoint(drop, &point);
if (used) { if (used) {
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_DROP_FILES; e.type = UHDK_EVENT_DROP_FILES;
e.window = w->handle; e.window = w->handle;
e.data.drop_files.files = (uhdk_file_list){files, used}; 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); metrics(w);
break; break;
case WM_ACTIVATEAPP: { case WM_ACTIVATEAPP: {
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_APP_ACTIVATION; e.type = UHDK_EVENT_APP_ACTIVATION;
e.data.active = !!wp; e.data.active = !!wp;
z_emit(host.app, &e); z_emit(host.app, &e);
@ -561,7 +561,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
w->tracking = TRUE; w->tracking = TRUE;
emit(w, UHDK_EVENT_POINTER_ENTER); emit(w, UHDK_EVENT_POINTER_ENTER);
} }
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_POINTER_MOVE; e.type = UHDK_EVENT_POINTER_MOVE;
e.window = w->handle; e.window = w->handle;
e.data.pointer.position = e.data.pointer.position =
@ -593,7 +593,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
msg == WM_XBUTTONUP; msg == WM_XBUTTONUP;
if (!up) if (!up)
SetFocus(hwnd); SetFocus(hwnd);
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_POINTER_BUTTON; e.type = UHDK_EVENT_POINTER_BUTTON;
e.window = w->handle; e.window = w->handle;
e.data.pointer.position = e.data.pointer.position =
@ -613,7 +613,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
return 0; return 0;
} }
case WM_CAPTURECHANGED: { case WM_CAPTURECHANGED: {
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_POINTER_MODE_CHANGED; e.type = UHDK_EVENT_POINTER_MODE_CHANGED;
e.window = w->handle; e.window = w->handle;
e.data.pointer_mode = UHDK_POINTER_NORMAL; 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: { case WM_MOUSEHWHEEL: {
POINT p = {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}; POINT p = {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)};
ScreenToClient(hwnd, &p); ScreenToClient(hwnd, &p);
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_SCROLL; e.type = UHDK_EVENT_SCROLL;
e.window = w->handle; e.window = w->handle;
e.data.scroll.position = (uhdk_point){p.x / w->scale, p.y / w->scale}; 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; return 0;
} }
} }
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_KEY; e.type = UHDK_EVENT_KEY;
e.window = w->handle; e.window = w->handle;
e.input_id = w->input_id = ++host.input_id; e.input_id = w->input_id = ++host.input_id;
@ -1194,7 +1194,7 @@ static void showDialog(void) {
} else } else
status = files ? UHDK_STATUS_OS_ERROR : UHDK_STATUS_OUT_OF_MEMORY; 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}; result.value.files = (uhdk_file_list){files, count};
host.dialog = NULL; host.dialog = NULL;
host.dialog_id = 0; host.dialog_id = 0;
@ -1279,7 +1279,7 @@ static uhdk_status createWatch(uhdk_file *root, uhdk_watch *handle) {
return UHDK_STATUS_OK; return UHDK_STATUS_OK;
} }
static void watchEvent(Watch *watch, uint32_t flags, uhdk_status status) { 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.type = UHDK_EVENT_WATCH;
e.data.watch.watch = watch->handle; e.data.watch.watch = watch->handle;
e.data.watch.flags = flags; e.data.watch.flags = flags;
@ -1402,7 +1402,7 @@ void z_native_run(void) {
while (host.running) { while (host.running) {
int result = GetMessageW(&message, NULL, 0, 0); int result = GetMessageW(&message, NULL, 0, 0);
if (result <= 0) { if (result <= 0) {
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_QUIT_REQUESTED; e.type = UHDK_EVENT_QUIT_REQUESTED;
z_emit(host.app, &e); z_emit(host.app, &e);
if (result < 0) if (result < 0)
@ -1578,7 +1578,7 @@ static uhdk_status clipboardRead(uint64_t id, uint64_t limit) {
profileFree(text); profileFree(text);
return UHDK_STATUS_BUFFER_TOO_SMALL; return UHDK_STATUS_BUFFER_TOO_SMALL;
} }
uhdk_request_result result = {0}; z_request_result result = {0};
result.value.text = (uhdk_text){text, len}; result.value.text = (uhdk_text){text, len};
z_complete(host.app, id, UHDK_STATUS_OK, &result); z_complete(host.app, id, UHDK_STATUS_OK, &result);
profileFree(text); profileFree(text);
@ -1632,7 +1632,7 @@ static uhdk_status setIcon(const uhdk_image *image) {
return UHDK_STATUS_OK; return UHDK_STATUS_OK;
} }
uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t request, 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; (void)app;
Window *w = Window *w =
cmd->target && z_object_kind(cmd->target) == Z_KIND_WINDOW ? windowOf(cmd->target) : NULL; 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(); ReleaseCapture();
break; break;
} }
uhdk_event e = {0}; z_event e = {0};
e.type = UHDK_EVENT_POINTER_MODE_CHANGED; e.type = UHDK_EVENT_POINTER_MODE_CHANGED;
e.window = w->handle; e.window = w->handle;
e.data.pointer_mode = (uint32_t)cmd->number; e.data.pointer_mode = (uint32_t)cmd->number;