diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 415e6f1..829bc45 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -526,7 +526,7 @@ A decoder or an encoder holds the device it was made on, so it may outlive its c | `emberwave_texture_create`, `_destroy`, `emberwave_decoder_decode_texture` | A texture of the picture's size, RGBA or BGRA, on the decoder's device, and a decode that draws the RGB picture into it and waits | | `emberwave_texture_create_plane`, `emberwave_decoder_decode_plane_textures` | A one-channel texture for the Y, the Cb or the Cr plane, and a decode that puts the planes into three and waits: on Metal it stores them there, elsewhere it copies them in (D52) | | `emberwave_texture_metal`, `emberwave_context_metal_device` | The `id` and `id` behind a texture and a context, borrowed, or `NULL` off Metal | -| `emberwave_context_create_metal`, `emberwave_decoder_submit_texture`, `_submit_plane_textures` | A context on the client's own Metal device and queue, and decodes into textures that return without waiting, which only such a context takes | +| `emberwave_context_create_metal`, `emberwave_decoder_submit_texture_metal`, `_submit_plane_textures_metal` | A context on the client's own Metal device and queue, and decodes into textures that return without waiting, which only such a context takes | | `emberwave_texture_from_metal`, `_from_metal_plane` | The client's own `id`, such as a `CVPixelBuffer`'s, as a texture every decode above fills | | `emberwave_vulkan_instance_extensions`, `emberwave_vulkan_requirements_create`, `_extensions`, `_features`, `_destroy` | What the client enables on its Vulkan instance and device for a context on them (D61) | | `emberwave_context_create_vulkan` | A context on the client's own `VkDevice` and one of its queues, under the client's lock of the queue (D61) | @@ -542,9 +542,9 @@ A decoder or an encoder holds the device it was made on, so it may outlive its c | `emberwave_decoder_submit_texture_d3d12`, `_submit_plane_textures_d3d12`, `emberwave_encoder_encode_textures_d3d12` | Decodes and encodes on such textures, with fences to wait for and to signal, leaving them in `D3D12_RESOURCE_STATE_COMMON` (D63) | | `emberwave_decoder_set_concealment` | Frames drawn in part keep the last frame's detail where the picture is still, for the blocks they lack (D66) | | `emberwave_frame_packets` | Where a frame splits into packets of a given size, at its blocks | -| `emberwave_version`, `emberwave_last_error` | The version, and the message of the last failure on the calling thread | +| `emberwave_version`, `emberwave_version_number`, `emberwave_last_error` | The library's version as text and as the number `EMBERWAVE_VERSION` gives the header's (D69), and the message of the last failure on the calling thread | -Every function returns a status, or a value that cannot fail. A Rust panic is caught at the boundary, because unwinding into C is not allowed, and becomes `EMBERWAVE_STATUS_INTERNAL`, or `EMBERWAVE_STATUS_DEVICE_LOST` when the device is lost: wgpu panics when the device fails while it is waited for (D22), after recording the loss. Arguments are checked before the frame is used: a `NULL` pointer, a stride shorter than a row, a picture size the format cannot have or an unknown constant is `EMBERWAVE_STATUS_INVALID_ARGUMENT`, and the frame can still be decoded with correct ones. What the caller passes in is a plain integer, never a Rust enum, so no value from C can be an invalid one. +Every function returns a status, or a value that cannot fail. A Rust panic is caught at the boundary, because unwinding into C is not allowed, and becomes `EMBERWAVE_STATUS_INTERNAL`, or `EMBERWAVE_STATUS_DEVICE_LOST` when the device is lost: wgpu panics when the device fails while it is waited for (D22), after recording the loss. Arguments are checked before the frame is used: a `NULL` pointer, a stride shorter than a row, a picture size the format cannot have, a struct's `struct_size` below its first size or an unknown constant is `EMBERWAVE_STATUS_INVALID_ARGUMENT`, and the frame can still be decoded with correct ones. A plane whose `size` is less than its rows span is `EMBERWAVE_STATUS_BUFFER_TOO_SMALL` for a decode's output and `EMBERWAVE_STATUS_INVALID_ARGUMENT` for an encode's input. A call this system lacks, such as Metal's off Apple's systems, is `EMBERWAVE_STATUS_UNSUPPORTED`. What the caller passes in is a plain integer, never a Rust enum, so no value from C can be an invalid one. Structs passed by pointer start with `struct_size`, read and written through `read_struct` and `write_struct`, so that they can grow (D69). `unsafe` code lives in this crate only. It has its own lint table instead of the workspace's `forbid`: every unsafe operation sits in an `unsafe` block of its own with a comment saying why it is sound, and every public function documents what the caller must guarantee. @@ -1747,3 +1747,47 @@ The last frame's detail, always, gains everything on a still picture and loses u - Less workgroup memory alone, 5 KiB instead of 9, made the inverse transform no faster: the load phase held it back, not the memory a workgroup takes. Halving workgroup memory, which D48 counted among the sources of upstream's FP16 gain, would not help emberwave's kernel by itself. - `BENCHMARKS.md` keeps its tables until the next full run on a quiet machine. - The test of the workgroup variables checked for writes before reads (D25) lists the new ones. + +### 2026-10-31 — D69: The C API before 1.0: structs that grow, statuses, versions + +**Context.** From 1.0 on the C API is to stay compatible (#152). Our review agent audited v0.16.0 and found five problems: +- No struct passed by pointer could grow. +- Planes carried no capacity, so decodes wrote what their strides implied on trust. +- `EMBERWAVE_STATUS_NO_GPU` also meant "not on this system", and `EMBERWAVE_STATUS_INVALID_ARGUMENT` also meant "too small". +- Metal's two submits had no suffix, while Vulkan's and Direct3D's had one. +- A program could not tell the library's version from its header's but by parsing a string. + +**Decision.** +- **`struct_size` first** in every struct passed by pointer: + - The structs: `EmberwaveContextOptions`, `EmberwaveStreamInfo`, `EmberwaveFrameInfo`, `EmberwaveColor`, `EmberwaveSignalling`, `EmberwaveVulkanInstance`, `EmberwaveVulkanDevice`, `EmberwaveVulkanImage`, `EmberwaveVulkanSync` and `EmberwaveD3D12Sync`. + - The caller sets it to `sizeof` the struct before the call, for structs in and out. A size below the struct's first size, its size in 0.17, is `EMBERWAVE_STATUS_INVALID_ARGUMENT`. + - A struct passed in is read up to its size, and the members past it are zeros, their defaults. A struct filled in is written up to its size. Members are added at the end only. + - `EmberwaveVulkanDevice` points to its instance instead of holding it, so that each can grow. + - `EmberwaveSignalling`'s reserved words go, as its size does their work. + - No padding may follow a struct's last member, or a member added there would leave the size, and so `struct_size`, as it was. `EmberwaveVulkanInstance` and `EmberwaveVulkanSync` put their 32-bit members first for that. + - Each struct's first size is where the last member of its first layout ends. A macro takes that member and the last one now, and checks at compile time that `struct_size` comes first and that no padding follows. A test pins the first sizes on 64-bit systems. + - `EmberwavePacket` stays as it is: it is the element of an array, whose stride is its size. +- **Planes carry `size`,** the bytes at `data`, and the calls check it. + - A decode's plane that is too small gives `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`. An encode's gives `EMBERWAVE_STATUS_INVALID_ARGUMENT`. + - They stay passed by value, since another pixel format would be another call. +- **Two new statuses.** + - `EMBERWAVE_STATUS_UNSUPPORTED`, which was `NO_GPU`, is for a call whose graphics API the system lacks, one that makes a context on it or wraps one of its objects: Metal's off Apple's systems, Vulkan's off Linux, Android and 64-bit Windows, Direct3D's off Windows. The calls that take what such a context made give `INVALID_ARGUMENT` for a context of another kind, as before: a caller cannot have made the right kind there. + - `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, which was `INVALID_ARGUMENT`, is for an output array, a plane or an encode's budget that is too small. + - The header says to treat any status but `OK` as a failure, as later versions may add more. + - The status stays a C enum of `int`'s width, as Rust's `repr(C)` gives it. Fixing it to `int32_t` would change how Swift imports it, for the sake of compilers set to `-fshort-enums`, which break other libraries' enums as well. +- **Arrays the caller sizes** take the array, its capacity, and a count out, which is the count needed. A capacity that is short gives `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`. `emberwave_frame_packets` did this already, and `emberwave_vulkan_instance_extensions` now does. The Vulkan calls count in `uint32_t`, as Vulkan does. +- **Names.** The two submits that only Metal has are now `emberwave_decoder_submit_texture_metal` and `emberwave_decoder_submit_plane_textures_metal`, beside the `_vulkan` and `_d3d12` forms. +- **Versions.** The header has `EMBERWAVE_VERSION_MAJOR`, `_MINOR` and `_PATCH`, and `EMBERWAVE_VERSION`: 10000 times the major, plus 100 times the minor, plus the patch. The library has `emberwave_version_number()`. A compile-time assertion ties them to the crate's version, so a release changes both. +- **Threads.** Compile-time assertions check the threads the docs promise: + - in the C API, contexts `Send` and `Sync`, and decoders, encoders and textures `Send`; + - in the core, outside the browser, `GpuContext` `Send` and `Sync`, and `Decoder` and `Encoder` `Send`. +- **Callbacks** given to emberwave must not call emberwave. +- **Provisional calls.** Until a client has used them, the calls on the caller's Metal, Vulkan and Direct3D objects may change in a minor version. This is decided again at 1.0. + +**Not done.** +- A versioned SONAME and install name, and an export list. The list would leave out the four Objective-C symbols that a dependency of wgpu exports. All of this waits for 1.0, when the major version starts to mean the ABI. +- The Rust API's half of #152, which comes next. + +**Consequence.** +- Every C caller changes. It sets `struct_size`, gives planes their `size`, calls the Metal submits by their new names, and checks the new statuses. The C examples, the Swift example, the benchmark app and the tests have been changed. +- From now on, a minor version can add a member to a struct without breaking a program, as the C API must from 1.0 on. diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fa468..421cc22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to emberwave. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +- `EMBERWAVE_VERSION`, with `EMBERWAVE_VERSION_MAJOR`, `_MINOR` and `_PATCH`, in the C header, and `emberwave_version_number()` in the library, so that a program can tell the library it runs with from the header it was built with (decision D69, [#152](https://github.com/with-developer/emberwave/issues/152)). +- The C API's statuses `EMBERWAVE_STATUS_UNSUPPORTED`, for a call the system lacks, and `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, for an output array, a plane or an encode's budget that is too small. Later versions may add more: treat any status but `EMBERWAVE_STATUS_OK` as a failure. + +### Changed + +- The C API's structs passed by pointer start with `struct_size`, which the caller sets to `sizeof` the struct before each call, for the structs it passes and those it gets filled in (decision D69). From now on a struct can grow by a member without breaking programs. The structs are `EmberwaveContextOptions`, `EmberwaveStreamInfo`, `EmberwaveFrameInfo`, `EmberwaveColor` and `EmberwaveSignalling`, whose `reserved` words go, and `EmberwaveVulkanInstance`, `EmberwaveVulkanDevice`, `EmberwaveVulkanImage`, `EmberwaveVulkanSync` and `EmberwaveD3D12Sync`. `EmberwaveVulkanDevice` points to its instance instead of holding it. +- `EmberwavePlane` and `EmberwaveSourcePlane` have `size`, the bytes at `data`, which the calls check. +- Two statuses moved: + - a call the system lacks, such as Metal's off Apple's systems, gives `EMBERWAVE_STATUS_UNSUPPORTED`, not `EMBERWAVE_STATUS_NO_GPU`; + - an encode's budget, the array of `emberwave_frame_packets` or a decode's plane that is too small gives `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, not `EMBERWAVE_STATUS_INVALID_ARGUMENT`. +- `emberwave_decoder_submit_texture` and `emberwave_decoder_submit_plane_textures`, which only Metal has, are now `emberwave_decoder_submit_texture_metal` and `emberwave_decoder_submit_plane_textures_metal`. +- `emberwave_vulkan_instance_extensions` takes the capacity of `names` apart from the count it stores, as `emberwave_frame_packets` does. +- The calls on the caller's Metal, Vulkan and Direct3D objects are provisional: until a client has used them, a minor version may change them. + ## [0.16.0] - 2026-10-31 Still pictures keep their detail through lost packets, and the wavelet transforms take less GPU time. A decoder that conceals, which a client turns on, fills the blocks of detail that a frame drawn in part lacks with the last frame's, wherever the coarsest level shows that the picture has not moved: in Rust, through the C API and in the browser player. The transforms read their rows straight from the bands, with the same output to the byte: on an Apple M3 Max a 4K frame's GPU work takes 0.47 ms instead of 0.54 ms. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eaa30d1..35c91a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,6 +72,8 @@ After a deliberate change to what the encoder writes, `scripts/gen_encoder_vecto - **Public items have doc comments.** - **No "fix later" comments.** Open an issue instead. - **New dependencies** need a reason in the pull request. +- **The C API's structs.** A struct the C API takes or fills in through a pointer starts with `struct_size` (decision D69). A member is added at the end, only, and its zero is its default; the struct's `FIRST_SIZE` stays the size it had before. The calls read and write a struct through `read_struct` and `write_struct`, never by dereferencing the pointer. +- **A release** changes the version in `Cargo.toml` and `EMBERWAVE_VERSION_MAJOR`, `_MINOR`, `_PATCH` and `EMBERWAVE_VERSION` in `crates/emberwave-ffi/src/lib.rs` together, as the build fails otherwise, and regenerates the header. ## Commits and pull requests diff --git a/crates/emberwave-core/src/gpu/mod.rs b/crates/emberwave-core/src/gpu/mod.rs index 3f8c042..60a251c 100644 --- a/crates/emberwave-core/src/gpu/mod.rs +++ b/crates/emberwave-core/src/gpu/mod.rs @@ -27,6 +27,18 @@ pub use decoder::{ pub use encoder::{EncodeStageTimings, EncodeTimings, Encoder, TexturePicture}; pub use error::GpuError; +// What the docs and the C API promise: a context is shared by threads, and +// decoders and encoders move between them. In a browser wgpu's objects stay +// on their thread. +#[cfg(not(target_arch = "wasm32"))] +const _: () = { + const fn shared() {} + const fn movable() {} + shared::(); + movable::(); + movable::(); +}; + /// The WGSL sources of the pipeline stages, for offline validation. pub mod shader_sources { /// Coefficient decoding and dequantisation. diff --git a/crates/emberwave-ffi/README.md b/crates/emberwave-ffi/README.md index 9136030..015a0e8 100644 --- a/crates/emberwave-ffi/README.md +++ b/crates/emberwave-ffi/README.md @@ -133,7 +133,8 @@ EmberwaveEncoder *encoder; /* one per stream, on a context as above */ emberwave_encoder_create(context, 1920, 1080, EMBERWAVE_CHROMA_420, NULL, &encoder); /* For every picture: planes with their strides, a buffer of `budget` bytes. */ -EmberwaveSourcePlane y = { y_data, y_stride }, cb = { cb_data, cb_stride }, cr = { cr_data, cr_stride }; +EmberwaveSourcePlane y = { y_data, y_stride, y_size }, cb = { cb_data, cb_stride, cb_size }, + cr = { cr_data, cr_stride, cr_size }; size_t size, count; emberwave_encoder_encode(encoder, y, cb, cr, frame, budget, &size); /* or a picture in BGRA: emberwave_encoder_encode_rgb(encoder, bgra, EMBERWAVE_TEXTURE_BGRA8, …) */ @@ -156,6 +157,33 @@ target/c-example/emberwave_encode testdata/encoder/scene_200x120_420.source.y4m `scripts/check_c_example.sh` decodes every test vector that way and compares the output with upstream's decoder, and encodes two of the encoder's sources; CI runs it on Linux, macOS and Windows. +## Versions, sizes and statuses + +The C API is to stay compatible from 1.0 on, and these rules let it grow without breaking programs (decision D69): + +- **Structs carry their size.** A struct passed by pointer starts with `struct_size`, which the caller sets to `sizeof` the struct before the call, for the structs it passes in and those it gets filled in: + + ```c + EmberwaveStreamInfo info = { .struct_size = sizeof info }; + emberwave_probe_pwv1(data, size, &info); + ``` + + emberwave reads and writes no more than that many bytes and takes the members a shorter struct lacks as zeros, their defaults, so a program built with an older header keeps working with a newer library. Members are only ever added at the end. A `struct_size` below the struct's first size is `EMBERWAVE_STATUS_INVALID_ARGUMENT`. The planes, passed by value, carry the bytes they span in `size` instead, which the calls check. +- **Versions.** `EMBERWAVE_VERSION`, with `EMBERWAVE_VERSION_MAJOR`, `_MINOR` and `_PATCH`, is the header's version as one number, 10000 times the major plus 100 times the minor plus the patch. `emberwave_version_number()` is the library's: a program that finds them different runs with another library than the one it was built for. +- **Statuses.** Later versions may add statuses: treat any but `EMBERWAVE_STATUS_OK` as a failure. `EMBERWAVE_STATUS_UNSUPPORTED` means the system lacks the graphics API of a call that makes a context on it or wraps one of its objects, such as Metal's off Apple's systems; the calls that take what such a context made give `EMBERWAVE_STATUS_INVALID_ARGUMENT` for a context of another kind. `EMBERWAVE_STATUS_BUFFER_TOO_SMALL` means that an output array or buffer, or an encode's budget, is too small; where the call can tell what it needs, it stores that where its documentation says. +- **Callbacks** given to emberwave must not call emberwave. +- **Provisional calls.** The calls on Metal, Vulkan and Direct3D objects of the caller's, below, are provisional: until a client has used them, a minor version may change them. + +| Platform | Built in CI | Tested in CI | Prebuilt in releases | +|---|---|---|---| +| macOS on Apple silicon, Metal | yes | yes, the C examples, the Metal interop and the benchmark app's checks | yes, and in the XCFramework | +| Linux on x86-64, Vulkan | yes | yes, on lavapipe, the Vulkan interop under the validation layer | yes | +| Windows on x86-64, Direct3D 12 | yes | yes, on WARP, the Direct3D 11 interop | yes | +| iOS and iPadOS, Metal | yes, the device and the Simulator | the app builds; it runs on a phone by hand | in the XCFramework | +| Android, Linux on Arm, 32-bit Windows | no | no | no | + +The header is C99 and C++11. + ## Pictures on the GPU: Metal A client that draws with Metal can have the picture put into a texture on the GPU instead of copied to CPU memory: on an Apple M3 Max, a 4K frame takes 1.3 to 1.4 ms that way, where `emberwave_decoder_decode_rgb` takes 2.6 to 2.7 ms, and a 1080p frame 0.56 ms against 0.81 ms. The texture is emberwave's, the Metal objects behind it the client's to use: @@ -211,13 +239,13 @@ emberwave_context_create_metal((__bridge void *)device, (__bridge void *)queue, /* ... a decoder on it, and one texture for every frame ... */ /* For every frame that is ready: */ -emberwave_decoder_submit_texture(decoder, NULL, texture); /* returns once the work is on the queue */ +emberwave_decoder_submit_texture_metal(decoder, NULL, texture); /* returns once the work is on the queue */ id commands = [queue commandBuffer]; /* the same queue, committed after */ /* ... encode the draw that samples the texture ... */ [commands commit]; ``` -`emberwave_decoder_submit_plane_textures` does the same for the planes. On an Apple M3 Max a 4K frame takes the decoding thread 0.26 ms instead of 1.3 ms, and the client's draw completes as soon as after the waiting decode, at 1080p 0.1 ms sooner. The rules of a shared queue: +`emberwave_decoder_submit_plane_textures_metal` does the same for the planes. On an Apple M3 Max a 4K frame takes the decoding thread 0.26 ms instead of 1.3 ms, and the client's draw completes as soon as after the waiting decode, at 1080p 0.1 ms sooner. The rules of a shared queue: - Use the textures directly in the command buffers that read them, not through a heap or through an argument buffer without `useResource`, as Metal does not track those. - Commit those command buffers after the call has returned. - A call makes all its command buffers before it commits them: on an Apple M3 Max, 9 for a decode to a texture or to RGB, 6 to planes or to plane textures and 18 for an encode, one for each pass and transition of wgpu's. The decoders and encoders of every context made on the queue take turns at this, and hold none between calls (decision D60). A full queue makes a call wait until one of its command buffers has completed and that buffer's completed handlers have returned; a queue without room for 20 besides those the client holds uncommitted can wait for good. The default of 64 is plenty. @@ -247,10 +275,10 @@ CFRelease(image); /* ... the pixel buffer holds the picture: enqueue it on the display layer */ ``` -For a pixel buffer that goes to a display layer, the decode that waits is the simple way. The display layer is not on the queue, so the submit gains nothing there unless the client queues GPU work of its own behind the decode. With `emberwave_decoder_submit_texture` the call returns before the GPU has written the pixel buffer. The queue orders only command buffers that use the texture: one that does not use it may run beside the decode and finish first. Commit one that uses it, such as the client's own pass over the texture or a copy of one pixel from it. In its completed handler, release the `CVMetalTextureRef` and enqueue the pixel buffer: +For a pixel buffer that goes to a display layer, the decode that waits is the simple way. The display layer is not on the queue, so the submit gains nothing there unless the client queues GPU work of its own behind the decode. With `emberwave_decoder_submit_texture_metal` the call returns before the GPU has written the pixel buffer. The queue orders only command buffers that use the texture: one that does not use it may run beside the decode and finish first. Commit one that uses it, such as the client's own pass over the texture or a copy of one pixel from it. In its completed handler, release the `CVMetalTextureRef` and enqueue the pixel buffer: ```objc -emberwave_decoder_submit_texture(decoder, NULL, texture); +emberwave_decoder_submit_texture_metal(decoder, NULL, texture); id metal = CVMetalTextureGetTexture(image); id commands = [queue commandBuffer]; /* the same queue, after the call */ id blit = [commands blitCommandEncoder]; @@ -282,11 +310,12 @@ On Linux, Android and 64-bit Windows, where wgpu runs on Vulkan, a context can r ```c uint32_t wanted = 0; -emberwave_vulkan_instance_extensions((EmberwaveVkGetInstanceProcAddr)vkGetInstanceProcAddr, NULL, &wanted); +emberwave_vulkan_instance_extensions((EmberwaveVkGetInstanceProcAddr)vkGetInstanceProcAddr, NULL, 0, &wanted); /* ... room for `wanted` names, then the same call fills them in; vkCreateInstance with them, and an apiVersion at least the physical device's, up to VK_API_VERSION_1_3 ... */ EmberwaveVulkanInstance instance = { + .struct_size = sizeof instance, .get_instance_proc_addr = (EmberwaveVkGetInstanceProcAddr)vkGetInstanceProcAddr, .instance = vk_instance, .api_version = VK_API_VERSION_1_3, /* the instance's apiVersion */ @@ -302,7 +331,8 @@ const VkPhysicalDeviceFeatures2 *features = emberwave_vulkan_requirements_featur emberwave_vulkan_requirements_destroy(needs); EmberwaveVulkanDevice device = { - .instance = instance, + .struct_size = sizeof device, + .instance = &instance, .physical_device = physical_device, .device = vk_device, .extensions = device_extensions, /* all the device's extensions */ @@ -338,6 +368,7 @@ The decodes that wait, `emberwave_decoder_decode_texture` and `emberwave_decoder ```c EmberwaveVulkanSync sync = { + .struct_size = sizeof sync, .wait_semaphore = (uint64_t)reads_done, /* signalled when the client's reads of the image end */ .wait_value = frame, .signal_semaphore = (uint64_t)picture_ready, /* the client's reads wait for it */ @@ -382,6 +413,7 @@ emberwave_texture_from_d3d12(decoder, texture_handle, &texture); /* For every frame that is ready: the decode waits for the client's reads of the last picture, and signals when the texture holds the new one. */ EmberwaveD3D12Sync sync = { + .struct_size = sizeof sync, .wait_fence = shared, .wait_value = reads_done, .signal_fence = shared, .signal_value = reads_done + 1, }; @@ -456,6 +488,7 @@ On Vulkan, `emberwave_texture_from_vulkan_source` wraps an image of the host's, ```c EmberwaveVulkanImage image = { + .struct_size = sizeof image, .image = (uint64_t)luma_image, .format = VK_FORMAT_R8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, @@ -466,7 +499,7 @@ emberwave_texture_from_vulkan_source(encoder, EMBERWAVE_PLANE_Y, &image, &y); /* ... and the VK_FORMAT_R8G8_UNORM image of Cb and Cr for EMBERWAVE_PLANE_CBCR */ /* For every picture: the host's copy of the capture into the images signals `copied`. */ -EmberwaveVulkanSync sync = { .wait_semaphore = (uint64_t)copied }; +EmberwaveVulkanSync sync = { .struct_size = sizeof sync, .wait_semaphore = (uint64_t)copied }; emberwave_encoder_encode_textures_vulkan(encoder, y, cbcr, NULL, &sync, frame, budget, &size); ``` @@ -509,8 +542,8 @@ CFRelease(bgra); - **Panics.** A panic inside emberwave is caught and reported as `EMBERWAVE_STATUS_INTERNAL`; destroy the object it happened in. - **Packets.** `emberwave_decoder_push_packet` takes one packet or several back to back, in any order, and ignores a block that arrived already. Frames are told apart by the bitstream's sequence number, which has three bits: a packet of a frame one to three frames newer discards what was received of the older one and starts its own; one four to seven frames newer, or one to four frames older, is ignored; one five to seven frames older passes for a newer frame, and one eight frames off for the same frame. So a transport keeps frame numbers of its own: it drops packets of frames older than the one being received, and before it gives the first packet of a newer frame it decodes the current one if it is ready, then calls `emberwave_decoder_clear`, not after. - **Partial frames.** `emberwave_decoder_is_ready` with `allow_partial_frame` accepts a frame that lost packets once every block of the coarsest level and more than 90 percent of the blocks its start of frame header announced have arrived, for a picture despite the loss; `emberwave_decoder_set_partial_share` sets another share, lower to draw frames that lost more, with more of their detail blank, or 1 for whole frames only (decision D59). `emberwave_decoder_set_concealment(decoder, 1)` fills the blocks of detail such a frame lacks with the last frame's where the picture is still, so that a still picture keeps its detail, and `0` turns it off again, as it is by default (decision D66). Until that header arrives the count is of all the picture's blocks. emberwave's encoder sends every block of the coarsest level for this; upstream's leaves out the empty ones, so a frame of it that lost any packet may never be ready in part (decision D58). -- **Encoding.** `emberwave_encoder_encode` reads planes with a stride of at least their width (`emberwave_encoder_info` gives the sizes), `emberwave_encoder_encode_rgb` a picture in RGB with a stride of at least four bytes a pixel, and `emberwave_encoder_encode_textures` reads either from textures on the GPU (see [Pictures to encode on the GPU](#pictures-to-encode-on-the-gpu)), as `_vulkan` and `_d3d12` do with fences or semaphores. Both write the frame, at most `budget` bytes, into a buffer of that many bytes the caller provides, and wait for the GPU. A budget below the smallest frame is `EMBERWAVE_STATUS_INVALID_ARGUMENT` with the smallest frame's size in `size`, and encodes nothing. The frame's sequence number, a three-bit counter from 1 that wraps around, advances only with frames that were encoded. `emberwave_frame_packets` splits a frame at its blocks; a block larger than the packet size gets a packet of its own. -- **Signalling.** `EmberwaveSignalling` sets what every frame says about its colours: matrix (BT.709 or BT.2020; the format cannot signal BT.601), range, chroma siting, primaries and transfer function (sRGB or PQ). `NULL` means BT.709, video range, centred chroma and sRGB. Its `reserved` words are zero. +- **Encoding.** `emberwave_encoder_encode` reads planes with a stride of at least their width (`emberwave_encoder_info` gives the sizes), `emberwave_encoder_encode_rgb` a picture in RGB with a stride of at least four bytes a pixel, and `emberwave_encoder_encode_textures` reads either from textures on the GPU (see [Pictures to encode on the GPU](#pictures-to-encode-on-the-gpu)), as `_vulkan` and `_d3d12` do with fences or semaphores. Both write the frame, at most `budget` bytes, into a buffer of that many bytes the caller provides, and wait for the GPU. A budget below the smallest frame is `EMBERWAVE_STATUS_BUFFER_TOO_SMALL` with the smallest frame's size in `size`, and encodes nothing. The frame's sequence number, a three-bit counter from 1 that wraps around, advances only with frames that were encoded. `emberwave_frame_packets` splits a frame at its blocks; a block larger than the packet size gets a packet of its own. +- **Signalling.** `EmberwaveSignalling` sets what every frame says about its colours: matrix (BT.709 or BT.2020; the format cannot signal BT.601), range, chroma siting, primaries and transfer function (sRGB or PQ). `NULL` means BT.709, video range, centred chroma and sRGB. - **Colours.** With a `NULL` colour, `emberwave_decoder_decode_rgb` converts the way the frame's start of frame header says. Upstream's Metal encoder writes BT.709, full range and centred chroma there whatever the pictures are, so pass what you know, such as the colours of a PWV1 file's header from `emberwave_probe_pwv1`. The header documents every function; `ARCHITECTURE.md` in the repository explains the design (decisions D27 and D33). diff --git a/crates/emberwave-ffi/cbindgen.toml b/crates/emberwave-ffi/cbindgen.toml index 82eabae..a5b3c08 100644 --- a/crates/emberwave-ffi/cbindgen.toml +++ b/crates/emberwave-ffi/cbindgen.toml @@ -20,6 +20,20 @@ header = """/* * not from new ones without end. * - A panic inside emberwave is reported as EMBERWAVE_STATUS_INTERNAL. * Destroy the object it happened in. + * - Later versions may add statuses: treat any status but + * EMBERWAVE_STATUS_OK as a failure. + * - A struct passed by pointer starts with struct_size, which the caller + * sets to sizeof the struct before the call: for the structs it passes in + * and for those it gets filled in. emberwave reads and writes no more + * bytes than that, and takes members a shorter struct lacks as zeros, their + * defaults, so that a program built with an older header works with a + * newer library. + * - Callbacks given to emberwave must not call emberwave. + * - EMBERWAVE_VERSION is the version of this header, and + * emberwave_version_number() that of the library. + * - The calls on Metal, Vulkan and Direct3D objects of the caller's are + * provisional: until a client has used them, a minor version may change + * them. */""" autogen_warning = "/* Generated by cbindgen from crates/emberwave-ffi/src/lib.rs; do not edit. */" include_guard = "EMBERWAVE_H" diff --git a/crates/emberwave-ffi/examples/c/decode.c b/crates/emberwave-ffi/examples/c/decode.c index a0186e5..2f58e07 100644 --- a/crates/emberwave-ffi/examples/c/decode.c +++ b/crates/emberwave-ffi/examples/c/decode.c @@ -74,10 +74,11 @@ int main(int argc, char **argv) unsigned char *file, *planes; FILE *out; EmberwaveStatus status; - EmberwaveStreamInfo stream; + /* A struct passed by pointer starts with its size, which the caller sets. */ + EmberwaveStreamInfo stream = {.struct_size = sizeof stream}; EmberwaveContext *context = NULL; EmberwaveDecoder *decoder = NULL; - EmberwaveFrameInfo frame; + EmberwaveFrameInfo frame = {.struct_size = sizeof frame}; EmberwavePlane y, cb, cr; size_t luma_bytes, chroma_bytes; unsigned frames = 0; @@ -124,10 +125,13 @@ int main(int argc, char **argv) } y.data = planes; y.stride = frame.width; + y.size = luma_bytes; cb.data = planes + luma_bytes; cb.stride = frame.chroma_width; + cb.size = chroma_bytes; cr.data = planes + luma_bytes + chroma_bytes; cr.stride = frame.chroma_width; + cr.size = chroma_bytes; out = fopen(argv[2], "wb"); if (!out) { diff --git a/crates/emberwave-ffi/examples/c/encode.c b/crates/emberwave-ffi/examples/c/encode.c index 88f8314..f45a506 100644 --- a/crates/emberwave-ffi/examples/c/encode.c +++ b/crates/emberwave-ffi/examples/c/encode.c @@ -134,7 +134,8 @@ int main(int argc, char **argv) EmberwaveContext *context = NULL; EmberwaveEncoder *encoder = NULL; EmberwaveDecoder *decoder = NULL; - EmberwaveFrameInfo info; + /* A struct passed by pointer starts with its size, which the caller sets. */ + EmberwaveFrameInfo info = {.struct_size = sizeof info}; EmberwaveStatus status; unsigned char *source[3] = { NULL, NULL, NULL }; unsigned char *decoded[3] = { NULL, NULL, NULL }; @@ -219,13 +220,16 @@ int main(int argc, char **argv) * stride instead of the width. */ y.data = source[0]; y.stride = info.width; + y.size = sizes[0]; cb.data = source[1]; cb.stride = info.chroma_width; + cb.size = sizes[1]; cr.data = source[2]; cr.stride = info.chroma_width; + cr.size = sizes[2]; status = emberwave_encoder_encode(encoder, y, cb, cr, frame, budget, &size); if (status != EMBERWAVE_STATUS_OK) { - if (status == EMBERWAVE_STATUS_INVALID_ARGUMENT && size > budget) { + if (status == EMBERWAVE_STATUS_BUFFER_TOO_SMALL) { fprintf(stderr, "the smallest frame takes %lu bytes\n", (unsigned long)size); } result = fail("emberwave_encoder_encode", status); @@ -269,6 +273,7 @@ int main(int argc, char **argv) for (plane = 0; plane < 3; plane++) { targets[plane].data = decoded[plane]; targets[plane].stride = plane == 0 ? info.width : info.chroma_width; + targets[plane].size = sizes[plane]; } status = emberwave_decoder_decode_yuv(decoder, targets[0], targets[1], targets[2]); if (status != EMBERWAVE_STATUS_OK) { diff --git a/crates/emberwave-ffi/examples/ios/EmberwaveBench/Benchmark.swift b/crates/emberwave-ffi/examples/ios/EmberwaveBench/Benchmark.swift index 1e130f2..84f4dd9 100644 --- a/crates/emberwave-ffi/examples/ios/EmberwaveBench/Benchmark.swift +++ b/crates/emberwave-ffi/examples/ios/EmberwaveBench/Benchmark.swift @@ -273,11 +273,11 @@ final class Emberwave { try check( emberwave_encoder_encode( encoder, - EmberwaveSourcePlane(data: y.baseAddress, stride: size.width), + EmberwaveSourcePlane(data: y.baseAddress, stride: size.width, size: y.count), EmberwaveSourcePlane( - data: cb.baseAddress, stride: size.width / 2), + data: cb.baseAddress, stride: size.width / 2, size: cb.count), EmberwaveSourcePlane( - data: cr.baseAddress, stride: size.width / 2), + data: cr.baseAddress, stride: size.width / 2, size: cr.count), frame.baseAddress, size.budget, &written), "emberwave_encoder_encode") } @@ -379,9 +379,9 @@ final class Emberwave { try check( emberwave_decoder_decode_yuv( decoder, - EmberwavePlane(data: y.baseAddress, stride: size.width), - EmberwavePlane(data: cb.baseAddress, stride: cw), - EmberwavePlane(data: cr.baseAddress, stride: cw)), + EmberwavePlane(data: y.baseAddress, stride: size.width, size: y.count), + EmberwavePlane(data: cb.baseAddress, stride: cw, size: cb.count), + EmberwavePlane(data: cr.baseAddress, stride: cw, size: cr.count)), "emberwave_decoder_decode_yuv") } } @@ -454,8 +454,8 @@ final class Emberwave { "emberwave_decoder_push_packet") } try check( - emberwave_decoder_submit_texture(decoder, nil, texture), - "emberwave_decoder_submit_texture") + emberwave_decoder_submit_texture_metal(decoder, nil, texture), + "emberwave_decoder_submit_texture_metal") guard let commands = appQueue.makeCommandBuffer(), let blit = commands.makeBlitCommandEncoder() else { @@ -510,7 +510,7 @@ final class Emberwave { try rgb.withUnsafeMutableBufferPointer { try check( emberwave_decoder_decode_rgb( - decoder, nil, EmberwavePlane(data: $0.baseAddress, stride: size.width * 3)), + decoder, nil, EmberwavePlane(data: $0.baseAddress, stride: size.width * 3, size: $0.count)), "emberwave_decoder_decode_rgb") } try push() @@ -547,9 +547,9 @@ final class Emberwave { try check( emberwave_decoder_decode_yuv( decoder, - EmberwavePlane(data: y.baseAddress, stride: size.width), - EmberwavePlane(data: cb.baseAddress, stride: cw), - EmberwavePlane(data: cr.baseAddress, stride: cw)), + EmberwavePlane(data: y.baseAddress, stride: size.width, size: y.count), + EmberwavePlane(data: cb.baseAddress, stride: cw, size: cb.count), + EmberwavePlane(data: cr.baseAddress, stride: cw, size: cr.count)), "emberwave_decoder_decode_yuv") } } @@ -613,8 +613,8 @@ final class Emberwave { "emberwave_decoder_push_packet") } try check( - emberwave_decoder_submit_texture(decoder, nil, texture), - "emberwave_decoder_submit_texture") + emberwave_decoder_submit_texture_metal(decoder, nil, texture), + "emberwave_decoder_submit_texture_metal") let metal = try metalTexture(texture, on: device, "queue check") // Metal orders the queue's work by the texture only while it tracks it. guard metal.hazardTrackingMode == .tracked else { @@ -647,9 +647,9 @@ final class Emberwave { try check( emberwave_encoder_encode( encoder, - EmberwaveSourcePlane(data: y.baseAddress, stride: size.width), - EmberwaveSourcePlane(data: cb.baseAddress, stride: size.width / 2), - EmberwaveSourcePlane(data: cr.baseAddress, stride: size.width / 2), + EmberwaveSourcePlane(data: y.baseAddress, stride: size.width, size: y.count), + EmberwaveSourcePlane(data: cb.baseAddress, stride: size.width / 2, size: cb.count), + EmberwaveSourcePlane(data: cr.baseAddress, stride: size.width / 2, size: cr.count), out.baseAddress, size.budget, &written), "emberwave_encoder_encode") } diff --git a/crates/emberwave-ffi/examples/ios/README.md b/crates/emberwave-ffi/examples/ios/README.md index e484446..42a9e78 100644 --- a/crates/emberwave-ffi/examples/ios/README.md +++ b/crates/emberwave-ffi/examples/ios/README.md @@ -4,7 +4,7 @@ - **decode into a Metal texture**: a frame's bytes to the picture in a texture, the path of a player (`emberwave_decoder_decode_texture`); - **decode into Y, Cb, Cr textures**: the same frames to their planes in three textures, for a player that converts the colours itself (`emberwave_decoder_decode_plane_textures`); -- **decode on the app's queue**: the same frames into a texture on the app's own command queue, where the call does not wait (`emberwave_context_create_metal`, `emberwave_decoder_submit_texture`), timed until the app's next command buffer, a copy of one pixel standing in for its draw, has read the picture; +- **decode on the app's queue**: the same frames into a texture on the app's own command queue, where the call does not wait (`emberwave_context_create_metal`, `emberwave_decoder_submit_texture_metal`), timed until the app's next command buffer, a copy of one pixel standing in for its draw, has read the picture; - **decode to planes in memory**: the same frames to Y, Cb and Cr planes in CPU memory (`emberwave_decoder_decode_yuv`); - **encode from memory**: pictures in CPU memory to frames within the byte budget, on the GPU (`emberwave_encoder_encode`); - **HEVC through VideoToolbox**, for comparison, as a Moonlight client decodes a game stream today: the same pictures, encoded by the device's own encoder into a stream at Moonlight's default bit rate and at emberwave's bytes per frame, and decoded by its decoder into NV12 and BGRA textures. diff --git a/crates/emberwave-ffi/examples/swift/Sources/EmberwaveDecode/main.swift b/crates/emberwave-ffi/examples/swift/Sources/EmberwaveDecode/main.swift index 023ef87..904ee59 100644 --- a/crates/emberwave-ffi/examples/swift/Sources/EmberwaveDecode/main.swift +++ b/crates/emberwave-ffi/examples/swift/Sources/EmberwaveDecode/main.swift @@ -42,7 +42,9 @@ func decode(input: String, output: String) throws { let bytes = [UInt8](file) // The file header says how large the pictures are. + // A struct passed by pointer starts with its size, which the caller sets. var stream = EmberwaveStreamInfo() + stream.struct_size = UInt32(MemoryLayout.size) try check(emberwave_probe_pwv1(bytes, bytes.count, &stream), "emberwave_probe_pwv1") // One context per GPU; its decoders share the compiled shaders. @@ -58,6 +60,7 @@ func decode(input: String, output: String) throws { emberwave_decoder_create(context, stream.width, stream.height, stream.chroma, &decoder), "emberwave_decoder_create") var frame = EmberwaveFrameInfo() + frame.struct_size = UInt32(MemoryLayout.size) try check(emberwave_decoder_info(decoder, &frame), "emberwave_decoder_info") // The three planes, back to back, as a Y4M frame holds them. @@ -115,10 +118,12 @@ func decode(input: String, output: String) throws { try planes.withUnsafeMutableBufferPointer { let base = $0.baseAddress! - let y = EmberwavePlane(data: base, stride: Int(frame.width)) - let cb = EmberwavePlane(data: base + lumaBytes, stride: Int(frame.chroma_width)) + let y = EmberwavePlane(data: base, stride: Int(frame.width), size: lumaBytes) + let cb = EmberwavePlane( + data: base + lumaBytes, stride: Int(frame.chroma_width), size: chromaBytes) let cr = EmberwavePlane( - data: base + lumaBytes + chromaBytes, stride: Int(frame.chroma_width)) + data: base + lumaBytes + chromaBytes, stride: Int(frame.chroma_width), + size: chromaBytes) try check( emberwave_decoder_decode_yuv(decoder, y, cb, cr), "emberwave_decoder_decode_yuv") } diff --git a/crates/emberwave-ffi/include/emberwave.h b/crates/emberwave-ffi/include/emberwave.h index 6cf09d9..8820151 100644 --- a/crates/emberwave-ffi/include/emberwave.h +++ b/crates/emberwave-ffi/include/emberwave.h @@ -16,6 +16,20 @@ * not from new ones without end. * - A panic inside emberwave is reported as EMBERWAVE_STATUS_INTERNAL. * Destroy the object it happened in. + * - Later versions may add statuses: treat any status but + * EMBERWAVE_STATUS_OK as a failure. + * - A struct passed by pointer starts with struct_size, which the caller + * sets to sizeof the struct before the call: for the structs it passes in + * and for those it gets filled in. emberwave reads and writes no more + * bytes than that, and takes members a shorter struct lacks as zeros, their + * defaults, so that a program built with an older header works with a + * newer library. + * - Callbacks given to emberwave must not call emberwave. + * - EMBERWAVE_VERSION is the version of this header, and + * emberwave_version_number() that of the library. + * - The calls on Metal, Vulkan and Direct3D objects of the caller's are + * provisional: until a client has used them, a minor version may change + * them. */ #ifndef EMBERWAVE_H @@ -27,6 +41,21 @@ #include #include +// The major version of emberwave that this header belongs to. +#define EMBERWAVE_VERSION_MAJOR 0 + +// The minor version of emberwave that this header belongs to. +#define EMBERWAVE_VERSION_MINOR 16 + +// The patch version of emberwave that this header belongs to. +#define EMBERWAVE_VERSION_PATCH 0 + +// The version this header belongs to as one number, major times 10000 plus +// minor times 100 plus patch: 1600 for 0.16.0. `emberwave_version_number()` +// gives the library's, which differs when a program runs with another +// library than the one it was built for. +#define EMBERWAVE_VERSION 1600 + // 4:2:0: the chroma planes have half the width and height of the luma plane, // whose width and height are even. #define EMBERWAVE_CHROMA_420 0 @@ -135,13 +164,14 @@ // to signal to `signal_value`. #define EMBERWAVE_SYNC_SIGNAL_TIMELINE (1 << 1) -// The result of a call. +// The result of a call. Later versions may add statuses: treat any but +// `EMBERWAVE_STATUS_OK` as a failure. typedef enum EmberwaveStatus { // The call succeeded. EMBERWAVE_STATUS_OK = 0, - // A pointer was `NULL`, a value out of range, an output buffer too - // small, or an object that does not fit the call, such as a texture of - // another device, size or kind. + // A pointer was `NULL`, a value out of range, a struct's `struct_size` + // too small, or an object that does not fit the call, such as a texture + // of another device, size or kind. EMBERWAVE_STATUS_INVALID_ARGUMENT = 1, // The data is not a PyroWave stream this decoder can take: malformed, of // another picture size or chroma format, or not a start of frame header @@ -159,6 +189,16 @@ typedef enum EmberwaveStatus { EMBERWAVE_STATUS_DEVICE_LOST = 6, // A bug in emberwave. Destroy the object the call was made on. EMBERWAVE_STATUS_INTERNAL = 7, + // The system lacks the graphics API of the call, which makes a context + // on it or wraps one of its objects: Metal's off Apple's systems, + // Vulkan's off Linux, Android and 64-bit Windows, Direct3D's off + // Windows. The calls that take what such a context made give + // `EMBERWAVE_STATUS_INVALID_ARGUMENT` for a context of another kind. + EMBERWAVE_STATUS_UNSUPPORTED = 8, + // An output array or buffer of the caller's, or an encode's budget, is + // too small. Where the call can tell how much it needs, it stores that + // where its documentation says. + EMBERWAVE_STATUS_BUFFER_TOO_SMALL = 9, } EmberwaveStatus; // A GPU device, shared by the decoders and encoders created on it. Creating @@ -180,9 +220,9 @@ typedef struct EmberwaveEncoder EmberwaveEncoder; // A texture on a decoder's GPU device that the decodes into textures fill // with the picture, `emberwave_decoder_decode_texture` and -// `emberwave_decoder_submit_texture`, or with one of its planes, +// `emberwave_decoder_submit_texture_metal`, or with one of its planes, // `emberwave_decoder_decode_plane_textures` and -// `emberwave_decoder_submit_plane_textures`, and their `_vulkan` and +// `emberwave_decoder_submit_plane_textures_metal`, and their `_vulkan` and // `_d3d12` forms, for the caller's graphics API to read on the GPU. // emberwave makes it (`emberwave_texture_create`, // `emberwave_texture_create_plane`), wraps a Metal texture or a Vulkan @@ -253,6 +293,9 @@ typedef struct EmberwaveVulkanRequirements EmberwaveVulkanRequirements; // What a stream says about its pictures. typedef struct EmberwaveStreamInfo { + // `sizeof(EmberwaveStreamInfo)`, set by the caller before the call + // (decision D69). + uint32_t struct_size; // Width of the pictures in pixels. uint32_t width; // Height of the pictures in pixels. @@ -275,6 +318,8 @@ typedef struct EmberwaveStreamInfo { // How `emberwave_context_create` chooses the GPU. typedef struct EmberwaveContextOptions { + // `sizeof(EmberwaveContextOptions)` (decision D69). + uint32_t struct_size; // `EMBERWAVE_BACKEND_*` values combined with `|`, or // `EMBERWAVE_BACKEND_ANY`. uint32_t backends; @@ -290,26 +335,30 @@ typedef void (*(*EmberwaveVkGetInstanceProcAddr)(void *instance, const char *nam // A Vulkan instance of the caller's (decision D61), made with the // extensions of `emberwave_vulkan_instance_extensions` among its own. typedef struct EmberwaveVulkanInstance { - // `vkGetInstanceProcAddr` of the loader the instance was made with. - EmberwaveVkGetInstanceProcAddr get_instance_proc_addr; - // The `VkInstance`. - void *instance; + // `sizeof(EmberwaveVulkanInstance)` (decision D69). + uint32_t struct_size; // The `apiVersion` the instance was made with, such as // `VK_API_VERSION_1_3`: at least the physical device's, up to 1.3, // which wgpu uses. uint32_t api_version; + // How many names `extensions` holds. + uint32_t extension_count; + // `vkGetInstanceProcAddr` of the loader the instance was made with. + EmberwaveVkGetInstanceProcAddr get_instance_proc_addr; + // The `VkInstance`. + void *instance; // The instance extensions it was made with: `extension_count` names, or // `NULL` for none. const char *const *extensions; - // How many names `extensions` holds. - uint32_t extension_count; } EmberwaveVulkanInstance; // A Vulkan device of the caller's, and the queue of it that emberwave is to // submit to (decision D61). typedef struct EmberwaveVulkanDevice { + // `sizeof(EmberwaveVulkanDevice)` (decision D69). + uint32_t struct_size; // The instance the device was made with. - struct EmberwaveVulkanInstance instance; + const struct EmberwaveVulkanInstance *instance; // The `VkPhysicalDevice` the device was made on. void *physical_device; // The `VkDevice`, made with the extensions and features that @@ -333,9 +382,11 @@ typedef struct EmberwaveVulkanDevice { // from any thread. `NULL`, with `unlock_queue` `NULL` too, if nothing // else uses the queue during emberwave's calls. Every context on one // queue takes the same `lock_queue`, `unlock_queue` and `user`, which - // stay valid until everything made on those contexts is destroyed. + // stay valid until everything made on those contexts is destroyed. It + // must not call emberwave. void (*lock_queue)(void *user); - // Frees the caller's lock of the queue that `lock_queue` took. + // Frees the caller's lock of the queue that `lock_queue` took. It must + // not call emberwave. void (*unlock_queue)(void *user); // Passed to `lock_queue` and `unlock_queue`. void *user; @@ -346,6 +397,8 @@ typedef struct EmberwaveVulkanDevice { // (decision D61), and for `emberwave_texture_from_vulkan_source` (decision // D62). typedef struct EmberwaveVulkanImage { + // `sizeof(EmberwaveVulkanImage)` (decision D69). + uint32_t struct_size; // The `VkImage`. uint64_t image; // Its `VkFormat`: `VK_FORMAT_B8G8R8A8_UNORM` or @@ -365,8 +418,8 @@ typedef struct EmberwaveVulkanImage { // into it or encode from it has completed: from then on the caller may // destroy the image and free its memory. From any thread, perhaps // during another call of emberwave's on the context, or in the destroy - // of its last decoder, encoder or context. `NULL` if the caller knows - // that moment otherwise. + // of its last decoder, encoder or context, and so it must not call + // emberwave. `NULL` if the caller knows that moment otherwise. void (*release)(void *user); // Passed to `release`. void *user; @@ -374,6 +427,8 @@ typedef struct EmberwaveVulkanImage { // The colour conversion of `emberwave_decoder_decode_rgb`. typedef struct EmberwaveColor { + // `sizeof(EmberwaveColor)` (decision D69). + uint32_t struct_size; // An `EMBERWAVE_MATRIX_*` value. uint32_t matrix; // An `EMBERWAVE_RANGE_*` value. @@ -392,6 +447,11 @@ typedef struct EmberwaveColor { // holds up every later submission to the queue, the caller's too, as the // decode's waits at every stage. typedef struct EmberwaveVulkanSync { + // `sizeof(EmberwaveVulkanSync)` (decision D69). + uint32_t struct_size; + // `EMBERWAVE_SYNC_WAIT_TIMELINE` and `EMBERWAVE_SYNC_SIGNAL_TIMELINE` + // combined with `|` for timeline semaphores, or 0 for binary ones. + uint32_t flags; // A `VkSemaphore` the decode's GPU work waits for, such as one the // caller signals once its reads of the textures are done, or 0 for // none. @@ -403,9 +463,6 @@ typedef struct EmberwaveVulkanSync { uint64_t signal_semaphore; // For a timeline `signal_semaphore`, the value to signal it to. uint64_t signal_value; - // `EMBERWAVE_SYNC_WAIT_TIMELINE` and `EMBERWAVE_SYNC_SIGNAL_TIMELINE` - // combined with `|` for timeline semaphores, or 0 for binary ones. - uint32_t flags; } EmberwaveVulkanSync; // Fences for a call on textures shared with the caller's Direct3D 12 or @@ -419,6 +476,8 @@ typedef struct EmberwaveVulkanSync { // hold on the context's queue, which waits for the queue to be idle, never // returns. typedef struct EmberwaveD3D12Sync { + // `sizeof(EmberwaveD3D12Sync)` (decision D69). + uint32_t struct_size; // A fence the call's GPU work waits for, such as one the caller signals // once its own work on the textures is done, or `NULL`. const struct EmberwaveD3D12Fence *wait_fence; @@ -433,6 +492,9 @@ typedef struct EmberwaveD3D12Sync { // The sizes of the planes a decoder writes or an encoder reads. typedef struct EmberwaveFrameInfo { + // `sizeof(EmberwaveFrameInfo)`, set by the caller before the call + // (decision D69). + uint32_t struct_size; // Width of the luma plane, and of the picture. uint32_t width; // Height of the luma plane, and of the picture. @@ -452,6 +514,9 @@ typedef struct EmberwavePlane { // Bytes from the start of one row to the start of the next: at least the // width of the plane, or three times the width for RGB. size_t stride; + // Bytes that may be written from `data` on: at least `stride` times the + // rows less one, plus a row. + size_t size; } EmberwavePlane; // What the frames of an encoder say about their colours, in every start of @@ -459,6 +524,8 @@ typedef struct EmberwavePlane { // chroma siting; the primaries and the transfer function are for whatever // shows the picture. typedef struct EmberwaveSignalling { + // `sizeof(EmberwaveSignalling)` (decision D69). + uint32_t struct_size; // `EMBERWAVE_MATRIX_BT709` or `EMBERWAVE_MATRIX_BT2020`: the format // cannot signal BT.601. uint32_t matrix; @@ -470,8 +537,6 @@ typedef struct EmberwaveSignalling { uint32_t primaries; // An `EMBERWAVE_TRANSFER_*` value. uint32_t transfer; - // Zeros: room for what later versions may add. - uint32_t reserved[3]; } EmberwaveSignalling; // One plane of a picture to encode, in the caller's memory. @@ -481,6 +546,9 @@ typedef struct EmberwaveSourcePlane { // Bytes from the start of one row to the start of the next: at least the // width of the plane. size_t stride; + // Bytes that may be read from `data` on: at least `stride` times the + // rows less one, plus a row. + size_t size; } EmberwaveSourcePlane; // One packet of a frame: where it starts in the frame and how many bytes it @@ -499,6 +567,11 @@ extern "C" { // The version of emberwave, such as `"0.3.0"`. The string is static. const char *emberwave_version(void); +// The version of the library as one number, as `EMBERWAVE_VERSION` numbers +// the header's: 1600 for 0.16.0. A program compares the two to learn that +// it runs with another library than the one it was built for. +uint32_t emberwave_version_number(void); + // Describes the last failure on the calling thread, or is empty if no call on // it has failed. The string stays valid until the next failing call on the // same thread, and no longer than the thread: copy it to keep it, or to @@ -555,12 +628,12 @@ enum EmberwaveStatus emberwave_context_create(const struct EmberwaveContextOptio // `device` is an `id` and `queue` an `id` of it, // which emberwave retains while the context, or anything made on it, lives. // emberwave's GPU work then goes onto the caller's queue, and -// `emberwave_decoder_submit_texture` and -// `emberwave_decoder_submit_plane_textures` need not wait for it: Metal runs +// `emberwave_decoder_submit_texture_metal` and +// `emberwave_decoder_submit_plane_textures_metal` need not wait for it: Metal runs // the command buffers the caller commits to the queue after such a call // after the decode, and a decode after the command buffers committed before // it, for the textures they share (decision D43). Only on iPhones, iPads and -// Macs; elsewhere `EMBERWAVE_STATUS_NO_GPU` for arguments that are not +// Macs; elsewhere `EMBERWAVE_STATUS_UNSUPPORTED` for arguments that are not // `NULL`. // // The queue is shared: @@ -598,19 +671,22 @@ enum EmberwaveStatus emberwave_context_create_metal(void *device, // `VK_KHR_get_physical_device_properties2` it learns less of a device and // uses fewer of its features. `VK_KHR_surface` goes with the // `VK_KHR_swapchain` that `emberwave_vulkan_requirements_create` lists on -// devices that have it. With `names` `NULL`, stores their number in -// `*count`; otherwise writes them to `names`, which has room for `*count`, -// or gives `EMBERWAVE_STATUS_INVALID_ARGUMENT` with the number needed in -// `*count` if that is too few. The names stay valid as long as the process. -// Only where wgpu runs on Vulkan; elsewhere `EMBERWAVE_STATUS_NO_GPU`. +// devices that have it. Stores their number in `*count`, and with `names` +// not `NULL` writes them there too, which has room for `capacity` names; if +// that is too few, nothing is written to `names` and the result is +// `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, as for `emberwave_frame_packets`. +// The counts are `uint32_t`, as Vulkan's are. The names stay valid as long +// as the process. Only where wgpu runs on Vulkan; elsewhere +// `EMBERWAVE_STATUS_UNSUPPORTED`. // // # Safety // // `get_instance_proc_addr` is `NULL` or a loader's `vkGetInstanceProcAddr`, -// `count` points to a `uint32_t`, and `names` is `NULL` or points to -// `*count` pointers that may be written. +// `names` is `NULL` or points to `capacity` pointers that may be written, +// and `count` points to a `uint32_t`. enum EmberwaveStatus emberwave_vulkan_instance_extensions(EmberwaveVkGetInstanceProcAddr get_instance_proc_addr, const char **names, + uint32_t capacity, uint32_t *count); // Lists what a Vulkan device of the caller's must have enabled for @@ -620,8 +696,8 @@ enum EmberwaveStatus emberwave_vulkan_instance_extensions(EmberwaveVkGetInstance // `emberwave_vulkan_requirements_features` gives. Make the device with // them, and with whatever else the caller needs. Only where wgpu runs on // Vulkan, on Linux, Android and 64-bit Windows; elsewhere -// `EMBERWAVE_STATUS_NO_GPU`. -// `EMBERWAVE_STATUS_NO_GPU` too for a physical device wgpu cannot use. +// `EMBERWAVE_STATUS_UNSUPPORTED`. `EMBERWAVE_STATUS_NO_GPU` for a physical +// device wgpu cannot use. // // # Safety // @@ -674,7 +750,7 @@ void emberwave_vulkan_requirements_destroy(struct EmberwaveVulkanRequirements *r // work on the caller's queue (decision D61). The device keeps belonging to // the caller, who destroys it after the context and everything made on it. // Only where wgpu runs on Vulkan, on Linux, Android and 64-bit Windows; -// elsewhere `EMBERWAVE_STATUS_NO_GPU`. +// elsewhere `EMBERWAVE_STATUS_UNSUPPORTED`. // // The device must have been made with the extensions and the features of // `emberwave_vulkan_requirements_create`. A lacking extension is @@ -700,14 +776,16 @@ void emberwave_vulkan_requirements_destroy(struct EmberwaveVulkanRequirements *r // - The decodes that wait return once the GPU is done. Those that do not // wait are `emberwave_decoder_submit_texture_vulkan` and // `emberwave_decoder_submit_plane_textures_vulkan`, which order the work -// with semaphores; `emberwave_decoder_submit_texture` and -// `emberwave_decoder_submit_plane_textures` are for Metal contexts only. +// with semaphores; `emberwave_decoder_submit_texture_metal` and +// `emberwave_decoder_submit_plane_textures_metal` are for Metal contexts only. // // # Safety // // `device` is `NULL` or points to an `EmberwaveVulkanDevice` that describes // a live device made as above, with a queue of that family and index, and -// callbacks that may be called from any thread. `context` points to where +// callbacks that may be called from any thread; its `instance` is `NULL` +// or points to an `EmberwaveVulkanInstance` with its `struct_size` set. +// `context` points to where // the new context is stored; it is set to `NULL` when the call fails. enum EmberwaveStatus emberwave_context_create_vulkan(const struct EmberwaveVulkanDevice *device, struct EmberwaveContext **context); @@ -1251,9 +1329,9 @@ void emberwave_decoder_clear(struct EmberwaveDecoder *decoder); // # Safety // // `decoder` is a live decoder that no other thread is using. Each plane's -// `data` points to `stride * (height - 1) + width` bytes that may be -// written, with the sizes of `emberwave_decoder_info`, and the planes do -// not overlap. +// `data` points to `size` bytes that may be written, `size` being at least +// `stride * (height - 1) + width` for the sizes of +// `emberwave_decoder_info`, and the planes do not overlap. enum EmberwaveStatus emberwave_decoder_decode_yuv(struct EmberwaveDecoder *decoder, struct EmberwavePlane y, struct EmberwavePlane cb, @@ -1277,7 +1355,8 @@ enum EmberwaveStatus emberwave_decoder_decode_yuv(struct EmberwaveDecoder *decod // // `decoder` is a live decoder that no other thread is using, `color` is // `NULL` or points to an `EmberwaveColor`, and `rgb.data` points to -// `stride * (height - 1) + 3 * width` bytes that may be written. +// `rgb.size` bytes that may be written, `rgb.size` being at least +// `stride * (height - 1) + 3 * width`. enum EmberwaveStatus emberwave_decoder_decode_rgb(struct EmberwaveDecoder *decoder, const struct EmberwaveColor *color, struct EmberwavePlane rgb); @@ -1324,7 +1403,7 @@ enum EmberwaveStatus emberwave_texture_create_plane(const struct EmberwaveDecode // `emberwave_texture_create`'s: a texture of a `CVPixelBuffer` from a // `CVMetalTextureCache`, to hand to an `AVSampleBufferDisplayLayer`, one the // caller made, or a drawable (decision D44). Only on iPhones, iPads and -// Macs; elsewhere `EMBERWAVE_STATUS_NO_GPU` for arguments that are not +// Macs; elsewhere `EMBERWAVE_STATUS_UNSUPPORTED` for arguments that are not // `NULL`. // // The texture is on the device of the decoder's context @@ -1387,7 +1466,7 @@ enum EmberwaveStatus emberwave_texture_from_metal_plane(const struct EmberwaveDe // of an NV12 pixel buffer holds them, or `EMBERWAVE_PLANE_RGB` for the // whole picture in `MTLPixelFormatBGRA8Unorm` or `MTLPixelFormatRGBA8Unorm`, // as a BGRA pixel buffer holds it (decision D65). Only on iPhones, iPads and -// Macs; elsewhere `EMBERWAVE_STATUS_NO_GPU` for arguments that are not +// Macs; elsewhere `EMBERWAVE_STATUS_UNSUPPORTED` for arguments that are not // `NULL`. // // The texture is on the device of the encoder's context @@ -1498,13 +1577,13 @@ enum EmberwaveStatus emberwave_decoder_decode_texture(struct EmberwaveDecoder *d // # Safety // // As for `emberwave_decoder_decode_texture`. -enum EmberwaveStatus emberwave_decoder_submit_texture(struct EmberwaveDecoder *decoder, - const struct EmberwaveColor *color, - const struct EmberwaveTexture *texture); +enum EmberwaveStatus emberwave_decoder_submit_texture_metal(struct EmberwaveDecoder *decoder, + const struct EmberwaveColor *color, + const struct EmberwaveTexture *texture); // Decodes the frame received so far into the plane textures `y`, `cb` and // `cr`, as `emberwave_decoder_decode_plane_textures` does, but returns once -// the work is on the queue, as `emberwave_decoder_submit_texture` does: only +// the work is on the queue, as `emberwave_decoder_submit_texture_metal` does: only // for a decoder whose context was made by `emberwave_context_create_metal`, // and textures whose hazards Metal tracks. Vulkan and Direct3D contexts have // `emberwave_decoder_submit_plane_textures_vulkan` and @@ -1513,10 +1592,10 @@ enum EmberwaveStatus emberwave_decoder_submit_texture(struct EmberwaveDecoder *d // # Safety // // As for `emberwave_decoder_decode_plane_textures`. -enum EmberwaveStatus emberwave_decoder_submit_plane_textures(struct EmberwaveDecoder *decoder, - const struct EmberwaveTexture *y, - const struct EmberwaveTexture *cb, - const struct EmberwaveTexture *cr); +enum EmberwaveStatus emberwave_decoder_submit_plane_textures_metal(struct EmberwaveDecoder *decoder, + const struct EmberwaveTexture *y, + const struct EmberwaveTexture *cb, + const struct EmberwaveTexture *cr); // Decodes the frame received so far into its Y, Cb and Cr planes in `y`, // `cb` and `cr` on the GPU, then waits for the GPU: when this returns, @@ -1642,7 +1721,7 @@ enum EmberwaveStatus emberwave_encoder_info(struct EmberwaveEncoder *encoder, // decoder. The planes have the sizes of `emberwave_encoder_info`, each row // `stride` bytes from the next, and the bytes between the rows are not // read. A budget too small for even the smallest frame is -// `EMBERWAVE_STATUS_INVALID_ARGUMENT` with the smallest frame's size in +// `EMBERWAVE_STATUS_BUFFER_TOO_SMALL` with the smallest frame's size in // `*size`; nothing is encoded then, and the next frame takes the sequence // number this one would have had. Waits for the GPU. // @@ -1653,7 +1732,8 @@ enum EmberwaveStatus emberwave_encoder_info(struct EmberwaveEncoder *encoder, // # Safety // // `encoder` is a live encoder that no other thread is using. Each plane's -// `data` points to `stride * (height - 1) + width` readable bytes. `frame` +// `data` points to `size` readable bytes, `size` being at least +// `stride * (height - 1) + width`. `frame` // points to `budget` bytes that may be written, and `size` to a `size_t`; // neither overlaps the planes or the other. enum EmberwaveStatus emberwave_encoder_encode(struct EmberwaveEncoder *encoder, @@ -1679,8 +1759,9 @@ enum EmberwaveStatus emberwave_encoder_encode(struct EmberwaveEncoder *encoder, // # Safety // // `encoder` is a live encoder that no other thread is using. `rgb.data` -// points to `rgb.stride * (height - 1) + 4 * width` readable bytes, at the -// size `emberwave_encoder_info` gives. `frame` points to `budget` bytes that +// points to `rgb.size` readable bytes, `rgb.size` being at least +// `rgb.stride * (height - 1) + 4 * width` at the size +// `emberwave_encoder_info` gives. `frame` points to `budget` bytes that // may be written, and `size` to a `size_t`; neither overlaps the picture or // the other. enum EmberwaveStatus emberwave_encoder_encode_rgb(struct EmberwaveEncoder *encoder, @@ -1703,9 +1784,10 @@ enum EmberwaveStatus emberwave_encoder_encode_rgb(struct EmberwaveEncoder *encod // `emberwave_texture_from_vulkan_source`, or textures that decodes put // planes or pictures into. Others give `EMBERWAVE_STATUS_INVALID_ARGUMENT` // before any work on the GPU, textures shared from Direct3D included, which -// take `emberwave_encoder_encode_textures_d3d12`. A budget too small gives it too, as for -// `emberwave_encoder_encode`, once the GPU has encoded the picture: no -// frame then, and the sequence number does not move. +// take `emberwave_encoder_encode_textures_d3d12`. A budget too small gives +// `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, as for `emberwave_encoder_encode`, +// once the GPU has encoded the picture: no frame then, and the sequence +// number does not move. // // Waits for the GPU: when it returns, the GPU has read the textures, and // the caller may write them again or hand them back to a capture's pool. @@ -1746,7 +1828,7 @@ enum EmberwaveStatus emberwave_encoder_encode_textures(struct EmberwaveEncoder * // boundaries of its blocks, and stores how many in `*count`. With `packets` // not `NULL`, also where each starts in the frame and its size, in order; // with fewer than `*count` places there (`capacity`), nothing is written to -// `packets` and the result is `EMBERWAVE_STATUS_INVALID_ARGUMENT`. The first +// `packets` and the result is `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`. The first // packet starts with the start of frame header. A block larger than // `max_packet_bytes` gets a packet of its own, as does the rest of a frame // from a malformed block on, so a packet can be larger. A decoder takes the diff --git a/crates/emberwave-ffi/src/lib.rs b/crates/emberwave-ffi/src/lib.rs index 587dab8..0256e21 100644 --- a/crates/emberwave-ffi/src/lib.rs +++ b/crates/emberwave-ffi/src/lib.rs @@ -20,6 +20,20 @@ //! not from new ones without end. //! - A panic inside emberwave is caught and reported as //! `EMBERWAVE_STATUS_INTERNAL`. Destroy the object it happened in. +//! - Later versions may add statuses: treat any status but +//! `EMBERWAVE_STATUS_OK` as a failure. +//! - A struct passed by pointer starts with `struct_size`, which the caller +//! sets to `sizeof` the struct, as its header declares it, before the call: +//! for the structs it passes in and for those it gets filled in. emberwave +//! reads and writes no more bytes than that, and takes members a shorter +//! struct lacks as zeros, their defaults, so that a program built with an +//! older header works with a newer library (decision D69). +//! - Callbacks the caller gives emberwave must not call emberwave. +//! - `EMBERWAVE_VERSION` is the version of the header, +//! `emberwave_version_number()` that of the library. +//! - The calls on Metal, Vulkan and Direct3D objects of the caller's are +//! provisional: until a client has used them, a minor version may change +//! them (decision D69). use std::cell::RefCell; use std::ffi::{CString, c_char, c_void}; @@ -36,15 +50,16 @@ use emberwave_core::gpu::{ Decoder, Encoder, GpuContext, GpuError, GpuOptions, QueueTurns, TexturePicture, }; -/// The result of a call. +/// The result of a call. Later versions may add statuses: treat any but +/// `EMBERWAVE_STATUS_OK` as a failure. #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EmberwaveStatus { /// The call succeeded. Ok = 0, - /// A pointer was `NULL`, a value out of range, an output buffer too - /// small, or an object that does not fit the call, such as a texture of - /// another device, size or kind. + /// A pointer was `NULL`, a value out of range, a struct's `struct_size` + /// too small, or an object that does not fit the call, such as a texture + /// of another device, size or kind. InvalidArgument = 1, /// The data is not a PyroWave stream this decoder can take: malformed, of /// another picture size or chroma format, or not a start of frame header @@ -62,6 +77,55 @@ pub enum EmberwaveStatus { DeviceLost = 6, /// A bug in emberwave. Destroy the object the call was made on. Internal = 7, + /// The system lacks the graphics API of the call, which makes a context + /// on it or wraps one of its objects: Metal's off Apple's systems, + /// Vulkan's off Linux, Android and 64-bit Windows, Direct3D's off + /// Windows. The calls that take what such a context made give + /// `EMBERWAVE_STATUS_INVALID_ARGUMENT` for a context of another kind. + Unsupported = 8, + /// An output array or buffer of the caller's, or an encode's budget, is + /// too small. Where the call can tell how much it needs, it stores that + /// where its documentation says. + BufferTooSmall = 9, +} + +/// The major version of emberwave that this header belongs to. +pub const EMBERWAVE_VERSION_MAJOR: u32 = 0; +/// The minor version of emberwave that this header belongs to. +pub const EMBERWAVE_VERSION_MINOR: u32 = 16; +/// The patch version of emberwave that this header belongs to. +pub const EMBERWAVE_VERSION_PATCH: u32 = 0; +/// The version this header belongs to as one number, major times 10000 plus +/// minor times 100 plus patch: 1600 for 0.16.0. `emberwave_version_number()` +/// gives the library's, which differs when a program runs with another +/// library than the one it was built for. +pub const EMBERWAVE_VERSION: u32 = 1600; + +/// The library's version as `EMBERWAVE_VERSION` numbers it, from the crate's. +const VERSION_NUMBER: u32 = decimal(env!("CARGO_PKG_VERSION_MAJOR")) * 10000 + + decimal(env!("CARGO_PKG_VERSION_MINOR")) * 100 + + decimal(env!("CARGO_PKG_VERSION_PATCH")); + +// The header's version is the crate's: a release changes both. +const _: () = assert!( + VERSION_NUMBER == EMBERWAVE_VERSION + && EMBERWAVE_VERSION + == EMBERWAVE_VERSION_MAJOR * 10000 + + EMBERWAVE_VERSION_MINOR * 100 + + EMBERWAVE_VERSION_PATCH, + "EMBERWAVE_VERSION* is not the crate's version" +); + +/// The number written in decimal `digits`. +const fn decimal(digits: &str) -> u32 { + let bytes = digits.as_bytes(); + let mut value = 0; + let mut at = 0; + while at < bytes.len() { + value = value * 10 + (bytes[at] - b'0') as u32; + at += 1; + } + value } /// 4:2:0: the chroma planes have half the width and height of the luma plane, @@ -151,6 +215,8 @@ pub const EMBERWAVE_PLANE_RGB: u32 = 4; #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveContextOptions { + /// `sizeof(EmberwaveContextOptions)` (decision D69). + pub struct_size: u32, /// `EMBERWAVE_BACKEND_*` values combined with `|`, or /// `EMBERWAVE_BACKEND_ANY`. pub backends: u32, @@ -160,8 +226,11 @@ pub struct EmberwaveContextOptions { /// What a stream says about its pictures. #[repr(C)] -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct EmberwaveStreamInfo { + /// `sizeof(EmberwaveStreamInfo)`, set by the caller before the call + /// (decision D69). + pub struct_size: u32, /// Width of the pictures in pixels. pub width: u32, /// Height of the pictures in pixels. @@ -184,8 +253,11 @@ pub struct EmberwaveStreamInfo { /// The sizes of the planes a decoder writes or an encoder reads. #[repr(C)] -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct EmberwaveFrameInfo { + /// `sizeof(EmberwaveFrameInfo)`, set by the caller before the call + /// (decision D69). + pub struct_size: u32, /// Width of the luma plane, and of the picture. pub width: u32, /// Height of the luma plane, and of the picture. @@ -207,12 +279,17 @@ pub struct EmberwavePlane { /// Bytes from the start of one row to the start of the next: at least the /// width of the plane, or three times the width for RGB. pub stride: usize, + /// Bytes that may be written from `data` on: at least `stride` times the + /// rows less one, plus a row. + pub size: usize, } /// The colour conversion of `emberwave_decoder_decode_rgb`. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveColor { + /// `sizeof(EmberwaveColor)` (decision D69). + pub struct_size: u32, /// An `EMBERWAVE_MATRIX_*` value. pub matrix: u32, /// An `EMBERWAVE_RANGE_*` value. @@ -228,6 +305,8 @@ pub struct EmberwaveColor { #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveSignalling { + /// `sizeof(EmberwaveSignalling)` (decision D69). + pub struct_size: u32, /// `EMBERWAVE_MATRIX_BT709` or `EMBERWAVE_MATRIX_BT2020`: the format /// cannot signal BT.601. pub matrix: u32, @@ -239,8 +318,6 @@ pub struct EmberwaveSignalling { pub primaries: u32, /// An `EMBERWAVE_TRANSFER_*` value. pub transfer: u32, - /// Zeros: room for what later versions may add. - pub reserved: [u32; 3], } /// One plane of a picture to encode, in the caller's memory. @@ -252,6 +329,9 @@ pub struct EmberwaveSourcePlane { /// Bytes from the start of one row to the start of the next: at least the /// width of the plane. pub stride: usize, + /// Bytes that may be read from `data` on: at least `stride` times the + /// rows less one, plus a row. + pub size: usize, } /// One packet of a frame: where it starts in the frame and how many bytes it @@ -265,6 +345,18 @@ pub struct EmberwavePacket { pub size: usize, } +// The threads the header promises: a context is used by several threads at +// once, and decoders, encoders and textures move between threads. A change +// that broke this would fail to build, not mislead callers (decision D69). +const _: () = { + const fn shared() {} + const fn movable() {} + shared::(); + movable::(); + movable::(); + movable::(); +}; + /// A GPU device, shared by the decoders and encoders created on it. Creating /// one compiles the shaders, so make one and keep it. pub struct EmberwaveContext { @@ -326,9 +418,9 @@ pub struct EmberwaveDecoder { /// A texture on a decoder's GPU device that the decodes into textures fill /// with the picture, `emberwave_decoder_decode_texture` and -/// `emberwave_decoder_submit_texture`, or with one of its planes, +/// `emberwave_decoder_submit_texture_metal`, or with one of its planes, /// `emberwave_decoder_decode_plane_textures` and -/// `emberwave_decoder_submit_plane_textures`, and their `_vulkan` and +/// `emberwave_decoder_submit_plane_textures_metal`, and their `_vulkan` and /// `_d3d12` forms, for the caller's graphics API to read on the GPU. /// emberwave makes it (`emberwave_texture_create`, /// `emberwave_texture_create_plane`), wraps a Metal texture or a Vulkan @@ -598,8 +690,9 @@ impl From for Failure { GpuError::Device(_) | GpuError::Readback(_) => EmberwaveStatus::Gpu, GpuError::DeviceLost(_) => EmberwaveStatus::DeviceLost, GpuError::IncompleteFrame { .. } | GpuError::Bitstream(_) => EmberwaveStatus::Bitstream, - // Planes or a budget the encoder cannot take, from - // emberwave_encoder_encode; a texture a decode cannot fill. + GpuError::Encode(EncodeError::BudgetTooSmall { .. }) => EmberwaveStatus::BufferTooSmall, + // Planes the encoder cannot take, from emberwave_encoder_encode; + // a texture a decode cannot fill. GpuError::Encode(_) | GpuError::Texture(_) => EmberwaveStatus::InvalidArgument, }; Self(status, error.to_string()) @@ -612,6 +705,162 @@ impl From for Failure { } } +/// A struct of the C API that starts with `struct_size` (decision D69). +/// +/// # Safety +/// +/// The type is `repr(C)`, its first member is the `u32` `struct_size`, and +/// any bytes are a value of it, zeros its defaults: its members are +/// integers, raw pointers and optional function pointers. +unsafe trait Versioned: Copy { + /// The struct's name, for errors. + const NAME: &'static str; + /// The least `struct_size` a caller may give: where the last member of + /// the struct's first layout, that of 0.17.0, ends. A member added later + /// lies past it. + const FIRST_SIZE: usize; +} + +/// Makes each struct [`Versioned`], given the last member of its first +/// layout, which never changes, and its last member now, which changes when +/// a member is added. It checks at compile time that `struct_size` comes +/// first, and that no padding follows the last member: a member added there +/// would leave the struct's size, and so `struct_size`, as it was, and the +/// library would read it from the padding of an older caller's struct. +macro_rules! versioned { + ($($name:ident { first_ends_with: $first:ident: $first_ty:ty, ends_with: $last:ident: $last_ty:ty }),* $(,)?) => {$( + // SAFETY: repr(C), `struct_size` first, which the assertions below + // check, and integers, raw pointers and optional function pointers + // only. + unsafe impl Versioned for $name { + const NAME: &'static str = stringify!($name); + const FIRST_SIZE: usize = + std::mem::offset_of!($name, $first) + std::mem::size_of::<$first_ty>(); + } + + const _: () = { + assert!(std::mem::offset_of!($name, struct_size) == 0); + assert!( + std::mem::offset_of!($name, $last) + std::mem::size_of::<$last_ty>() + == std::mem::size_of::<$name>(), + "padding after the last member" + ); + assert!(<$name as Versioned>::FIRST_SIZE <= std::mem::size_of::<$name>()); + }; + + /// Zeros, the defaults, with `struct_size` set. + impl Default for $name { + fn default() -> Self { + // SAFETY: zeros are a value of a `Versioned` struct. + let mut value: Self = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + value.struct_size = std::mem::size_of::() as u32; + value + } + } + )*}; +} + +versioned!( + EmberwaveContextOptions { first_ends_with: flags: u32, ends_with: flags: u32 }, + EmberwaveStreamInfo { first_ends_with: frame_rate_den: u32, ends_with: frame_rate_den: u32 }, + EmberwaveFrameInfo { first_ends_with: chroma_height: u32, ends_with: chroma_height: u32 }, + EmberwaveColor { first_ends_with: chroma_siting: u32, ends_with: chroma_siting: u32 }, + EmberwaveSignalling { first_ends_with: transfer: u32, ends_with: transfer: u32 }, + EmberwaveVulkanInstance { + first_ends_with: extensions: *const *const c_char, + ends_with: extensions: *const *const c_char + }, + EmberwaveVulkanDevice { first_ends_with: user: *mut c_void, ends_with: user: *mut c_void }, + EmberwaveVulkanImage { first_ends_with: user: *mut c_void, ends_with: user: *mut c_void }, + EmberwaveVulkanSync { first_ends_with: signal_value: u64, ends_with: signal_value: u64 }, + EmberwaveD3D12Sync { first_ends_with: signal_value: u64, ends_with: signal_value: u64 }, +); + +/// The `struct_size` of the struct at `ptr`, `what` to the caller, checked +/// to be at least the struct's first size. +/// +/// # Safety +/// +/// `ptr` points to a struct of `T`'s kind, whose first member is +/// `struct_size`. +unsafe fn struct_size(ptr: *const T, what: &str) -> Result { + // SAFETY: an aligned `T` starts with a `u32`, as the caller promises. + let size = unsafe { ptr.cast::().read() } as usize; + if size < T::FIRST_SIZE { + return Err(Failure::argument(format!( + "{what}.struct_size is {size}, less than the {} bytes of an {}: set it to sizeof({})", + T::FIRST_SIZE, + T::NAME, + T::NAME + ))); + } + Ok(size) +} + +/// The struct at `ptr`, `None` for `NULL`: the bytes its `struct_size` +/// covers, up to the size this version knows, and zeros, the defaults, for +/// the members past them, which a caller built with an older header lacks. +/// +/// # Safety +/// +/// `ptr` is `NULL` or points to `struct_size` readable bytes of a struct of +/// `T`'s kind. +unsafe fn read_struct(ptr: *const T, what: &str) -> Result, Failure> { + if ptr.is_null() { + return Ok(None); + } + // SAFETY: as the caller promises. + let size = unsafe { struct_size(ptr, what) }?; + let mut value = std::mem::MaybeUninit::::zeroed(); + // SAFETY: the caller's struct has `size` readable bytes, and as many of + // them as `T` has go into the zeroed value, which any bytes are a value + // of (`Versioned`). + unsafe { + std::ptr::copy_nonoverlapping( + ptr.cast::(), + value.as_mut_ptr().cast::(), + size.min(std::mem::size_of::()), + ); + Ok(Some(value.assume_init())) + } +} + +/// [`read_struct`] for a struct that may not be `NULL`. +/// +/// # Safety +/// +/// As for [`read_struct`]. +unsafe fn read_given(ptr: *const T, what: &str) -> Result { + // SAFETY: as the caller promises. + unsafe { read_struct(ptr, what) }?.ok_or_else(|| Failure::argument(format!("{what} is NULL"))) +} + +/// Writes `value` to the struct at `out`, `what` to the caller: the members +/// after `struct_size`, which stays the caller's, as far as its size covers. +/// +/// # Safety +/// +/// `out` is `NULL` or points to `struct_size` writable bytes of a struct of +/// `T`'s kind. +unsafe fn write_struct(out: *mut T, what: &str, value: T) -> Result<(), Failure> { + if out.is_null() { + return Err(Failure::argument(format!("{what} is NULL"))); + } + // SAFETY: as the caller promises. + let size = unsafe { struct_size(out, what) }?.min(std::mem::size_of::()); + let skip = std::mem::size_of::(); + // SAFETY: `size` is at least the first size, so past `struct_size`, and + // the caller's struct has that many writable bytes. + unsafe { + std::ptr::copy_nonoverlapping( + (&value as *const T).cast::().add(skip), + out.cast::().add(skip), + size - skip, + ); + } + Ok(()) +} + /// Runs the body of an API function: keeps the message of a failure for /// [`emberwave_last_error`] and turns a panic into /// [`EmberwaveStatus::Internal`]. @@ -767,8 +1016,8 @@ unsafe fn conversion( color: *const EmberwaveColor, ) -> Result { // SAFETY: as the caller promises. - match unsafe { color.as_ref() } { - Some(color) => color_params(color), + match unsafe { read_struct(color, "color") }? { + Some(color) => color_params(&color), None => Ok(decoder .decoder .start_of_frame() @@ -789,9 +1038,6 @@ fn creation_failure(error: GpuError) -> Failure { } fn signalling(value: &EmberwaveSignalling) -> Result { - if value.reserved != [0; 3] { - return Err(Failure::argument("signalling.reserved is not zero")); - } Ok(Signalling { color_primaries: match value.primaries { EMBERWAVE_PRIMARIES_SRGB => ColorPrimaries::Srgb, @@ -835,8 +1081,8 @@ fn signalling(value: &EmberwaveSignalling) -> Result { /// /// # Safety /// -/// `source.data` is `NULL` or points to `stride * (rows - 1) + row_bytes` -/// readable bytes that stay unchanged while the slice is used. +/// `source.data` is `NULL` or points to `source.size` readable bytes that +/// stay unchanged while the slice is used. unsafe fn source_plane<'a>( source: &EmberwaveSourcePlane, name: &str, @@ -862,6 +1108,12 @@ unsafe fn source_plane<'a>( "{name} spans more memory than a pointer can address" )) })?; + if source.size < span { + return Err(Failure::argument(format!( + "{name}.size is {}, less than the {span} bytes its {rows} rows span", + source.size + ))); + } // SAFETY: not NULL, not too large, and the caller promises the bytes. Ok(unsafe { std::slice::from_raw_parts(source.data, span) }) } @@ -876,6 +1128,7 @@ fn chroma_code(chroma: ChromaSubsampling) -> u32 { fn stream_info(stream: &SequenceHeader) -> EmberwaveStreamInfo { let color = ColorParams::from_stream(stream); EmberwaveStreamInfo { + struct_size: std::mem::size_of::() as u32, width: stream.width, height: stream.height, chroma: chroma_code(stream.chroma), @@ -958,8 +1211,8 @@ fn color_params(color: &EmberwaveColor) -> Result { } /// Checks that `target` takes `rows` rows of `row_bytes` bytes: its pointer is -/// not `NULL`, its stride is at least a row, and the last byte is -/// addressable. +/// not `NULL`, its stride is at least a row, and its size reaches the last +/// byte, which is addressable. fn check_target( target: &EmberwavePlane, name: &str, @@ -979,11 +1232,20 @@ fn check_target( .stride .checked_mul(rows.saturating_sub(1)) .and_then(|bytes| bytes.checked_add(row_bytes)) - .filter(|&bytes| bytes <= isize::MAX as usize); - if span.is_none() { - return Err(Failure::argument(format!( - "{name} spans more memory than a pointer can address" - ))); + .filter(|&bytes| bytes <= isize::MAX as usize) + .ok_or_else(|| { + Failure::argument(format!( + "{name} spans more memory than a pointer can address" + )) + })?; + if target.size < span { + return Err(Failure( + EmberwaveStatus::BufferTooSmall, + format!( + "{name}.size is {}, less than the {span} bytes its {rows} rows span", + target.size + ), + )); } Ok(()) } @@ -1016,6 +1278,14 @@ pub extern "C" fn emberwave_version() -> *const c_char { VERSION.as_ptr().cast() } +/// The version of the library as one number, as `EMBERWAVE_VERSION` numbers +/// the header's: 1600 for 0.16.0. A program compares the two to learn that +/// it runs with another library than the one it was built for. +#[unsafe(no_mangle)] +pub extern "C" fn emberwave_version_number() -> u32 { + VERSION_NUMBER +} + /// Describes the last failure on the calling thread, or is empty if no call on /// it has failed. The string stays valid until the next failing call on the /// same thread, and no longer than the thread: copy it to keep it, or to @@ -1053,7 +1323,7 @@ pub unsafe extern "C" fn emberwave_probe_pwv1( ..stream_info(&header.stream) }; // SAFETY: as the caller promises. - unsafe { output(info, "info", described) } + unsafe { write_struct(info, "info", described) } }) } @@ -1077,7 +1347,9 @@ pub unsafe extern "C" fn emberwave_probe_packet( let bytes = unsafe { input(data, size) }?; match Header::parse(bytes)? { // SAFETY: as the caller promises. - Header::StartOfFrame(stream) => unsafe { output(info, "info", stream_info(&stream)) }, + Header::StartOfFrame(stream) => unsafe { + write_struct(info, "info", stream_info(&stream)) + }, Header::Block(_) => Err(Failure( EmberwaveStatus::Bitstream, "the packet starts with a block, not with a start of frame header".into(), @@ -1112,8 +1384,8 @@ pub unsafe extern "C" fn emberwave_context_create( // SAFETY: as the caller promises. unsafe { output(context, "context", std::ptr::null_mut()) }?; // SAFETY: as the caller promises. - let gpu_options = match unsafe { options.as_ref() } { - Some(options) => gpu_options(options)?, + let gpu_options = match unsafe { read_struct(options, "options") }? { + Some(options) => gpu_options(&options)?, None => GpuOptions::default(), }; let created = GpuContext::new(&gpu_options)?; @@ -1127,12 +1399,12 @@ pub unsafe extern "C" fn emberwave_context_create( /// `device` is an `id` and `queue` an `id` of it, /// which emberwave retains while the context, or anything made on it, lives. /// emberwave's GPU work then goes onto the caller's queue, and -/// `emberwave_decoder_submit_texture` and -/// `emberwave_decoder_submit_plane_textures` need not wait for it: Metal runs +/// `emberwave_decoder_submit_texture_metal` and +/// `emberwave_decoder_submit_plane_textures_metal` need not wait for it: Metal runs /// the command buffers the caller commits to the queue after such a call /// after the decode, and a decode after the command buffers committed before /// it, for the textures they share (decision D43). Only on iPhones, iPads and -/// Macs; elsewhere `EMBERWAVE_STATUS_NO_GPU` for arguments that are not +/// Macs; elsewhere `EMBERWAVE_STATUS_UNSUPPORTED` for arguments that are not /// `NULL`. /// /// The queue is shared: @@ -1306,7 +1578,7 @@ type LockKey = Option<(usize, usize, usize)>; #[cfg(not(any(target_os = "macos", target_os = "ios")))] unsafe fn metal_context(_device: *mut c_void, _queue: *mut c_void) -> Result { Err(Failure( - EmberwaveStatus::NoGpu, + EmberwaveStatus::Unsupported, "Metal contexts are made on iPhones, iPads and Macs only".into(), )) } @@ -1326,19 +1598,21 @@ pub type EmberwaveVkGetInstanceProcAddr = Option< #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveVulkanInstance { - /// `vkGetInstanceProcAddr` of the loader the instance was made with. - pub get_instance_proc_addr: EmberwaveVkGetInstanceProcAddr, - /// The `VkInstance`. - pub instance: *mut c_void, + /// `sizeof(EmberwaveVulkanInstance)` (decision D69). + pub struct_size: u32, /// The `apiVersion` the instance was made with, such as /// `VK_API_VERSION_1_3`: at least the physical device's, up to 1.3, /// which wgpu uses. pub api_version: u32, + /// How many names `extensions` holds. + pub extension_count: u32, + /// `vkGetInstanceProcAddr` of the loader the instance was made with. + pub get_instance_proc_addr: EmberwaveVkGetInstanceProcAddr, + /// The `VkInstance`. + pub instance: *mut c_void, /// The instance extensions it was made with: `extension_count` names, or /// `NULL` for none. pub extensions: *const *const c_char, - /// How many names `extensions` holds. - pub extension_count: u32, } /// A Vulkan device of the caller's, and the queue of it that emberwave is to @@ -1346,8 +1620,10 @@ pub struct EmberwaveVulkanInstance { #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveVulkanDevice { + /// `sizeof(EmberwaveVulkanDevice)` (decision D69). + pub struct_size: u32, /// The instance the device was made with. - pub instance: EmberwaveVulkanInstance, + pub instance: *const EmberwaveVulkanInstance, /// The `VkPhysicalDevice` the device was made on. pub physical_device: *mut c_void, /// The `VkDevice`, made with the extensions and features that @@ -1371,9 +1647,11 @@ pub struct EmberwaveVulkanDevice { /// from any thread. `NULL`, with `unlock_queue` `NULL` too, if nothing /// else uses the queue during emberwave's calls. Every context on one /// queue takes the same `lock_queue`, `unlock_queue` and `user`, which - /// stay valid until everything made on those contexts is destroyed. + /// stay valid until everything made on those contexts is destroyed. It + /// must not call emberwave. pub lock_queue: Option, - /// Frees the caller's lock of the queue that `lock_queue` took. + /// Frees the caller's lock of the queue that `lock_queue` took. It must + /// not call emberwave. pub unlock_queue: Option, /// Passed to `lock_queue` and `unlock_queue`. pub user: *mut c_void, @@ -1386,6 +1664,8 @@ pub struct EmberwaveVulkanDevice { #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveVulkanImage { + /// `sizeof(EmberwaveVulkanImage)` (decision D69). + pub struct_size: u32, /// The `VkImage`. pub image: u64, /// Its `VkFormat`: `VK_FORMAT_B8G8R8A8_UNORM` or @@ -1405,8 +1685,8 @@ pub struct EmberwaveVulkanImage { /// into it or encode from it has completed: from then on the caller may /// destroy the image and free its memory. From any thread, perhaps /// during another call of emberwave's on the context, or in the destroy - /// of its last decoder, encoder or context. `NULL` if the caller knows - /// that moment otherwise. + /// of its last decoder, encoder or context, and so it must not call + /// emberwave. `NULL` if the caller knows that moment otherwise. pub release: Option, /// Passed to `release`. pub user: *mut c_void, @@ -1422,8 +1702,13 @@ pub struct EmberwaveVulkanImage { /// holds up every later submission to the queue, the caller's too, as the /// decode's waits at every stage. #[repr(C)] -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct EmberwaveVulkanSync { + /// `sizeof(EmberwaveVulkanSync)` (decision D69). + pub struct_size: u32, + /// `EMBERWAVE_SYNC_WAIT_TIMELINE` and `EMBERWAVE_SYNC_SIGNAL_TIMELINE` + /// combined with `|` for timeline semaphores, or 0 for binary ones. + pub flags: u32, /// A `VkSemaphore` the decode's GPU work waits for, such as one the /// caller signals once its reads of the textures are done, or 0 for /// none. @@ -1435,9 +1720,6 @@ pub struct EmberwaveVulkanSync { pub signal_semaphore: u64, /// For a timeline `signal_semaphore`, the value to signal it to. pub signal_value: u64, - /// `EMBERWAVE_SYNC_WAIT_TIMELINE` and `EMBERWAVE_SYNC_SIGNAL_TIMELINE` - /// combined with `|` for timeline semaphores, or 0 for binary ones. - pub flags: u32, } /// `wait_semaphore` of an `EmberwaveVulkanSync` is a timeline semaphore, to @@ -1472,6 +1754,8 @@ pub struct EmberwaveD3D12Fence { #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct EmberwaveD3D12Sync { + /// `sizeof(EmberwaveD3D12Sync)` (decision D69). + pub struct_size: u32, /// A fence the call's GPU work waits for, such as one the caller signals /// once its own work on the textures is done, or `NULL`. pub wait_fence: *const EmberwaveD3D12Fence, @@ -1484,17 +1768,6 @@ pub struct EmberwaveD3D12Sync { pub signal_value: u64, } -impl Default for EmberwaveD3D12Sync { - fn default() -> Self { - Self { - wait_fence: std::ptr::null(), - wait_value: 0, - signal_fence: std::ptr::null(), - signal_value: 0, - } - } -} - /// What emberwave needs a Vulkan device of the caller's to have enabled: /// device extensions and features, from /// `emberwave_vulkan_requirements_create`. @@ -1517,43 +1790,46 @@ pub struct EmberwaveVulkanRequirements { /// `VK_KHR_get_physical_device_properties2` it learns less of a device and /// uses fewer of its features. `VK_KHR_surface` goes with the /// `VK_KHR_swapchain` that `emberwave_vulkan_requirements_create` lists on -/// devices that have it. With `names` `NULL`, stores their number in -/// `*count`; otherwise writes them to `names`, which has room for `*count`, -/// or gives `EMBERWAVE_STATUS_INVALID_ARGUMENT` with the number needed in -/// `*count` if that is too few. The names stay valid as long as the process. -/// Only where wgpu runs on Vulkan; elsewhere `EMBERWAVE_STATUS_NO_GPU`. +/// devices that have it. Stores their number in `*count`, and with `names` +/// not `NULL` writes them there too, which has room for `capacity` names; if +/// that is too few, nothing is written to `names` and the result is +/// `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, as for `emberwave_frame_packets`. +/// The counts are `uint32_t`, as Vulkan's are. The names stay valid as long +/// as the process. Only where wgpu runs on Vulkan; elsewhere +/// `EMBERWAVE_STATUS_UNSUPPORTED`. /// /// # Safety /// /// `get_instance_proc_addr` is `NULL` or a loader's `vkGetInstanceProcAddr`, -/// `count` points to a `uint32_t`, and `names` is `NULL` or points to -/// `*count` pointers that may be written. +/// `names` is `NULL` or points to `capacity` pointers that may be written, +/// and `count` points to a `uint32_t`. #[unsafe(no_mangle)] pub unsafe extern "C" fn emberwave_vulkan_instance_extensions( get_instance_proc_addr: EmberwaveVkGetInstanceProcAddr, names: *mut *const c_char, + capacity: u32, count: *mut u32, ) -> EmberwaveStatus { run(|| { - if count.is_null() { - return Err(Failure::argument("count is NULL")); - } - let wanted = vulkan_instance_extensions(get_instance_proc_addr)?; - // SAFETY: as the caller promises. - let room = unsafe { count.read() }; // SAFETY: as the caller promises. + unsafe { output(count, "count", 0) }?; + let wanted = vulkan_instance_extensions(get_instance_proc_addr)?; + // SAFETY: checked by output() above. unsafe { count.write(wanted.len() as u32) }; if names.is_null() { return Ok(()); } - if (room as usize) < wanted.len() { - return Err(Failure::argument(format!( - "names has room for {room} names, not the {} needed", - wanted.len() - ))); + if (capacity as usize) < wanted.len() { + return Err(Failure( + EmberwaveStatus::BufferTooSmall, + format!( + "capacity is {capacity}, less than the {} names", + wanted.len() + ), + )); } for (at, name) in wanted.iter().enumerate() { - // SAFETY: room for `room` pointers, as the caller promises. + // SAFETY: `at` is below `capacity`, as the caller promises room for. unsafe { names.add(at).write(name.as_ptr()) }; } Ok(()) @@ -1589,8 +1865,8 @@ fn vulkan_instance_extensions( /// `emberwave_vulkan_requirements_features` gives. Make the device with /// them, and with whatever else the caller needs. Only where wgpu runs on /// Vulkan, on Linux, Android and 64-bit Windows; elsewhere -/// `EMBERWAVE_STATUS_NO_GPU`. -/// `EMBERWAVE_STATUS_NO_GPU` too for a physical device wgpu cannot use. +/// `EMBERWAVE_STATUS_UNSUPPORTED`. `EMBERWAVE_STATUS_NO_GPU` for a physical +/// device wgpu cannot use. /// /// # Safety /// @@ -1608,11 +1884,9 @@ pub unsafe extern "C" fn emberwave_vulkan_requirements_create( // SAFETY: as the caller promises. unsafe { output(requirements, "requirements", std::ptr::null_mut()) }?; // SAFETY: as the caller promises. - let Some(instance) = (unsafe { instance.as_ref() }) else { - return Err(Failure::argument("instance is NULL")); - }; + let instance = unsafe { read_given(instance, "instance") }?; // SAFETY: as the caller promises. - let made = unsafe { vulkan_requirements(instance, physical_device) }?; + let made = unsafe { vulkan_requirements(&instance, physical_device) }?; // SAFETY: checked by the first output() above. unsafe { requirements.write(Box::into_raw(Box::new(made))) }; Ok(()) @@ -1690,7 +1964,7 @@ pub unsafe extern "C" fn emberwave_vulkan_requirements_destroy( /// work on the caller's queue (decision D61). The device keeps belonging to /// the caller, who destroys it after the context and everything made on it. /// Only where wgpu runs on Vulkan, on Linux, Android and 64-bit Windows; -/// elsewhere `EMBERWAVE_STATUS_NO_GPU`. +/// elsewhere `EMBERWAVE_STATUS_UNSUPPORTED`. /// /// The device must have been made with the extensions and the features of /// `emberwave_vulkan_requirements_create`. A lacking extension is @@ -1716,14 +1990,16 @@ pub unsafe extern "C" fn emberwave_vulkan_requirements_destroy( /// - The decodes that wait return once the GPU is done. Those that do not /// wait are `emberwave_decoder_submit_texture_vulkan` and /// `emberwave_decoder_submit_plane_textures_vulkan`, which order the work -/// with semaphores; `emberwave_decoder_submit_texture` and -/// `emberwave_decoder_submit_plane_textures` are for Metal contexts only. +/// with semaphores; `emberwave_decoder_submit_texture_metal` and +/// `emberwave_decoder_submit_plane_textures_metal` are for Metal contexts only. /// /// # Safety /// /// `device` is `NULL` or points to an `EmberwaveVulkanDevice` that describes /// a live device made as above, with a queue of that family and index, and -/// callbacks that may be called from any thread. `context` points to where +/// callbacks that may be called from any thread; its `instance` is `NULL` +/// or points to an `EmberwaveVulkanInstance` with its `struct_size` set. +/// `context` points to where /// the new context is stored; it is set to `NULL` when the call fails. #[unsafe(no_mangle)] pub unsafe extern "C" fn emberwave_context_create_vulkan( @@ -1734,11 +2010,9 @@ pub unsafe extern "C" fn emberwave_context_create_vulkan( // SAFETY: as the caller promises. unsafe { output(context, "context", std::ptr::null_mut()) }?; // SAFETY: as the caller promises. - let Some(device) = (unsafe { device.as_ref() }) else { - return Err(Failure::argument("device is NULL")); - }; + let device = unsafe { read_given(device, "device") }?; // SAFETY: as the caller promises. - let created = unsafe { vulkan_context(device) }?; + let created = unsafe { vulkan_context(&device) }?; // SAFETY: checked by the first output() above. unsafe { context.write(EmberwaveContext::boxed(created, false, true, false)) }; Ok(()) @@ -1879,12 +2153,10 @@ pub unsafe extern "C" fn emberwave_texture_from_vulkan_source( let encoder = unsafe { encoder.as_ref() }.ok_or_else(|| Failure::argument("encoder is NULL"))?; // SAFETY: as the caller promises. - let Some(image) = (unsafe { image.as_ref() }) else { - return Err(Failure::argument("image is NULL")); - }; + let image = unsafe { read_given(image, "image") }?; require_vulkan(encoder.vulkan, "encoder")?; // SAFETY: as the caller promises. - let wrapped = unsafe { vulkan_import(&Wrapping::encoder(encoder, holds), image) }?; + let wrapped = unsafe { vulkan_import(&Wrapping::encoder(encoder, holds), &image) }?; // SAFETY: checked by the first output() above. unsafe { texture.write(Box::into_raw(Box::new(wrapped))) }; Ok(()) @@ -1914,12 +2186,10 @@ unsafe fn texture_from_vulkan( // SAFETY: as the caller promises. let decoder = unsafe { live(decoder) }?; // SAFETY: as the caller promises. - let Some(image) = (unsafe { image.as_ref() }) else { - return Err(Failure::argument("image is NULL")); - }; + let image = unsafe { read_given(image, "image") }?; require_vulkan(decoder.vulkan, "decoder")?; // SAFETY: as the caller promises. - let wrapped = unsafe { vulkan_import(&Wrapping::decoder(decoder, plane), image) }?; + let wrapped = unsafe { vulkan_import(&Wrapping::decoder(decoder, plane), &image) }?; // SAFETY: checked by the first output() above. unsafe { texture.write(Box::into_raw(Box::new(wrapped))) }; Ok(()) @@ -2528,7 +2798,7 @@ unsafe fn d3d12_fences( textures: &[&EmberwaveTexture], ) -> Result, Failure> { // SAFETY: as the caller promises. - let sync = unsafe { sync.as_ref() }.copied().unwrap_or_default(); + let sync = unsafe { read_struct(sync, "sync") }?.unwrap_or_default(); // SAFETY: as the caller promises. let (wait, signal) = unsafe { (sync.wait_fence.as_ref(), sync.signal_fence.as_ref()) }; for (fence, name) in [(wait, "wait_fence"), (signal, "signal_fence")] { @@ -2628,7 +2898,7 @@ unsafe fn d3d12_import( #[cfg(not(windows))] fn d3d12_elsewhere() -> Failure { Failure( - EmberwaveStatus::NoGpu, + EmberwaveStatus::Unsupported, "Direct3D 12 exists on Windows only".into(), ) } @@ -2640,7 +2910,7 @@ fn d3d12_elsewhere() -> Failure { /// `sync` is `NULL` or points to an `EmberwaveVulkanSync`. unsafe fn vulkan_sync(sync: *const EmberwaveVulkanSync) -> Result { // SAFETY: as the caller promises. - let sync = unsafe { sync.as_ref() }.copied().unwrap_or_default(); + let sync = unsafe { read_struct(sync, "sync") }?.unwrap_or_default(); let known = EMBERWAVE_SYNC_WAIT_TIMELINE | EMBERWAVE_SYNC_SIGNAL_TIMELINE; if sync.flags & !known != 0 { return Err(Failure::argument(format!( @@ -2794,7 +3064,7 @@ unsafe fn vulkan_context(_device: &EmberwaveVulkanDevice) -> Result Failure { Failure( - EmberwaveStatus::NoGpu, + EmberwaveStatus::Unsupported, "Vulkan contexts are made on Linux, Android and 64-bit Windows only".into(), ) } @@ -2819,7 +3089,7 @@ mod vulkan { use super::{ EmberwaveStatus, EmberwaveTexture, EmberwaveVkGetInstanceProcAddr, EmberwaveVulkanDevice, EmberwaveVulkanImage, EmberwaveVulkanInstance, EmberwaveVulkanRequirements, - EmberwaveVulkanSync, Failure, Holds, Wrapping, queue_turns, + EmberwaveVulkanSync, Failure, Holds, Wrapping, queue_turns, read_given, }; /// What keeps the chain of an `EmberwaveVulkanRequirements` alive. @@ -3069,7 +3339,9 @@ mod vulkan { ) }?; // SAFETY: as the caller promises. - let adapter = unsafe { adapter(&device.instance, device.physical_device) }?; + let instance = unsafe { read_given(device.instance, "device.instance") }?; + // SAFETY: as the caller promises. + let adapter = unsafe { adapter(&instance, device.physical_device) }?; let descriptor = GpuContext::device_descriptor(&adapter, &GpuOptions::default()); let (open, turns) = { // SAFETY: only read. @@ -3857,6 +4129,7 @@ pub unsafe extern "C" fn emberwave_decoder_info( let layout = unsafe { live(decoder) }?.decoder.layout(); let (chroma_width, chroma_height) = layout.plane_size(Component::Cb); let described = EmberwaveFrameInfo { + struct_size: std::mem::size_of::() as u32, width: layout.width(), height: layout.height(), chroma: chroma_code(layout.chroma()), @@ -3864,7 +4137,7 @@ pub unsafe extern "C" fn emberwave_decoder_info( chroma_height, }; // SAFETY: as the caller promises. - unsafe { output(info, "info", described) } + unsafe { write_struct(info, "info", described) } }) } @@ -4017,9 +4290,9 @@ pub unsafe extern "C" fn emberwave_decoder_clear(decoder: *mut EmberwaveDecoder) /// # Safety /// /// `decoder` is a live decoder that no other thread is using. Each plane's -/// `data` points to `stride * (height - 1) + width` bytes that may be -/// written, with the sizes of `emberwave_decoder_info`, and the planes do -/// not overlap. +/// `data` points to `size` bytes that may be written, `size` being at least +/// `stride * (height - 1) + width` for the sizes of +/// `emberwave_decoder_info`, and the planes do not overlap. #[unsafe(no_mangle)] pub unsafe extern "C" fn emberwave_decoder_decode_yuv( decoder: *mut EmberwaveDecoder, @@ -4077,7 +4350,8 @@ pub unsafe extern "C" fn emberwave_decoder_decode_yuv( /// /// `decoder` is a live decoder that no other thread is using, `color` is /// `NULL` or points to an `EmberwaveColor`, and `rgb.data` points to -/// `stride * (height - 1) + 3 * width` bytes that may be written. +/// `rgb.size` bytes that may be written, `rgb.size` being at least +/// `stride * (height - 1) + 3 * width`. #[unsafe(no_mangle)] pub unsafe extern "C" fn emberwave_decoder_decode_rgb( decoder: *mut EmberwaveDecoder, @@ -4196,7 +4470,7 @@ pub unsafe extern "C" fn emberwave_texture_create_plane( /// `emberwave_texture_create`'s: a texture of a `CVPixelBuffer` from a /// `CVMetalTextureCache`, to hand to an `AVSampleBufferDisplayLayer`, one the /// caller made, or a drawable (decision D44). Only on iPhones, iPads and -/// Macs; elsewhere `EMBERWAVE_STATUS_NO_GPU` for arguments that are not +/// Macs; elsewhere `EMBERWAVE_STATUS_UNSUPPORTED` for arguments that are not /// `NULL`. /// /// The texture is on the device of the decoder's context @@ -4299,7 +4573,7 @@ pub unsafe extern "C" fn emberwave_texture_from_metal_plane( /// of an NV12 pixel buffer holds them, or `EMBERWAVE_PLANE_RGB` for the /// whole picture in `MTLPixelFormatBGRA8Unorm` or `MTLPixelFormatRGBA8Unorm`, /// as a BGRA pixel buffer holds it (decision D65). Only on iPhones, iPads and -/// Macs; elsewhere `EMBERWAVE_STATUS_NO_GPU` for arguments that are not +/// Macs; elsewhere `EMBERWAVE_STATUS_UNSUPPORTED` for arguments that are not /// `NULL`. /// /// The texture is on the device of the encoder's context @@ -4559,7 +4833,7 @@ unsafe fn metal_import( _metal: *mut c_void, ) -> Result { Err(Failure( - EmberwaveStatus::NoGpu, + EmberwaveStatus::Unsupported, "Metal textures exist on iPhones, iPads and Macs only".into(), )) } @@ -4723,7 +4997,7 @@ fn require_tracked(texture: &EmberwaveTexture, name: &str) -> Result<(), Failure /// /// As for `emberwave_decoder_decode_texture`. #[unsafe(no_mangle)] -pub unsafe extern "C" fn emberwave_decoder_submit_texture( +pub unsafe extern "C" fn emberwave_decoder_submit_texture_metal( decoder: *mut EmberwaveDecoder, color: *const EmberwaveColor, texture: *const EmberwaveTexture, @@ -4750,7 +5024,7 @@ pub unsafe extern "C" fn emberwave_decoder_submit_texture( /// Decodes the frame received so far into the plane textures `y`, `cb` and /// `cr`, as `emberwave_decoder_decode_plane_textures` does, but returns once -/// the work is on the queue, as `emberwave_decoder_submit_texture` does: only +/// the work is on the queue, as `emberwave_decoder_submit_texture_metal` does: only /// for a decoder whose context was made by `emberwave_context_create_metal`, /// and textures whose hazards Metal tracks. Vulkan and Direct3D contexts have /// `emberwave_decoder_submit_plane_textures_vulkan` and @@ -4760,7 +5034,7 @@ pub unsafe extern "C" fn emberwave_decoder_submit_texture( /// /// As for `emberwave_decoder_decode_plane_textures`. #[unsafe(no_mangle)] -pub unsafe extern "C" fn emberwave_decoder_submit_plane_textures( +pub unsafe extern "C" fn emberwave_decoder_submit_plane_textures_metal( decoder: *mut EmberwaveDecoder, y: *const EmberwaveTexture, cb: *const EmberwaveTexture, @@ -5038,8 +5312,8 @@ pub unsafe extern "C" fn emberwave_encoder_create( }; let chroma = chroma_subsampling(chroma)?; // SAFETY: as the caller promises. - let signalling = match unsafe { signalling.as_ref() } { - Some(value) => self::signalling(value)?, + let signalling = match unsafe { read_struct(signalling, "signalling") }? { + Some(value) => self::signalling(&value)?, None => Signalling::default(), }; let created = Encoder::new(&context.context, width, height, chroma, signalling) @@ -5093,6 +5367,7 @@ pub unsafe extern "C" fn emberwave_encoder_info( let layout = unsafe { live_encoder(encoder) }?.encoder.layout(); let (chroma_width, chroma_height) = layout.plane_size(Component::Cb); let described = EmberwaveFrameInfo { + struct_size: std::mem::size_of::() as u32, width: layout.width(), height: layout.height(), chroma: chroma_code(layout.chroma()), @@ -5100,7 +5375,7 @@ pub unsafe extern "C" fn emberwave_encoder_info( chroma_height, }; // SAFETY: as the caller promises. - unsafe { output(info, "info", described) } + unsafe { write_struct(info, "info", described) } }) } @@ -5110,7 +5385,7 @@ pub unsafe extern "C" fn emberwave_encoder_info( /// decoder. The planes have the sizes of `emberwave_encoder_info`, each row /// `stride` bytes from the next, and the bytes between the rows are not /// read. A budget too small for even the smallest frame is -/// `EMBERWAVE_STATUS_INVALID_ARGUMENT` with the smallest frame's size in +/// `EMBERWAVE_STATUS_BUFFER_TOO_SMALL` with the smallest frame's size in /// `*size`; nothing is encoded then, and the next frame takes the sequence /// number this one would have had. Waits for the GPU. /// @@ -5121,7 +5396,8 @@ pub unsafe extern "C" fn emberwave_encoder_info( /// # Safety /// /// `encoder` is a live encoder that no other thread is using. Each plane's -/// `data` points to `stride * (height - 1) + width` readable bytes. `frame` +/// `data` points to `size` readable bytes, `size` being at least +/// `stride * (height - 1) + width`. `frame` /// points to `budget` bytes that may be written, and `size` to a `size_t`; /// neither overlaps the planes or the other. #[unsafe(no_mangle)] @@ -5178,8 +5454,9 @@ pub unsafe extern "C" fn emberwave_encoder_encode( /// # Safety /// /// `encoder` is a live encoder that no other thread is using. `rgb.data` -/// points to `rgb.stride * (height - 1) + 4 * width` readable bytes, at the -/// size `emberwave_encoder_info` gives. `frame` points to `budget` bytes that +/// points to `rgb.size` readable bytes, `rgb.size` being at least +/// `rgb.stride * (height - 1) + 4 * width` at the size +/// `emberwave_encoder_info` gives. `frame` points to `budget` bytes that /// may be written, and `size` to a `size_t`; neither overlaps the picture or /// the other. #[unsafe(no_mangle)] @@ -5238,9 +5515,10 @@ fn rgb_order(format: u32) -> Result { /// `emberwave_texture_from_vulkan_source`, or textures that decodes put /// planes or pictures into. Others give `EMBERWAVE_STATUS_INVALID_ARGUMENT` /// before any work on the GPU, textures shared from Direct3D included, which -/// take `emberwave_encoder_encode_textures_d3d12`. A budget too small gives it too, as for -/// `emberwave_encoder_encode`, once the GPU has encoded the picture: no -/// frame then, and the sequence number does not move. +/// take `emberwave_encoder_encode_textures_d3d12`. A budget too small gives +/// `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`, as for `emberwave_encoder_encode`, +/// once the GPU has encoded the picture: no frame then, and the sequence +/// number does not move. /// /// Waits for the GPU: when it returns, the GPU has read the textures, and /// the caller may write them again or hand them back to a capture's pool. @@ -5419,7 +5697,7 @@ unsafe fn source_picture<'a>( /// boundaries of its blocks, and stores how many in `*count`. With `packets` /// not `NULL`, also where each starts in the frame and its size, in order; /// with fewer than `*count` places there (`capacity`), nothing is written to -/// `packets` and the result is `EMBERWAVE_STATUS_INVALID_ARGUMENT`. The first +/// `packets` and the result is `EMBERWAVE_STATUS_BUFFER_TOO_SMALL`. The first /// packet starts with the start of frame header. A block larger than /// `max_packet_bytes` gets a packet of its own, as does the rest of a frame /// from a malformed block on, so a packet can be larger. A decoder takes the @@ -5465,10 +5743,13 @@ pub unsafe extern "C" fn emberwave_frame_packets( return Ok(()); } if capacity < described.len() { - return Err(Failure::argument(format!( - "capacity is {capacity}, less than the {} packets of the frame", - described.len() - ))); + return Err(Failure( + EmberwaveStatus::BufferTooSmall, + format!( + "capacity is {capacity}, less than the {} packets of the frame", + described.len() + ), + )); } for (i, packet) in described.into_iter().enumerate() { // SAFETY: i is below capacity, which the caller promises. @@ -5482,15 +5763,21 @@ pub unsafe extern "C" fn emberwave_frame_packets( mod tests { use super::*; - fn assert_send() {} - fn assert_sync() {} - + /// What programs built with the header of 0.17 pass as `struct_size`, on + /// 64-bit systems: these sizes may never grow (decision D69). #[test] - fn handles_may_cross_threads_as_the_header_says() { - assert_send::(); - assert_sync::(); - assert_send::(); - assert_send::(); + #[cfg(target_pointer_width = "64")] + fn the_first_sizes_of_the_structs_stay() { + assert_eq!(EmberwaveContextOptions::FIRST_SIZE, 12); + assert_eq!(EmberwaveStreamInfo::FIRST_SIZE, 36); + assert_eq!(EmberwaveFrameInfo::FIRST_SIZE, 24); + assert_eq!(EmberwaveColor::FIRST_SIZE, 16); + assert_eq!(EmberwaveSignalling::FIRST_SIZE, 24); + assert_eq!(EmberwaveVulkanInstance::FIRST_SIZE, 40); + assert_eq!(EmberwaveVulkanDevice::FIRST_SIZE, 80); + assert_eq!(EmberwaveVulkanImage::FIRST_SIZE, 48); + assert_eq!(EmberwaveVulkanSync::FIRST_SIZE, 40); + assert_eq!(EmberwaveD3D12Sync::FIRST_SIZE, 40); } #[test] @@ -5524,6 +5811,7 @@ mod tests { let plane = |bytes: &mut Vec, stride| EmberwavePlane { data: bytes.as_mut_ptr(), stride, + size: bytes.len(), }; // SAFETY: a live decoder, and planes as large as their strides say. let status = unsafe { @@ -5608,6 +5896,7 @@ mod tests { let source = |plane: &Vec, stride| EmberwaveSourcePlane { data: plane.as_ptr(), stride, + size: plane.len(), }; let mut frame = vec![0u8; 10_000]; let mut size = 0; diff --git a/crates/emberwave-ffi/tests/api.rs b/crates/emberwave-ffi/tests/api.rs index df19c79..9b45edd 100644 --- a/crates/emberwave-ffi/tests/api.rs +++ b/crates/emberwave-ffi/tests/api.rs @@ -102,6 +102,7 @@ impl Target { EmberwavePlane { data: self.bytes.as_mut_ptr(), stride: self.stride, + size: self.bytes.len(), } } @@ -218,10 +219,12 @@ fn bad_arguments_are_errors_with_a_message_not_crashes() { EmberwaveContextOptions { backends: 1 << 7, flags: 0, + ..Default::default() }, EmberwaveContextOptions { backends: EMBERWAVE_BACKEND_ANY, flags: 1 << 5, + ..Default::default() }, ] { assert_eq!( @@ -377,6 +380,7 @@ fn rgb_through_the_c_api_follows_the_stream_or_the_caller() { matrix: info.matrix, range: info.range, chroma_siting: info.chroma_siting, + ..Default::default() }; let expected = rust .decode_rgb(frame, &ColorParams::from_stream(&header.stream)) @@ -388,6 +392,7 @@ fn rgb_through_the_c_api_follows_the_stream_or_the_caller() { matrix: EMBERWAVE_MATRIX_BT601, range: EMBERWAVE_RANGE_FULL, chroma_siting: EMBERWAVE_SITING_LEFT, + ..Default::default() }; let expected = rust .decode_rgb( @@ -449,6 +454,7 @@ fn a_bad_target_is_refused_before_the_frame_is_used() { let missing = EmberwavePlane { data: ptr::null_mut(), stride: 96, + size: 0, }; assert_eq!( emberwave_decoder_decode_yuv(decoder, y.plane(), cb.plane(), missing), @@ -960,7 +966,7 @@ fn a_decode_without_a_wait_needs_the_callers_own_queue() { EmberwaveStatus::Ok ); assert_eq!( - emberwave_decoder_submit_texture(decoder, ptr::null(), picture), + emberwave_decoder_submit_texture_metal(decoder, ptr::null(), picture), EmberwaveStatus::InvalidArgument ); assert!( @@ -969,7 +975,7 @@ fn a_decode_without_a_wait_needs_the_callers_own_queue() { last_error() ); assert_eq!( - emberwave_decoder_submit_plane_textures(decoder, y, cb, cr), + emberwave_decoder_submit_plane_textures_metal(decoder, y, cb, cr), EmberwaveStatus::InvalidArgument ); assert!(emberwave_decoder_is_ready(decoder, 0)); @@ -1005,7 +1011,7 @@ fn a_metal_context_needs_a_device_and_a_queue() { let anything = std::ptr::from_mut(&mut marker).cast(); // SAFETY: never dereferenced off Apple systems. let status = unsafe { emberwave_context_create_metal(anything, anything, &mut context) }; - assert_eq!(status, EmberwaveStatus::NoGpu); + assert_eq!(status, EmberwaveStatus::Unsupported); assert!(context.is_null()); } } @@ -1028,7 +1034,7 @@ fn vulkan_contexts_need_their_handles_and_vulkan() { ); assert!(requirements.is_null()); assert_eq!( - emberwave_vulkan_instance_extensions(None, ptr::null_mut(), ptr::null_mut()), + emberwave_vulkan_instance_extensions(None, ptr::null_mut(), 0, ptr::null_mut()), EmberwaveStatus::InvalidArgument ); assert!(emberwave_vulkan_requirements_extensions(ptr::null(), &mut count).is_null()); @@ -1057,9 +1063,10 @@ fn vulkan_contexts_need_their_handles_and_vulkan() { api_version: 0, extensions: ptr::null(), extension_count: 0, + ..Default::default() }; let device = EmberwaveVulkanDevice { - instance, + instance: &instance, physical_device: anything, device: anything, extensions: ptr::null(), @@ -1069,22 +1076,23 @@ fn vulkan_contexts_need_their_handles_and_vulkan() { lock_queue: None, unlock_queue: None, user: ptr::null_mut(), + ..Default::default() }; // SAFETY: never dereferenced where there is no Vulkan. unsafe { assert_eq!( emberwave_vulkan_requirements_create(&instance, anything, &mut requirements), - EmberwaveStatus::NoGpu + EmberwaveStatus::Unsupported ); assert!(requirements.is_null()); assert_eq!( emberwave_context_create_vulkan(&device, &mut context), - EmberwaveStatus::NoGpu + EmberwaveStatus::Unsupported ); assert!(context.is_null()); assert_eq!( - emberwave_vulkan_instance_extensions(Some(nothing), ptr::null_mut(), &mut count), - EmberwaveStatus::NoGpu + emberwave_vulkan_instance_extensions(Some(nothing), ptr::null_mut(), 0, &mut count), + EmberwaveStatus::Unsupported ); } } @@ -1096,7 +1104,12 @@ fn direct3d_calls_need_a_context_of_their_own() { let mut made = ptr::null_mut(); // SAFETY: a pointer to store the context in. let status = unsafe { emberwave_context_create_d3d12(0, 0, &mut made) }; - assert_eq!(status, EmberwaveStatus::NoGpu, "{}", last_error()); + let expected = if cfg!(windows) { + EmberwaveStatus::NoGpu + } else { + EmberwaveStatus::Unsupported + }; + assert_eq!(status, expected, "{}", last_error()); assert!(made.is_null()); // A context of emberwave_context_create takes no shared handle nor @@ -1152,7 +1165,7 @@ fn metal_textures_of_the_callers_own_need_metal() { let expected = if cfg!(any(target_os = "macos", target_os = "ios")) { EmberwaveStatus::InvalidArgument } else { - EmberwaveStatus::NoGpu + EmberwaveStatus::Unsupported }; assert_eq!( emberwave_texture_from_metal(decoder, ptr::null_mut(), &mut texture), @@ -1169,3 +1182,133 @@ fn metal_textures_of_the_callers_own_need_metal() { emberwave_context_destroy(context); } } + +#[test] +fn the_header_and_the_library_have_one_version() { + assert_eq!(emberwave_version_number(), EMBERWAVE_VERSION); + // SAFETY: a static string. + let text = unsafe { CStr::from_ptr(emberwave_version()) } + .to_str() + .unwrap(); + assert_eq!( + text, + format!("{EMBERWAVE_VERSION_MAJOR}.{EMBERWAVE_VERSION_MINOR}.{EMBERWAVE_VERSION_PATCH}") + ); +} + +#[test] +fn structs_carry_their_size_and_planes_theirs() { + let bytes = testdata("gradient_200x120_420.pwv1"); + let size = std::mem::size_of::(); + // SAFETY: valid buffers, and structs as large as their sizes say. + unsafe { + // A size below the struct's is refused, and nothing is written. + let mut info = EmberwaveStreamInfo { + struct_size: 4, + width: 7, + ..Default::default() + }; + assert_eq!( + emberwave_probe_pwv1(bytes.as_ptr(), bytes.len(), &mut info), + EmberwaveStatus::InvalidArgument + ); + assert!( + last_error().contains("info.struct_size is 4"), + "{}", + last_error() + ); + assert_eq!(info.width, 7); + + // A caller built with a larger struct gets what this version knows, + // and the bytes past it stay as they were. + #[repr(C)] + struct Later { + info: EmberwaveStreamInfo, + added: [u32; 4], + } + let mut later = Later { + info: EmberwaveStreamInfo { + struct_size: std::mem::size_of::() as u32, + ..Default::default() + }, + added: [0x5a5a_5a5a; 4], + }; + assert_eq!( + emberwave_probe_pwv1(bytes.as_ptr(), bytes.len(), &mut later.info), + EmberwaveStatus::Ok, + "{}", + last_error() + ); + assert_eq!((later.info.width, later.info.height), (200, 120)); + assert_eq!( + later.info.struct_size as usize, + std::mem::size_of::() + ); + assert_eq!(later.added, [0x5a5a_5a5a; 4]); + assert!(size < std::mem::size_of::()); + + // An options struct of size 0, as a zeroed one is, is refused. + let mut context = ptr::null_mut(); + let zeroed = EmberwaveContextOptions { + struct_size: 0, + ..Default::default() + }; + assert_eq!( + emberwave_context_create(&zeroed, &mut context), + EmberwaveStatus::InvalidArgument + ); + assert!( + last_error().contains("options.struct_size is 0"), + "{}", + last_error() + ); + assert!(context.is_null()); + } + + // Planes too small for their rows: a decode's are refused as too small, + // and nothing is decoded. + let Some(context) = context() else { + return; + }; + let decoder = decoder_for(context, &bytes); + let frame = frames(&bytes)[0]; + let mut y = vec![0u8; 200 * 120]; + let mut cb = vec![0u8; 100 * 60]; + let mut cr = vec![0u8; 100 * 60]; + let plane = |bytes: &mut Vec, stride, size| EmberwavePlane { + data: bytes.as_mut_ptr(), + stride, + size, + }; + // SAFETY: a live decoder, and planes no larger than their buffers. + unsafe { + assert_eq!( + emberwave_decoder_push_packet(decoder, frame.as_ptr(), frame.len()), + EmberwaveStatus::Ok + ); + let short = plane(&mut cb, 100, 100 * 60 - 1); + assert_eq!( + emberwave_decoder_decode_yuv( + decoder, + plane(&mut y, 200, 200 * 120), + short, + plane(&mut cr, 100, 100 * 60) + ), + EmberwaveStatus::BufferTooSmall + ); + assert!(last_error().contains("cb.size is 5999"), "{}", last_error()); + assert_eq!( + emberwave_decoder_decode_yuv( + decoder, + plane(&mut y, 200, 200 * 120), + plane(&mut cb, 100, 100 * 60), + plane(&mut cr, 100, 100 * 60) + ), + EmberwaveStatus::Ok, + "{}", + last_error() + ); + emberwave_decoder_destroy(decoder); + emberwave_context_destroy(context); + } +} diff --git a/crates/emberwave-ffi/tests/d3d12.rs b/crates/emberwave-ffi/tests/d3d12.rs index f4b5862..b0839b8 100644 --- a/crates/emberwave-ffi/tests/d3d12.rs +++ b/crates/emberwave-ffi/tests/d3d12.rs @@ -336,6 +336,7 @@ fn expected( let target = EmberwavePlane { data: rgb.as_mut_ptr(), stride: width * 3, + size: rgb.len(), }; let status = emberwave_decoder_decode_rgb(decoder, ptr::null(), target); assert_eq!(status, EmberwaveStatus::Ok, "{}", last_error()); @@ -343,6 +344,7 @@ fn expected( let [y, cb, cr] = [0, 1, 2].map(|i| EmberwavePlane { data: planes[i].as_mut_ptr(), stride: sizes[i].0, + size: planes[i].len(), }); let status = emberwave_decoder_decode_yuv(decoder, y, cb, cr); assert_eq!(status, EmberwaveStatus::Ok, "{}", last_error()); @@ -396,6 +398,7 @@ fn a_client_reads_pictures_and_planes_after_the_fence() { wait_value: host.value, signal_fence: host.shared_fence, signal_value: host.value + 1, + ..Default::default() }; host.value += 1; // SAFETY: a live decoder and texture, and live fences. @@ -414,6 +417,7 @@ fn a_client_reads_pictures_and_planes_after_the_fence() { wait_value: host.value, signal_fence: host.shared_fence, signal_value: host.value + 1, + ..Default::default() }; host.value += 1; // SAFETY: a live decoder and textures, and live fences. @@ -446,6 +450,7 @@ fn a_client_reads_pictures_and_planes_after_the_fence() { wait_value: held, signal_fence: host.shared_fence, signal_value: held + 1, + ..Default::default() }; // SAFETY: a live decoder and texture, and live fences. let status = unsafe { @@ -620,6 +625,7 @@ fn a_hosts_shared_planes_are_encoded_after_the_fence() { let [y, cb, cr] = [0, 1, 2].map(|i| EmberwaveSourcePlane { data: picture[i].as_ptr(), stride: if i == 0 { size.0 } else { chroma.0 }, + size: picture[i].len(), }); // SAFETY: a live encoder, planes of its sizes, and a frame of the // length passed. @@ -649,6 +655,7 @@ fn a_hosts_shared_planes_are_encoded_after_the_fence() { wait_value: held, signal_fence: host.shared_fence, signal_value: held + 1, + ..Default::default() }, }; let thread = std::thread::spawn(move || { @@ -672,6 +679,7 @@ fn a_hosts_shared_planes_are_encoded_after_the_fence() { wait_value: written, signal_fence: host.shared_fence, signal_value: written + 1, + ..Default::default() }; host.value = written + 1; let planar = encode(as_planes, [sources[0], sources[1], sources[2]], &sync); @@ -681,6 +689,7 @@ fn a_hosts_shared_planes_are_encoded_after_the_fence() { wait_value: host.value, signal_fence: host.shared_fence, signal_value: host.value + 1, + ..Default::default() }; host.value += 1; let nv12 = encode(as_nv12, [sources[0], cbcr, ptr::null_mut()], &sync); @@ -786,6 +795,7 @@ fn a_hosts_shared_bgra_picture_is_encoded_as_rgb() { let picture = EmberwaveSourcePlane { data: bgra.as_ptr(), stride: 4 * size.0, + size: bgra.len(), }; // SAFETY: a live encoder, a picture of its size, and a frame of the // length passed. @@ -807,6 +817,7 @@ fn a_hosts_shared_bgra_picture_is_encoded_as_rgb() { wait_value: written, signal_fence: host.shared_fence, signal_value: written + 1, + ..Default::default() }; host.value = written + 1; let mut frame = vec![0u8; 9_000]; diff --git a/crates/emberwave-ffi/tests/encoder.rs b/crates/emberwave-ffi/tests/encoder.rs index fd1c236..b34eda1 100644 --- a/crates/emberwave-ffi/tests/encoder.rs +++ b/crates/emberwave-ffi/tests/encoder.rs @@ -114,6 +114,7 @@ fn source_planes( EmberwaveSourcePlane { data: storage[plane].as_ptr(), stride: width + padding, + size: storage[plane].len(), } }) } @@ -261,7 +262,7 @@ fn a_budget_too_small_gives_the_size_of_the_smallest_frame() { let mut storage = Vec::new(); let planes = source_planes(&layout, &source, 0, &mut storage); let (status, _, minimum) = encode_c(encoder, planes, 7); - assert_eq!(status, EmberwaveStatus::InvalidArgument); + assert_eq!(status, EmberwaveStatus::BufferTooSmall); assert!(last_error().contains("smallest frame"), "{}", last_error()); // The header and the blocks of the coarsest level, which every frame // carries, each as small as it gets: 12 bytes at least. @@ -309,7 +310,7 @@ fn every_start_of_frame_header_says_what_the_signalling_does() { chroma_siting: EMBERWAVE_SITING_CENTER, primaries: EMBERWAVE_PRIMARIES_SRGB, transfer: EMBERWAVE_TRANSFER_SRGB, - reserved: [0; 3], + ..Default::default() }; // NULL, the defaults spelt out, and each field away from its default on // its own, so that one field cannot stand in for another. @@ -421,7 +422,7 @@ fn bad_encoder_arguments_are_errors_with_a_message() { chroma_siting: EMBERWAVE_SITING_CENTER, primaries: EMBERWAVE_PRIMARIES_SRGB, transfer: EMBERWAVE_TRANSFER_SRGB, - reserved: [0; 3], + ..Default::default() }; let signalling_cases = [ ( @@ -440,10 +441,10 @@ fn bad_encoder_arguments_are_errors_with_a_message() { ), ( EmberwaveSignalling { - reserved: [0, 1, 0], + struct_size: 4, ..good }, - "reserved", + "signalling.struct_size is 4", ), ]; let create = |width, height, chroma, signalling: *const EmberwaveSignalling| { @@ -581,7 +582,7 @@ fn frames_split_into_the_packets_of_the_rust_function() { &mut count, ) }; - assert_eq!(status, EmberwaveStatus::InvalidArgument); + assert_eq!(status, EmberwaveStatus::BufferTooSmall); assert_eq!(count, needed); assert!(packets.iter().all(|p| *p == EmberwavePacket::default())); // Room to spare: the packets, and the rest untouched. @@ -666,6 +667,7 @@ fn the_c_decoder_shows_the_c_encoders_pictures() { .map(|(plane, bytes)| EmberwavePlane { data: bytes.as_mut_ptr(), stride: layout.plane_size(Component::ALL[plane]).0 as usize, + size: bytes.len(), }) .collect(); // SAFETY: a live decoder and planes of its sizes. @@ -796,6 +798,7 @@ fn planes_a_decode_left_in_textures_encode_as_the_same_planes_in_memory() { let [y, cb, cr] = [0, 1, 2].map(|plane| EmberwavePlane { data: decoded[plane].as_mut_ptr(), stride: layout.plane_size(Component::ALL[plane]).0 as usize, + size: decoded[plane].len(), }); emberwave_decoder_decode_yuv(decoder, y, cb, cr) }; @@ -822,7 +825,7 @@ fn planes_a_decode_left_in_textures_encode_as_the_same_planes_in_memory() { // A budget too small, as from CPU memory. let (status, _, size) = encode_textures_c(from_textures, textures.map(|t| t.cast_const()), 13); - assert_eq!(status, EmberwaveStatus::InvalidArgument); + assert_eq!(status, EmberwaveStatus::BufferTooSmall); assert!(size > 13, "the smallest frame's size, not {size}"); // SAFETY: live, destroyed once each. @@ -912,6 +915,7 @@ fn textures_an_encode_cannot_read_are_refused() { layout: 5, release: None, user: ptr::null_mut(), + ..Default::default() }; let mut texture = ptr::null_mut(); let status = @@ -1004,7 +1008,7 @@ fn pictures_in_rgb_encode_as_the_planes_they_convert_to() { chroma_siting: EMBERWAVE_SITING_LEFT, primaries: EMBERWAVE_PRIMARIES_BT2020, transfer: EMBERWAVE_TRANSFER_PQ, - reserved: [0; 3], + ..Default::default() }; let other_params = ColorParams { matrix: ColorMatrix::Bt2020, @@ -1036,6 +1040,7 @@ fn pictures_in_rgb_encode_as_the_planes_they_convert_to() { let rgb = EmberwaveSourcePlane { data: bytes.as_ptr(), stride, + size: bytes.len(), }; let (status, frame, _) = encode_rgb_c(from_rgb, rgb, format, 9_000); assert_eq!(status, EmberwaveStatus::Ok, "{}", last_error()); @@ -1092,6 +1097,7 @@ fn a_picture_a_decode_left_in_a_texture_encodes_as_the_same_picture_in_memory() let plane = EmberwavePlane { data: rgb.as_mut_ptr(), stride: 3 * w, + size: rgb.len(), }; emberwave_decoder_decode_rgb(decoder, ptr::null(), plane) }; @@ -1115,6 +1121,7 @@ fn a_picture_a_decode_left_in_a_texture_encodes_as_the_same_picture_in_memory() let picture = EmberwaveSourcePlane { data: rgba.as_ptr(), stride: 4 * w, + size: rgba.len(), }; let (status, expected, _) = encode_rgb_c(from_memory, picture, EMBERWAVE_TEXTURE_RGBA8, 9_000); @@ -1158,6 +1165,7 @@ fn rgb_pictures_an_encode_cannot_read_are_refused() { let good = EmberwaveSourcePlane { data: bytes.as_ptr(), stride: 256, + size: bytes.len(), }; refused( good, @@ -1168,6 +1176,7 @@ fn rgb_pictures_an_encode_cannot_read_are_refused() { EmberwaveSourcePlane { data: ptr::null(), stride: 256, + size: 0, }, EMBERWAVE_TEXTURE_BGRA8, "rgb.data is NULL", @@ -1176,6 +1185,7 @@ fn rgb_pictures_an_encode_cannot_read_are_refused() { EmberwaveSourcePlane { data: bytes.as_ptr(), stride: 255, + size: bytes.len(), }, EMBERWAVE_TEXTURE_BGRA8, "rgb.stride is 255, less than the 256 bytes of a row", @@ -1199,3 +1209,27 @@ fn rgb_pictures_an_encode_cannot_read_are_refused() { fn context_or_panic() -> *mut EmberwaveContext { context().expect("a second context where there is a first") } + +#[test] +fn source_planes_shorter_than_their_rows_are_refused() { + let Some(context) = context() else { + return; + }; + let layout = FrameLayout::new(200, 120, ChromaSubsampling::Yuv420).unwrap(); + let encoder = encoder_for(context, &layout, None); + let source = picture(&layout, 7); + let mut storage = Vec::new(); + let mut planes = source_planes(&layout, &source, 0, &mut storage); + planes[1].size -= 1; + let (status, _, _) = encode_c(encoder, planes, 20_000); + assert_eq!(status, EmberwaveStatus::InvalidArgument); + assert!(last_error().contains("cb.size"), "{}", last_error()); + planes[1].size += 1; + let (status, _, _) = encode_c(encoder, planes, 20_000); + assert_eq!(status, EmberwaveStatus::Ok, "{}", last_error()); + // SAFETY: live, destroyed once each. + unsafe { + emberwave_encoder_destroy(encoder); + emberwave_context_destroy(context); + } +} diff --git a/crates/emberwave-ffi/tests/metal.rs b/crates/emberwave-ffi/tests/metal.rs index 232a846..965dd0d 100644 --- a/crates/emberwave-ffi/tests/metal.rs +++ b/crates/emberwave-ffi/tests/metal.rs @@ -180,6 +180,7 @@ fn an_app_reads_the_picture_from_the_metal_texture() { let plane = EmberwavePlane { data: rgb.as_mut_ptr(), stride: width * 3, + size: rgb.len(), }; assert_eq!( emberwave_decoder_decode_rgb(decoder, ptr::null(), plane), @@ -293,9 +294,10 @@ fn read_planes( // The planes in CPU memory, then the same frame into textures. let mut planes = sizes.map(|(width, height)| vec![0u8; width * height]); let [y, cb, cr] = planes.each_mut().map(|plane| plane.as_mut_ptr()); - let target = |data, (width, _): (usize, usize)| EmberwavePlane { + let target = |data, (width, height): (usize, usize)| EmberwavePlane { data, stride: width, + size: width * height, }; assert_eq!( emberwave_decoder_push_packet(decoder, frame.as_ptr(), frame.len()), @@ -445,6 +447,7 @@ fn decode_on_the_app_queue( let target = EmberwavePlane { data: rgb.as_mut_ptr(), stride: width * 3, + size: rgb.len(), }; assert_eq!( emberwave_decoder_decode_rgb(decoder, ptr::null(), target), @@ -452,7 +455,11 @@ fn decode_on_the_app_queue( ); let mut planes = sizes.map(|(w, h)| vec![0u8; w * h]); let [y, cb, cr] = planes.each_mut().map(|plane| plane.as_mut_ptr()); - let plane = |data, (w, _): (usize, usize)| EmberwavePlane { data, stride: w }; + let plane = |data, (w, h): (usize, usize)| EmberwavePlane { + data, + stride: w, + size: w * h, + }; // The same frames again: forget their order. emberwave_decoder_clear(decoder); assert_eq!( @@ -509,7 +516,7 @@ fn decode_on_the_app_queue( EmberwaveStatus::Ok ); assert_eq!( - emberwave_decoder_submit_texture(decoder, ptr::null(), texture), + emberwave_decoder_submit_texture_metal(decoder, ptr::null(), texture), EmberwaveStatus::Ok, "{}", last_error() @@ -521,7 +528,9 @@ fn decode_on_the_app_queue( EmberwaveStatus::Ok ); assert_eq!( - emberwave_decoder_submit_plane_textures(decoder, planes[0], planes[1], planes[2]), + emberwave_decoder_submit_plane_textures_metal( + decoder, planes[0], planes[1], planes[2] + ), EmberwaveStatus::Ok, "{}", last_error() @@ -840,6 +849,7 @@ fn encode_on(context: *mut EmberwaveContext) { let plane = |data: &[u8], stride| EmberwaveSourcePlane { data: data.as_ptr(), stride, + size: data.len(), }; for _ in 0..20 { let mut size = 0; @@ -889,7 +899,7 @@ fn decode_on(context: *mut EmberwaveContext, frame: &[u8]) { let status = if round % 2 == 0 { emberwave_decoder_decode_texture(decoder, ptr::null(), texture) } else { - emberwave_decoder_submit_texture(decoder, ptr::null(), texture) + emberwave_decoder_submit_texture_metal(decoder, ptr::null(), texture) }; assert_eq!(status, EmberwaveStatus::Ok, "{}", last_error()); } @@ -1017,6 +1027,7 @@ fn into_app_textures( let target = EmberwavePlane { data: rgb.as_mut_ptr(), stride: width * 3, + size: rgb.len(), }; assert_eq!( emberwave_decoder_decode_rgb(decoder, ptr::null(), target), @@ -1024,7 +1035,11 @@ fn into_app_textures( ); let mut planes = sizes.map(|(w, h)| vec![0u8; w * h]); let [y, cb, cr] = planes.each_mut().map(|plane| plane.as_mut_ptr()); - let plane = |data, (w, _): (usize, usize)| EmberwavePlane { data, stride: w }; + let plane = |data, (w, h): (usize, usize)| EmberwavePlane { + data, + stride: w, + size: w * h, + }; push(frame); assert_eq!( emberwave_decoder_decode_yuv( @@ -1071,7 +1086,7 @@ fn into_app_textures( // Without a wait, read by the app's next command buffer. push(frames[1]); assert_eq!( - emberwave_decoder_submit_texture(decoder, ptr::null(), texture), + emberwave_decoder_submit_texture_metal(decoder, ptr::null(), texture), EmberwaveStatus::Ok, "{}", last_error() @@ -1148,7 +1163,7 @@ fn into_app_textures( } push(frames[1]); assert_eq!( - emberwave_decoder_submit_plane_textures( + emberwave_decoder_submit_plane_textures_metal( decoder, writable[0].1, writable[1].1, @@ -1192,7 +1207,7 @@ fn into_app_textures( last_error() ); assert_eq!( - emberwave_decoder_submit_plane_textures(decoder, planes[0].1, twice[0], twice[1]), + emberwave_decoder_submit_plane_textures_metal(decoder, planes[0].1, twice[0], twice[1]), EmberwaveStatus::InvalidArgument ); assert!( @@ -1225,7 +1240,7 @@ fn into_app_textures( ); assert!(read(device, &untracked) == expected[0].0, "untracked"); assert_eq!( - emberwave_decoder_submit_texture(decoder, ptr::null(), loose), + emberwave_decoder_submit_texture_metal(decoder, ptr::null(), loose), EmberwaveStatus::InvalidArgument ); assert!(last_error().contains("track"), "{}", last_error()); @@ -1378,6 +1393,7 @@ fn encoded_from_memory( let [y, cb, cr] = [0, 1, 2].map(|plane| EmberwaveSourcePlane { data: planes[plane].as_ptr(), stride: widths[plane], + size: planes[plane].len(), }); // SAFETY: a live encoder, planes of its sizes, and a frame of the length // passed. @@ -1650,6 +1666,7 @@ fn encoded_from_rgb( let rgb = EmberwaveSourcePlane { data: pixels.as_ptr(), stride: 4 * width, + size: pixels.len(), }; // SAFETY: a live encoder, a picture of its size, and a frame of the // length passed. diff --git a/crates/emberwave-ffi/tests/vulkan.rs b/crates/emberwave-ffi/tests/vulkan.rs index e0f135e..19e7c8b 100644 --- a/crates/emberwave-ffi/tests/vulkan.rs +++ b/crates/emberwave-ffi/tests/vulkan.rs @@ -134,6 +134,7 @@ impl Client { emberwave_vulkan_instance_extensions( get_instance_proc_addr, ptr::null_mut(), + 0, &mut count ), EmberwaveStatus::Ok, @@ -145,6 +146,7 @@ impl Client { emberwave_vulkan_instance_extensions( get_instance_proc_addr, names.as_mut_ptr(), + names.len() as u32, &mut count ), EmberwaveStatus::Ok, @@ -228,6 +230,7 @@ impl Client { api_version: vk::API_VERSION_1_3, extensions: extensions.as_ptr(), extension_count: extensions.len() as u32, + ..Default::default() }; let mut requirements = ptr::null_mut(); // SAFETY: a live instance and physical device of it, and a @@ -285,6 +288,7 @@ impl Client { api_version: vk::API_VERSION_1_3, extensions: self.extensions.as_ptr(), extension_count: self.extensions.len() as u32, + ..Default::default() } } @@ -361,8 +365,9 @@ impl Client { lock: Option<&QueueLock>, get_instance_proc_addr: EmberwaveVkGetInstanceProcAddr, ) -> Result<*mut EmberwaveContext, EmberwaveStatus> { + let instance = self.described_through(get_instance_proc_addr); let described = EmberwaveVulkanDevice { - instance: self.described_through(get_instance_proc_addr), + instance: &instance, physical_device: self.physical_device(), device: device.handle().as_raw() as usize as *mut c_void, extensions: names.as_ptr(), @@ -374,6 +379,7 @@ impl Client { user: lock.map_or(ptr::null_mut(), |lock| { ptr::from_ref(lock).cast_mut().cast() }), + ..Default::default() }; let mut context = ptr::null_mut(); // SAFETY: a live device made with emberwave's requirements, and a @@ -613,6 +619,7 @@ fn decoded(context: *mut EmberwaveContext, frames: &[Vec]) -> Vec> { let plane = |data: &mut [u8], stride| EmberwavePlane { data: data.as_mut_ptr(), stride, + size: data.len(), }; assert_eq!( emberwave_decoder_decode_yuv( @@ -659,6 +666,7 @@ fn encoded(context: *mut EmberwaveContext) -> Vec { let plane = |data: &[u8], stride| EmberwaveSourcePlane { data: data.as_ptr(), stride, + size: data.len(), }; let mut size = 0; assert_eq!( @@ -693,6 +701,7 @@ fn a_context_on_the_callers_device_works_as_one_of_emberwaves() { let options = EmberwaveContextOptions { backends: EMBERWAVE_BACKEND_VULKAN, flags: 0, + ..Default::default() }; // SAFETY: options and a pointer to store the context in. let status = unsafe { emberwave_context_create(&options, &mut own) }; @@ -766,6 +775,46 @@ fn contexts_on_one_queue_hold_the_callers_lock_around_their_submissions() { unsafe { device.destroy_device(None) }; } +#[test] +fn instance_extension_names_need_room_for_all() { + let Some(client) = Client::new() else { + return; + }; + let get_instance_proc_addr = proc_addr(&client.entry); + let mut count = 0; + // SAFETY: the loader's vkGetInstanceProcAddr, room for as many names as + // the capacity says, and a count to store. + unsafe { + assert_eq!( + emberwave_vulkan_instance_extensions( + get_instance_proc_addr, + ptr::null_mut(), + 0, + &mut count + ), + EmberwaveStatus::Ok, + "{}", + last_error() + ); + if count == 0 { + return; + } + let mut names = vec![ptr::null(); count as usize]; + // One short: nothing is written, and the count says how many. + assert_eq!( + emberwave_vulkan_instance_extensions( + get_instance_proc_addr, + names.as_mut_ptr(), + count - 1, + &mut count + ), + EmberwaveStatus::BufferTooSmall + ); + assert_eq!(count as usize, names.len()); + assert!(names.iter().all(|name| name.is_null())); + } +} + #[test] fn a_device_without_the_required_extensions_is_refused() { let Some(client) = Client::new() else { @@ -773,8 +822,9 @@ fn a_device_without_the_required_extensions_is_refused() { }; let (names, device) = client.device(); let mut context = ptr::null_mut(); + let instance = client.described(); let described = |extension_count: u32, lock: bool, unlock: bool| EmberwaveVulkanDevice { - instance: client.described(), + instance: &instance, physical_device: client.physical_device(), device: device.handle().as_raw() as usize as *mut c_void, extensions: names.as_ptr(), @@ -784,6 +834,7 @@ fn a_device_without_the_required_extensions_is_refused() { lock_queue: lock.then_some(QueueLock::lock as unsafe extern "C" fn(*mut c_void)), unlock_queue: unlock.then_some(QueueLock::unlock as unsafe extern "C" fn(*mut c_void)), user: ptr::null_mut(), + ..Default::default() }; // SAFETY: descriptions of a live device, and a pointer to store a // context in, which none of them makes. @@ -804,6 +855,34 @@ fn a_device_without_the_required_extensions_is_refused() { emberwave_context_create_vulkan(ptr::null(), &mut context), EmberwaveStatus::InvalidArgument ); + // The device's instance, NULL or too small for an instance. + let mut without = described(names.len() as u32, false, false); + without.instance = ptr::null(); + assert_eq!( + emberwave_context_create_vulkan(&without, &mut context), + EmberwaveStatus::InvalidArgument + ); + assert!( + last_error().contains("device.instance is NULL"), + "{}", + last_error() + ); + let short = EmberwaveVulkanInstance { + struct_size: 8, + ..instance + }; + let mut shortened = described(names.len() as u32, false, false); + shortened.instance = &short; + assert_eq!( + emberwave_context_create_vulkan(&shortened, &mut context), + EmberwaveStatus::InvalidArgument + ); + assert!( + last_error().contains("device.instance.struct_size is 8"), + "{}", + last_error() + ); + assert!(context.is_null()); let mut requirements = ptr::null_mut(); assert_eq!( emberwave_vulkan_requirements_create( @@ -1096,6 +1175,7 @@ fn wrapped( layout: vk::ImageLayout::UNDEFINED.as_raw() as u32, release: None, user: ptr::null_mut(), + ..Default::default() }, plane, ) @@ -1142,6 +1222,7 @@ fn expected(decoder: *mut EmberwaveDecoder, frame: &[u8]) -> (Vec, [Vec; let target = EmberwavePlane { data: rgb.as_mut_ptr(), stride: width * 3, + size: rgb.len(), }; assert_eq!( emberwave_decoder_decode_rgb(decoder, ptr::null(), target), @@ -1154,14 +1235,15 @@ fn expected(decoder: *mut EmberwaveDecoder, frame: &[u8]) -> (Vec, [Vec; ); let mut planes = [width * height, width * height / 4, width * height / 4].map(|size| vec![0u8; size]); + let sizes = planes.each_ref().map(|plane| plane.len()); let [y, cb, cr] = planes.each_mut().map(|plane| plane.as_mut_ptr()); - let plane = |data, stride| EmberwavePlane { data, stride }; + let plane = |data, stride, size| EmberwavePlane { data, stride, size }; assert_eq!( emberwave_decoder_decode_yuv( decoder, - plane(y, width), - plane(cb, width / 2), - plane(cr, width / 2) + plane(y, width, sizes[0]), + plane(cb, width / 2, sizes[1]), + plane(cr, width / 2, sizes[2]) ), EmberwaveStatus::Ok ); @@ -1419,6 +1501,7 @@ fn images_emberwave_cannot_fill_or_leave_readable_are_refused() { layout: vk::ImageLayout::GENERAL.as_raw() as u32, release: None, user: ptr::null_mut(), + ..Default::default() }; let mut texture = ptr::null_mut(); // SAFETY: a live decoder, and descriptions that are refused. @@ -1535,6 +1618,7 @@ fn expected_picture( let target = EmberwavePlane { data: rgb.as_mut_ptr(), stride: width * 3, + size: rgb.len(), }; assert_eq!( emberwave_decoder_decode_rgb(decoder, ptr::null(), target), @@ -1598,6 +1682,7 @@ fn semaphores_hold_the_decode_back_and_order_the_reads() { signal_semaphore: done.as_raw(), signal_value: 1, flags: EMBERWAVE_SYNC_WAIT_TIMELINE | EMBERWAVE_SYNC_SIGNAL_TIMELINE, + ..Default::default() }; assert_eq!(submit(Some(&sync)), EmberwaveStatus::Ok, "{}", last_error()); std::thread::sleep(std::time::Duration::from_millis(300)); @@ -1712,6 +1797,7 @@ fn a_wrapped_image_is_drawn_into_and_released() { layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL.as_raw() as u32, release: Some(count_release), user: ptr::null_mut(), + ..Default::default() }, None, ) @@ -2056,6 +2142,7 @@ fn source( layout: layout.as_raw() as u32, release: None, user: ptr::null_mut(), + ..Default::default() }; let mut texture = ptr::null_mut(); // SAFETY: a live encoder, an image made as described, and a pointer to @@ -2124,6 +2211,7 @@ fn encoded_from_memory( let [y, cb, cr] = [0, 1, 2].map(|plane| EmberwaveSourcePlane { data: picture[plane].as_ptr(), stride: widths[plane] as usize, + size: picture[plane].len(), }); // SAFETY: a live encoder, planes of its sizes, and a frame of the length // passed. @@ -2283,6 +2371,7 @@ fn encoded_from_bgra(encoder: *mut EmberwaveEncoder, bgra: &[u8], width: u32) -> let rgb = EmberwaveSourcePlane { data: bgra.as_ptr(), stride: 4 * width as usize, + size: bgra.len(), }; // SAFETY: a live encoder, a picture of its size, and a frame of the // length passed. @@ -2435,6 +2524,7 @@ fn semaphores_hold_the_encode_back_until_the_picture_is_written() { signal_semaphore: read.as_raw(), signal_value: 1, flags: EMBERWAVE_SYNC_WAIT_TIMELINE | EMBERWAVE_SYNC_SIGNAL_TIMELINE, + ..Default::default() }; go.send(()).expect("the signaller waits"); let frame = encoded_from_images(encoder, [y, cb, cr], Some(&sync));