diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 062c6b1..0d5b16d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,23 @@ jobs: # The README's figures are drawn by a script: they must be what it draws. - run: python3 -B scripts/readme_figures.py && git diff --exit-code docs/images + msrv: + name: oldest Rust (1.87) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + # Cargo.toml's rust-version, which the READMEs promise: a newer language + # feature or library function fails here, not on a user's toolchain. + - name: Install Rust 1.87 + run: | + rustup toolchain install 1.87 --profile minimal --no-self-update + rustup default 1.87 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + - run: cargo check --workspace --all-targets --locked + wasm: name: browser build (wasm32) runs-on: ubuntu-latest diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 104595c..13c137b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -26,7 +26,7 @@ Steps that upstream runs on the CPU stay on the CPU, and steps that upstream run |---|---| | `bitstream` | Pure Rust, GPU independent: headers, block layout, packet assembly, PWV1 files. Never panics on malformed input. This is the CPU stage. | | `cpu` | Dequantisation and inverse DWT as plain sequential Rust, following `bitstream.md` literally. A test oracle and the yardstick for shader stages, not a product feature. | -| `color` | The YCbCr to RGB conversion: matrices, range handling and chroma siting, with a CPU implementation that the shader is tested against. | +| `color` | The YCbCr to RGB conversion: matrices, range handling and chroma siting, with a CPU implementation that the shader is tested against. And the encoder's RGB to Y'CbCr conversion in integers (`rgb_to_ycbcr8`, `ForwardMatrix`, `RgbOrder`, D65), which `gather_rgb.wgsl` matches to the bit. | | `encode` | The encoder on the CPU, plain sequential Rust: forward DWT, quantisation, rate control and packing, the reference of the GPU encoder; and `packets`, which splits a frame at its blocks for a network. | | `gpu` | wgpu pipelines and WGSL shaders for the GPU stages (cargo feature `gpu`, on by default). | @@ -300,7 +300,7 @@ The gap to the default precision is the reference decoder's own FP16 storage. In ## GPU pipeline -`gpu::Decoder` decodes frames of one size and chroma format. All GPU memory is allocated when it is created and reused for every frame. The payload buffer is the one exception: it grows when a frame needs more room. The encoder's GPU pipeline, `gpu::Encoder`, is described under [Encoder](#on-the-gpu). +`gpu::Decoder` decodes frames of one size and chroma format. The memory every decode uses, the pyramid, the block offsets and the packed planes, is allocated when it is created and reused for every frame, and the payload buffer grows when a frame needs more room. What only some decodes need, the upload and read-back buffers, the RGB picture and concealment's copy of the coarsest level, is made by the first decode that needs it and then reused. The encoder's GPU pipeline, `gpu::Encoder`, is described under [Encoder](#on-the-gpu). ### Stages @@ -323,7 +323,7 @@ The packed planes are copied to a mappable buffer and returned as a `Frame`. Whe | block offsets (`array`) | word offset of every block, `u32::MAX` if missing | 49 KiB | | pyramid (`array`) | the four bands of every level of every component, back to back | 63.6 MiB | | packed planes (`array`) | 8-bit planes, four pixels per word, rows padded to words | 11.9 MiB | -| readback (`MAP_READ`) | copy of the packed planes for the CPU | 11.9 MiB | +| readback (`MAP_READ`) | copy of the packed planes for the CPU, created by the first decode that reads them back | 11.9 MiB | | RGBA words (`array`) | one word per pixel, created on the first RGB decode | 31.6 MiB | | RGB readback (`MAP_READ`) | copy of the RGBA words, likewise | 31.6 MiB | | last coarsest level (`array`) | the coarsest level's bands of every component as the last decode left them, copied before a decode that conceals; created on the first (D66) | 383 KiB | @@ -336,7 +336,7 @@ No GPU resource outlives a frame. The decoder's own buffers are created once (th ### Dequantisation -One workgroup per block index, found by a lookup in a table of the (at most 48) transmitted bands. Each invocation decodes the 8 values of one 4x2 subblock. If a block was not received, a value that comes through `workgroupUniformLoad` makes the whole workgroup write zeros and return, so the later barriers stay in uniform control flow. +One workgroup per block index, found by a lookup in a table of the (at most 48) transmitted bands. Each invocation decodes the 8 values of one 4x2 subblock. If a block was not received, a value that comes through `workgroupUniformLoad` makes the whole workgroup write zeros and return, so the later barriers stay in uniform control flow. In a frame drawn in part that the decoder conceals, the workgroup of a block of detail below the coarsest level returns without writing, and leaves the block as the last decode left it for `conceal.wgsl` (D66). 1. Sixteen invocations read the control word and scale byte of one 8x8 block each and compute its number of bit-plane bytes into workgroup memory. 2. After a barrier every invocation adds up the sizes before its own 8x8 block, and the sizes of the preceding subblocks of its block, to find its bit-planes, and decodes its magnitudes. @@ -421,7 +421,7 @@ The wall-clock time exceeds the sum of the stages, because submitting work and n ### Portability -The shaders, the decoder's and the encoder's, use only what WebGPU guarantees: no subgroup operations, no 8-bit or 16-bit storage, no `f16`, no immediates (push constants), no storage textures. One shader is the exception, on a path of its own: on Metal, where the adapter offers it, `idwt_planes.wgsl` stores the planes into `r8unorm` storage textures (D52), and the portable copies stay the fallback; likewise the frames written where the shaders read them on Apple silicon need `MAPPABLE_PRIMARY_BUFFERS` (D53). Neither is asked for under strict WebGPU. All buffers are plain storage or uniform buffers and every workgroup stays within 256 invocations and 16 KiB. The encoder reads pictures in textures as sampled `r8unorm` and `rg8unorm` textures with `textureGather` and a non-filtering sampler, which WebGPU has without any feature. The encoder's rate control adds with 32-bit integer atomics, which WGSL has without any feature. Two tests hold this to account: +The shaders, the decoder's and the encoder's, use only what WebGPU guarantees: no subgroup operations, no 8-bit or 16-bit storage, no `f16`, no immediates (push constants), no storage textures. One shader is the exception, on a path of its own: on Metal, where the adapter offers it, `idwt_planes.wgsl` stores the planes into `r8unorm` storage textures (D52), and the portable copies stay the fallback; likewise the frames written where the shaders read them on Apple silicon need `MAPPABLE_PRIMARY_BUFFERS` (D53). Neither is asked for under strict WebGPU. All buffers are plain storage or uniform buffers and every workgroup stays within 256 invocations and 16 KiB. The encoder reads pictures in textures as sampled `r8unorm` and `rg8unorm` textures with `textureGather` and a non-filtering sampler, and pictures in RGB as sampled `rgba8unorm` and `bgra8unorm` textures with `textureLoad` (D65), which WebGPU has without any feature. The encoder's rate control adds with 32-bit integer atomics, which WGSL has without any feature. Two tests hold this to account: - `tests/shaders.rs` validates the WGSL of every shader with naga using an empty capability set, without a GPU, and checks the entry points, the workgroup sizes and memory. - `tests/gpu.rs` and `tests/gpu_encoder.rs` create a device with `InstanceFlags::STRICT_WEBGPU_COMPLIANCE` and the default limits and decode a vector on it, and encode pictures on it. @@ -499,7 +499,7 @@ The page paces frames by the stream's frame rate, keeping the remainder of each Without a file the page plays a demo stream it makes itself (`crates/emberwave-web/src/demo.rs`, decision D46): 60 pictures of 960x540 at 30 frames a second, two seconds that loop, of a glowing wavelet whose crests travel and embers that rise from it. The page draws each picture and encodes it with the CPU encoder within 40 kB, one picture at a time, and shows its progress in between. It then plays the PWV1 file this makes like any other. In headless Chrome on an Apple M3 Max the 60 pictures took about 2 s. A file opened or dropped meanwhile, or a sample, takes over, and `?play=NAME` starts that sample instead of the demo. -The player also decodes a stream packet by packet, as a client of a live stream receives it (#109, D55). `Player.open_stream` sets up a stream of a known size without a file, `push_packet` takes a packet, a start of frame header with blocks or blocks alone, in any order, `is_ready` says whether the frame can be drawn, whole or, if asked, the part that arrived, `draw_assembled` draws it through `Decoder::submit_assembled_to_texture`, which uploads through the queue and does not wait, and `drop_packets` lets go of what arrived of a frame and of its sequence number. The bitstream's sequence number has three bits: a packet one to three frames newer starts that frame; 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 current frame. So a transport keeps frame numbers of its own: it ignores packets of frames older than the one being assembled, and before it pushes the first packet of a newer frame it draws the current one if it is ready, then calls `drop_packets`, never after the push, which would drop that packet too. The colours of such a stream follow each frame's start of frame header unless `set_colors` sets the matrix and the range, as for upstream's encoder, which signals full range whatever the pictures are; the chroma siting follows the header still. A loaded file's colours, siting included, are those of its PWV1 header, for `draw_next` and `draw_assembled` alike. `draw_next`, which plays a loaded file, lets go of each frame it assembled, so that packets pushed after a switch to them start afresh. `packet_ends` says where a frame splits into packets at its blocks, as `emberwave_frame_packets` does in the C API, and `frames` and `frame` hand out the loaded file's frames that can be played. With "Play as packets" (`?packets`, and `?loss=0.05` for a share lost, from 0 to 1) the page plays its stream that way: it takes each frame from the player, cuts it into packets of up to 1200 or 8000 bytes (a larger block goes alone), shuffles them, drops the share chosen, and draws what arrived. A frame of which every block of the coarsest level and more than 90 percent of the blocks its start of frame header announced arrived is drawn with the lost finer blocks blank, and other frames are skipped, the last picture staying; `set_partial_share` sets another share than 90 percent, and the page offers 70 and 50 percent (`?partial=0.7`, D59). Until the header arrives the count is of all the picture's blocks, so a frame that lost its header is skipped unless its encoder left out few blocks. An encoder that leaves out empty blocks of the coarsest level, as upstream's does and emberwave's did before decision D58, leaves such frames nothing to draw in part. No loss reaches the next frame. In headless Chrome on the M3 Max the demo's frames took 38 packets each; with 5 % of them lost, about one frame in six was drawn whole, half in part and the rest skipped. +The player also decodes a stream packet by packet, as a client of a live stream receives it (#109, D55). `Player.open_stream` sets up a stream of a known size without a file, `push_packet` takes a packet, a start of frame header with blocks or blocks alone, in any order, `is_ready` says whether the frame can be drawn, whole or, if asked, the part that arrived, `draw_assembled` draws it through `Decoder::submit_assembled_to_texture`, which uploads through the queue and does not wait, and `drop_packets` lets go of what arrived of a frame and of its sequence number. The bitstream's sequence number has three bits: a packet one to three frames newer starts that frame; 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 current frame. So a transport keeps frame numbers of its own: it ignores packets of frames older than the one being assembled, and before it pushes the first packet of a newer frame it draws the current one if it is ready, then calls `drop_packets`, never after the push, which would drop that packet too. The colours of such a stream follow each frame's start of frame header unless `set_colors` sets the matrix and the range, as for upstream's encoder, which signals full range whatever the pictures are; the chroma siting follows the header still. A loaded file's colours, siting included, are those of its PWV1 header, for `draw_next` and `draw_assembled` alike. `draw_next`, which plays a loaded file, lets go of each frame it assembled, so that packets pushed after a switch to them start afresh. `packet_ends` says where a frame splits into packets at its blocks, as `emberwave_frame_packets` does in the C API, and `frames` and `frame` hand out the loaded file's frames that can be played. With "Play as packets" (`?packets`, and `?loss=0.05` for a share lost, from 0 to 1) the page plays its stream that way: it takes each frame from the player, cuts it into packets of up to 1200 or 8000 bytes (a larger block goes alone), shuffles them, drops the share chosen, and draws what arrived. A frame of which every block of the coarsest level and more than 90 percent of the blocks its start of frame header announced arrived is drawn with the lost finer blocks blank, and other frames are skipped, the last picture staying; `set_partial_share` sets another share than 90 percent, and the page offers 70 and 50 percent (`?partial=0.7`, D59). With `set_concealment(true)`, off by default, the lost finer blocks keep the last frame's detail where the picture is still instead of going blank (D66). Until the header arrives the count is of all the picture's blocks, so a frame that lost its header is skipped unless its encoder left out few blocks. An encoder that leaves out empty blocks of the coarsest level, as upstream's does and emberwave's did before decision D58, leaves such frames nothing to draw in part. No loss reaches the next frame. In headless Chrome on the M3 Max the demo's frames took 38 packets each; with 5 % of them lost, about one frame in six was drawn whole, half in part and the rest skipped. `web/bench.html` times the decoding on the device it runs on, a phone included, with the code of `crates/emberwave-web/src/bench.rs`. It makes test streams at 1080p, 1440p and 4K, four pictures each of a synthetic scene encoded by the CPU encoder in the page at the budgets of the native benchmarks, then decodes them in turn on the player's path, to RGB in GPU memory, and waits for each frame through `onSubmittedWorkDone` before the next: 300 frames after at least 30 and a second of warm-up, as `emberwave-cli bench --output rgb` does natively. Then it decodes them again 60 a second, the warm-up too, on a schedule that the page's timers keep, as the benchmark app does natively at a stream's pace (#95). wgpu's callback for that has to be `Send`, which a JavaScript function is not, so it sets a flag and wakes a future that `wasm_bindgen_futures` turns into the page's promise. Browsers coarsen `performance.now()`, to a millisecond in some, so the mean comes from the time of the whole run and the median and 99th percentile, which are per frame, are as fine as the timer, whose step the report states. The times include what WebGPU adds over native wgpu: the browser's checks, its process boundary and the delay of the report. A GPU error the browser reports during a size fails that size rather than give times for frames that were not decoded, a run during which the page was hidden is marked as not valid, and the benchmark lets go of its device when the run ends. diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 29be9bc..135a984 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -22,7 +22,7 @@ scripts/bench_lavapipe.sh # optional: software Vulkan in a Linux conta - **Statistics.** Mean, median and 99th percentile (nearest rank) over 600 frames, ten passes over each 60-frame stream. With only 60 frames the 99th percentile would be the slowest frame. - **Upstream** is the commit `testdata/README.md` pins; the tables were made at 79a3232, which has neither of upstream's fixes to its encoder of 2026-10-26; those left its decoder's output and its encoder's time as they were (decision D56). It is timed by `refgen bench` (`scripts/refgen/refgen.mm`) around upstream's C API, in the same way: give the decoder the frame's packet, encode the decode into a command buffer, commit it and wait. Its output, three 8-bit planes, stays in GPU memory, so it compares with emberwave's `--output yuv`. - **Encoding from textures** starts from an R8 texture per plane, made from each picture before the measurement (`bench-encode --gpu --textures`, `Encoder::encode_textures`), and ends with the frame's bytes in CPU memory like the encoding from CPU memory below. -- **Encoding from BGRA** starts from the same pictures converted to BGRA, as most captures hold them, in CPU memory (`bench-encode --gpu --rgb`, `Encoder::encode_rgb`) or in a BGRA texture each, made before the measurement (`--rgb --textures`, `TexturePicture::Rgb`), and ends like the others. +- **Encoding from BGRA** starts from the same pictures converted to BGRA, as most captures hold them, in CPU memory (`bench-encode --gpu --rgb`, `Encoder::encode_rgb`) or in a BGRA texture each, made before the measurement (`--rgb --textures`, `TexturePicture::Rgb`), and ends like the others. Its table comes with the next full run: the results below are older. - **Encoding** goes from the picture's 8-bit planes in CPU memory to the frame's bytes in CPU memory: `Encoder::encode` for emberwave, `emberwave-cli bench-encode`; `pyrowave_encoder_encode_cpu`, counting the packets, which waits for the GPU, and `pyrowave_encoder_packetize` for upstream, `refgen bench-encode`. Both upload the planes and read the frame back within the time. The pictures are 20 frames of the same scene, made by `scripts/gen_bench_data.sh --sources`, at the budgets of the decoder's streams of the same size; both tools read them into memory first and take them in turn, so that encodes follow each other as decodes do. Upstream's encoder is timed at its default precision, an FP16 wavelet pyramid, and at FP32, which is what emberwave's uses. It hides its command buffer, so only its total is known. The CPU encoder, at a quarter of a second for a 4K frame, is timed over 60 frames instead of 600. ## Results diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 942cb50..eaa30d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,13 @@ cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` +CI runs two more lints, for code that builds only without the `gpu` feature or only for the browser, and a check on Rust 1.87. The lints need the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`): + +```sh +cargo clippy -p emberwave-core --no-default-features --all-targets -- -D warnings +cargo clippy --target wasm32-unknown-unknown -p emberwave-web -p emberwave-core -- -D warnings +``` + ## Tests | Suite | Where | What it checks | diff --git a/crates/emberwave-core/src/cpu/mod.rs b/crates/emberwave-core/src/cpu/mod.rs index d6f46cc..81c57e9 100644 --- a/crates/emberwave-core/src/cpu/mod.rs +++ b/crates/emberwave-core/src/cpu/mod.rs @@ -133,10 +133,11 @@ impl Concealer { pub fn decode(&mut self, frame: &FrameView<'_>, partial: bool) -> Result { let layout = frame.layout(); let mut bands = dequant::dequantize(frame)?; - if let (true, Some((last_layout, last))) = (partial, &self.last) - && last_layout == layout - { - conceal(frame, &mut bands, last); + // Not a let chain: those need Rust 1.88, and emberwave builds on 1.87. + if let (true, Some((last_layout, last))) = (partial, &self.last) { + if last_layout == layout { + conceal(frame, &mut bands, last); + } } let kept = bands.clone(); let planes = diff --git a/crates/emberwave-core/src/gpu/decoder.rs b/crates/emberwave-core/src/gpu/decoder.rs index a9d27e9..d46b5a5 100644 --- a/crates/emberwave-core/src/gpu/decoder.rs +++ b/crates/emberwave-core/src/gpu/decoder.rs @@ -183,8 +183,11 @@ enum Output { /// Decodes PyroWave frames of one size and chroma format on the GPU. /// -/// All GPU memory is allocated up front and reused for every frame, apart from -/// the payload buffer which grows when a frame needs more room. +/// The memory every decode uses is allocated up front and reused for every +/// frame, and the payload buffer grows when a frame needs more room. What +/// only some decodes need, such as the read-back buffer, the RGB picture or +/// what concealment keeps, is made by the first decode that needs it and then +/// reused. pub struct Decoder { device: wgpu::Device, queue: wgpu::Queue, diff --git a/crates/emberwave-ffi/README.md b/crates/emberwave-ffi/README.md index 7cef329..23a2f8b 100644 --- a/crates/emberwave-ffi/README.md +++ b/crates/emberwave-ffi/README.md @@ -508,7 +508,7 @@ CFRelease(bgra); - **Waiting.** `emberwave_decoder_decode_yuv` and `emberwave_decoder_decode_rgb` wait for the GPU and copy the picture to the CPU. On an Apple M3 Max a 4K frame takes 1.2 ms to planes and 2.6 to 2.7 ms to RGB that way, against 0.9 and 1.2 ms when the picture stays on the GPU. A call that fails may have written some rows. `emberwave_decoder_decode_texture` and `emberwave_decoder_decode_plane_textures` wait as well, and leave the picture on the GPU, in textures for Metal (see [Pictures on the GPU: Metal](#pictures-on-the-gpu-metal)). On a context of the client's own Metal queue, no call that waits may be made inside a completion handler of a command buffer of that queue: the wait never ends there. That includes the destroy functions, which wait when they let go of the last hold on the queue. - **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` 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 (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). +- **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. - **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`. diff --git a/crates/emberwave-ffi/include/emberwave.h b/crates/emberwave-ffi/include/emberwave.h index 23e6462..6cf09d9 100644 --- a/crates/emberwave-ffi/include/emberwave.h +++ b/crates/emberwave-ffi/include/emberwave.h @@ -182,17 +182,21 @@ typedef struct EmberwaveEncoder EmberwaveEncoder; // with the picture, `emberwave_decoder_decode_texture` and // `emberwave_decoder_submit_texture`, or with one of its planes, // `emberwave_decoder_decode_plane_textures` and -// `emberwave_decoder_submit_plane_textures`, for the caller's graphics API -// to read on the GPU. emberwave makes it (`emberwave_texture_create`, -// `emberwave_texture_create_plane`), or wraps a Metal texture or a Vulkan +// `emberwave_decoder_submit_plane_textures`, 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 // image of the caller's (`emberwave_texture_from_metal`, // `emberwave_texture_from_metal_plane`, `emberwave_texture_from_vulkan`, -// `emberwave_texture_from_vulkan_plane`). +// `emberwave_texture_from_vulkan_plane`), or opens a Direct3D texture the +// caller shares (`emberwave_texture_from_d3d12`, +// `emberwave_texture_from_d3d12_plane`). // // Or a texture of the caller's that holds a plane of a picture to encode, -// for `emberwave_encoder_encode_textures` to read -// (`emberwave_texture_from_metal_source`, -// `emberwave_texture_from_vulkan_source`, decision D62). A texture of a +// for `emberwave_encoder_encode_textures` and its `_vulkan` and `_d3d12` +// forms to read (`emberwave_texture_from_metal_source`, +// `emberwave_texture_from_vulkan_source`, +// `emberwave_texture_from_d3d12_source`, decision D62). A texture of a // plane serves the decoders and encoders of its context whose pictures // have planes of its size, so planes a decode put into textures can be // encoded again. @@ -222,6 +226,24 @@ typedef struct EmberwaveEncoder EmberwaveEncoder; // until a decode that waits returns, or until a command buffer that uses // the texture, committed to the queue after a decode that does not wait, // has completed. +// - On a context of `emberwave_context_create_vulkan`, emberwave works on +// the caller's queue. A decode that waits returns once the GPU has +// written the image, and nothing orders the next decode after the +// caller's reads: keep images in turn, as on a context of emberwave's own +// queue. The decodes that do not wait, +// `emberwave_decoder_submit_texture_vulkan` and +// `emberwave_decoder_submit_plane_textures_vulkan`, take semaphores: one +// that the decode waits for, signalled when the caller's reads of the +// image end, and one that the caller's reads wait for. With them one +// image takes every frame. Between calls an image is in +// `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` (decision D61). +// - On a context of `emberwave_context_create_d3d12`, a texture the caller +// shares from Direct3D takes only the `_d3d12` calls, ordered by the +// fences of their `EmberwaveD3D12Sync`: one that the call's work waits +// for, signalled when the caller's reads end, and one it signals when the +// texture holds the picture. With them one texture takes every frame. The +// other calls refuse such a texture with +// `EMBERWAVE_STATUS_INVALID_ARGUMENT` (decision D63). typedef struct EmberwaveTexture EmberwaveTexture; // What emberwave needs a Vulkan device of the caller's to have enabled: @@ -597,7 +619,8 @@ enum EmberwaveStatus emberwave_vulkan_instance_extensions(EmberwaveVkGetInstance // `emberwave_vulkan_requirements_extensions` gives, and the features, which // `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 Windows; elsewhere `EMBERWAVE_STATUS_NO_GPU`. +// 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. // // # Safety @@ -650,15 +673,16 @@ void emberwave_vulkan_requirements_destroy(struct EmberwaveVulkanRequirements *r // Creates a context on the caller's own Vulkan device, for emberwave to // 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 Windows; elsewhere -// `EMBERWAVE_STATUS_NO_GPU`. +// Only where wgpu runs on Vulkan, on Linux, Android and 64-bit Windows; +// elsewhere `EMBERWAVE_STATUS_NO_GPU`. // // The device must have been made with the extensions and the features of -// `emberwave_vulkan_requirements_create`: a lacking extension is -// `EMBERWAVE_STATUS_INVALID_ARGUMENT`, and a lacking feature undefined -// behaviour, as Vulkan cannot tell which were enabled. So is a queue family -// without graphics and compute, a queue it does not have, and an instance -// whose `apiVersion` is below the physical device's, up to 1.3. +// `emberwave_vulkan_requirements_create`. A lacking extension is +// `EMBERWAVE_STATUS_INVALID_ARGUMENT`, and so are a queue family without +// graphics and compute, a queue index past the family's queues, and an +// instance whose `apiVersion` is below the physical device's, up to 1.3. A +// lacking feature is undefined behaviour, as Vulkan cannot tell which were +// enabled. // // The queue is shared: // - emberwave submits to it during its calls. Either nothing else uses the @@ -673,9 +697,11 @@ void emberwave_vulkan_requirements_destroy(struct EmberwaveVulkanRequirements *r // where the device has one, needs neither. // - The decoders and encoders of every context made on the queue take // turns at it, as on Metal (decision D60). -// - The decodes that wait return once the GPU is done. The decodes that do -// not wait, `emberwave_decoder_submit_texture` and -// `emberwave_decoder_submit_plane_textures`, are for Metal contexts only. +// - 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. // // # Safety // @@ -1077,8 +1103,9 @@ enum EmberwaveStatus emberwave_encoder_encode_textures_d3d12(struct EmberwaveEnc // # Safety // // `context` is `NULL`, or a context from `emberwave_context_create`, -// `emberwave_context_create_metal` or `emberwave_context_create_vulkan` -// that has not been destroyed and that no other thread is using. +// `emberwave_context_create_metal`, `emberwave_context_create_vulkan` or +// `emberwave_context_create_d3d12` that has not been destroyed and that no +// other thread is using. void emberwave_context_destroy(struct EmberwaveContext *context); // The name and backend of the context's GPU, such as @@ -1415,15 +1442,19 @@ void emberwave_texture_destroy(struct EmberwaveTexture *texture); // The call does not wait for the caller's GPU work that still reads the // texture. Whether something orders the decode after that work, and so how // many textures the caller keeps in turn, depends on the context and the -// texture: see `EmberwaveTexture`. In short, on a context of -// `emberwave_context_create` keep three, and on one of -// `emberwave_context_create_metal` one tracked texture does. -// -// `texture` is a texture of the picture from `emberwave_texture_create` or -// `emberwave_texture_from_metal`, on a decoder of the same context and -// picture size. `NULL`, a texture of a plane, of another context's device or -// of another picture size gives `EMBERWAVE_STATUS_INVALID_ARGUMENT` and -// leaves the frame to decode. +// texture: see `EmberwaveTexture`. In short, with the decodes that wait, +// keep three on any context but one of `emberwave_context_create_metal`, +// where one tracked texture does. With the decodes that do not wait, one +// texture does: on Metal through the queue, on Vulkan and Direct3D through +// semaphores and fences. +// +// `texture` is a texture of the picture from `emberwave_texture_create`, +// `emberwave_texture_from_metal` or `emberwave_texture_from_vulkan`, on a +// decoder of the same context and picture size. `NULL`, a texture of a +// plane, of another context's device or of another picture size gives +// `EMBERWAVE_STATUS_INVALID_ARGUMENT` and leaves the frame to decode, as +// does a texture shared from Direct3D, which takes +// `emberwave_decoder_submit_texture_d3d12`. // // On a context of `emberwave_context_create_metal`, do not call it inside a // completion handler of a command buffer of the context's queue: the wait @@ -1445,10 +1476,14 @@ enum EmberwaveStatus emberwave_decoder_decode_texture(struct EmberwaveDecoder *d // caller's command buffers committed to it after this call returns see the // picture, and the decode waits for those committed before it that still // read the texture, so one texture takes every frame (see -// `EmberwaveTexture`). A decoder of a context of `emberwave_context_create`, -// or a texture Metal does not track, gives -// `EMBERWAVE_STATUS_INVALID_ARGUMENT`, as nothing there would order the -// caller's reads after the decode. +// `EmberwaveTexture`). A decoder of any other context, or a texture Metal +// does not track, gives `EMBERWAVE_STATUS_INVALID_ARGUMENT`, as nothing +// there would order the caller's reads after the decode. On a context of +// `emberwave_context_create_vulkan` the decode that does not wait is +// `emberwave_decoder_submit_texture_vulkan`, and on one of +// `emberwave_context_create_d3d12` it is +// `emberwave_decoder_submit_texture_d3d12`: they order the work with +// semaphores and fences. // // The queue orders only command buffers that use the texture. Hand the // picture to anything else that reads its memory, such as an @@ -1471,7 +1506,9 @@ enum EmberwaveStatus emberwave_decoder_submit_texture(struct EmberwaveDecoder *d // `cr`, as `emberwave_decoder_decode_plane_textures` does, but returns once // the work is on the queue, as `emberwave_decoder_submit_texture` does: only // for a decoder whose context was made by `emberwave_context_create_metal`, -// and textures whose hazards Metal tracks. +// and textures whose hazards Metal tracks. Vulkan and Direct3D contexts have +// `emberwave_decoder_submit_plane_textures_vulkan` and +// `emberwave_decoder_submit_plane_textures_d3d12`. // // # Safety // @@ -1496,11 +1533,14 @@ enum EmberwaveStatus emberwave_decoder_submit_plane_textures(struct EmberwaveDec // them in otherwise (decision D52). Blocks that did not arrive decode as // zeros, or are concealed (`emberwave_decoder_set_concealment`). // -// Each texture is one for that plane, from `emberwave_texture_create_plane` -// or `emberwave_texture_from_metal_plane`, on a decoder of the same context, +// Each texture is one for that plane, from `emberwave_texture_create_plane`, +// `emberwave_texture_from_metal_plane` or +// `emberwave_texture_from_vulkan_plane`, on a decoder of the same context, // picture size and chroma format, and no Metal texture serves two planes. // Another, a texture of the RGB picture included, gives -// `EMBERWAVE_STATUS_INVALID_ARGUMENT` and leaves the frame to decode. As +// `EMBERWAVE_STATUS_INVALID_ARGUMENT` and leaves the frame to decode, as +// does a texture shared from Direct3D, which takes +// `emberwave_decoder_submit_plane_textures_d3d12`. As // with `emberwave_decoder_decode_texture`, the call does not wait for the // caller's GPU work that still reads the textures, and how many sets of // three to keep in turn depends on the context and the textures: see @@ -1662,7 +1702,8 @@ enum EmberwaveStatus emberwave_encoder_encode_rgb(struct EmberwaveEncoder *encod // the caller's, from `emberwave_texture_from_metal_source` or // `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. A budget too small gives it too, as for +// 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. // diff --git a/crates/emberwave-ffi/src/lib.rs b/crates/emberwave-ffi/src/lib.rs index 4f0b453..587dab8 100644 --- a/crates/emberwave-ffi/src/lib.rs +++ b/crates/emberwave-ffi/src/lib.rs @@ -328,17 +328,21 @@ pub struct EmberwaveDecoder { /// with the picture, `emberwave_decoder_decode_texture` and /// `emberwave_decoder_submit_texture`, or with one of its planes, /// `emberwave_decoder_decode_plane_textures` and -/// `emberwave_decoder_submit_plane_textures`, for the caller's graphics API -/// to read on the GPU. emberwave makes it (`emberwave_texture_create`, -/// `emberwave_texture_create_plane`), or wraps a Metal texture or a Vulkan +/// `emberwave_decoder_submit_plane_textures`, 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 /// image of the caller's (`emberwave_texture_from_metal`, /// `emberwave_texture_from_metal_plane`, `emberwave_texture_from_vulkan`, -/// `emberwave_texture_from_vulkan_plane`). +/// `emberwave_texture_from_vulkan_plane`), or opens a Direct3D texture the +/// caller shares (`emberwave_texture_from_d3d12`, +/// `emberwave_texture_from_d3d12_plane`). /// /// Or a texture of the caller's that holds a plane of a picture to encode, -/// for `emberwave_encoder_encode_textures` to read -/// (`emberwave_texture_from_metal_source`, -/// `emberwave_texture_from_vulkan_source`, decision D62). A texture of a +/// for `emberwave_encoder_encode_textures` and its `_vulkan` and `_d3d12` +/// forms to read (`emberwave_texture_from_metal_source`, +/// `emberwave_texture_from_vulkan_source`, +/// `emberwave_texture_from_d3d12_source`, decision D62). A texture of a /// plane serves the decoders and encoders of its context whose pictures /// have planes of its size, so planes a decode put into textures can be /// encoded again. @@ -368,6 +372,24 @@ pub struct EmberwaveDecoder { /// until a decode that waits returns, or until a command buffer that uses /// the texture, committed to the queue after a decode that does not wait, /// has completed. +/// - On a context of `emberwave_context_create_vulkan`, emberwave works on +/// the caller's queue. A decode that waits returns once the GPU has +/// written the image, and nothing orders the next decode after the +/// caller's reads: keep images in turn, as on a context of emberwave's own +/// queue. The decodes that do not wait, +/// `emberwave_decoder_submit_texture_vulkan` and +/// `emberwave_decoder_submit_plane_textures_vulkan`, take semaphores: one +/// that the decode waits for, signalled when the caller's reads of the +/// image end, and one that the caller's reads wait for. With them one +/// image takes every frame. Between calls an image is in +/// `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` (decision D61). +/// - On a context of `emberwave_context_create_d3d12`, a texture the caller +/// shares from Direct3D takes only the `_d3d12` calls, ordered by the +/// fences of their `EmberwaveD3D12Sync`: one that the call's work waits +/// for, signalled when the caller's reads end, and one it signals when the +/// texture holds the picture. With them one texture takes every frame. The +/// other calls refuse such a texture with +/// `EMBERWAVE_STATUS_INVALID_ARGUMENT` (decision D63). pub struct EmberwaveTexture { texture: wgpu::Texture, view: wgpu::TextureView, @@ -1566,7 +1588,8 @@ fn vulkan_instance_extensions( /// `emberwave_vulkan_requirements_extensions` gives, and the features, which /// `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 Windows; elsewhere `EMBERWAVE_STATUS_NO_GPU`. +/// 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. /// /// # Safety @@ -1666,15 +1689,16 @@ pub unsafe extern "C" fn emberwave_vulkan_requirements_destroy( /// Creates a context on the caller's own Vulkan device, for emberwave to /// 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 Windows; elsewhere -/// `EMBERWAVE_STATUS_NO_GPU`. +/// Only where wgpu runs on Vulkan, on Linux, Android and 64-bit Windows; +/// elsewhere `EMBERWAVE_STATUS_NO_GPU`. /// /// The device must have been made with the extensions and the features of -/// `emberwave_vulkan_requirements_create`: a lacking extension is -/// `EMBERWAVE_STATUS_INVALID_ARGUMENT`, and a lacking feature undefined -/// behaviour, as Vulkan cannot tell which were enabled. So is a queue family -/// without graphics and compute, a queue it does not have, and an instance -/// whose `apiVersion` is below the physical device's, up to 1.3. +/// `emberwave_vulkan_requirements_create`. A lacking extension is +/// `EMBERWAVE_STATUS_INVALID_ARGUMENT`, and so are a queue family without +/// graphics and compute, a queue index past the family's queues, and an +/// instance whose `apiVersion` is below the physical device's, up to 1.3. A +/// lacking feature is undefined behaviour, as Vulkan cannot tell which were +/// enabled. /// /// The queue is shared: /// - emberwave submits to it during its calls. Either nothing else uses the @@ -1689,9 +1713,11 @@ pub unsafe extern "C" fn emberwave_vulkan_requirements_destroy( /// where the device has one, needs neither. /// - The decoders and encoders of every context made on the queue take /// turns at it, as on Metal (decision D60). -/// - The decodes that wait return once the GPU is done. The decodes that do -/// not wait, `emberwave_decoder_submit_texture` and -/// `emberwave_decoder_submit_plane_textures`, are for Metal contexts only. +/// - 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. /// /// # Safety /// @@ -3721,8 +3747,9 @@ mod d3d12 { /// # Safety /// /// `context` is `NULL`, or a context from `emberwave_context_create`, -/// `emberwave_context_create_metal` or `emberwave_context_create_vulkan` -/// that has not been destroyed and that no other thread is using. +/// `emberwave_context_create_metal`, `emberwave_context_create_vulkan` or +/// `emberwave_context_create_d3d12` that has not been destroyed and that no +/// other thread is using. #[unsafe(no_mangle)] pub unsafe extern "C" fn emberwave_context_destroy(context: *mut EmberwaveContext) { if !context.is_null() { @@ -4571,15 +4598,19 @@ pub unsafe extern "C" fn emberwave_texture_destroy(texture: *mut EmberwaveTextur /// The call does not wait for the caller's GPU work that still reads the /// texture. Whether something orders the decode after that work, and so how /// many textures the caller keeps in turn, depends on the context and the -/// texture: see `EmberwaveTexture`. In short, on a context of -/// `emberwave_context_create` keep three, and on one of -/// `emberwave_context_create_metal` one tracked texture does. -/// -/// `texture` is a texture of the picture from `emberwave_texture_create` or -/// `emberwave_texture_from_metal`, on a decoder of the same context and -/// picture size. `NULL`, a texture of a plane, of another context's device or -/// of another picture size gives `EMBERWAVE_STATUS_INVALID_ARGUMENT` and -/// leaves the frame to decode. +/// texture: see `EmberwaveTexture`. In short, with the decodes that wait, +/// keep three on any context but one of `emberwave_context_create_metal`, +/// where one tracked texture does. With the decodes that do not wait, one +/// texture does: on Metal through the queue, on Vulkan and Direct3D through +/// semaphores and fences. +/// +/// `texture` is a texture of the picture from `emberwave_texture_create`, +/// `emberwave_texture_from_metal` or `emberwave_texture_from_vulkan`, on a +/// decoder of the same context and picture size. `NULL`, a texture of a +/// plane, of another context's device or of another picture size gives +/// `EMBERWAVE_STATUS_INVALID_ARGUMENT` and leaves the frame to decode, as +/// does a texture shared from Direct3D, which takes +/// `emberwave_decoder_submit_texture_d3d12`. /// /// On a context of `emberwave_context_create_metal`, do not call it inside a /// completion handler of a command buffer of the context's queue: the wait @@ -4669,10 +4700,14 @@ fn require_tracked(texture: &EmberwaveTexture, name: &str) -> Result<(), Failure /// caller's command buffers committed to it after this call returns see the /// picture, and the decode waits for those committed before it that still /// read the texture, so one texture takes every frame (see -/// `EmberwaveTexture`). A decoder of a context of `emberwave_context_create`, -/// or a texture Metal does not track, gives -/// `EMBERWAVE_STATUS_INVALID_ARGUMENT`, as nothing there would order the -/// caller's reads after the decode. +/// `EmberwaveTexture`). A decoder of any other context, or a texture Metal +/// does not track, gives `EMBERWAVE_STATUS_INVALID_ARGUMENT`, as nothing +/// there would order the caller's reads after the decode. On a context of +/// `emberwave_context_create_vulkan` the decode that does not wait is +/// `emberwave_decoder_submit_texture_vulkan`, and on one of +/// `emberwave_context_create_d3d12` it is +/// `emberwave_decoder_submit_texture_d3d12`: they order the work with +/// semaphores and fences. /// /// The queue orders only command buffers that use the texture. Hand the /// picture to anything else that reads its memory, such as an @@ -4717,7 +4752,9 @@ pub unsafe extern "C" fn emberwave_decoder_submit_texture( /// `cr`, as `emberwave_decoder_decode_plane_textures` does, but returns once /// the work is on the queue, as `emberwave_decoder_submit_texture` does: only /// for a decoder whose context was made by `emberwave_context_create_metal`, -/// and textures whose hazards Metal tracks. +/// and textures whose hazards Metal tracks. Vulkan and Direct3D contexts have +/// `emberwave_decoder_submit_plane_textures_vulkan` and +/// `emberwave_decoder_submit_plane_textures_d3d12`. /// /// # Safety /// @@ -4787,11 +4824,14 @@ fn require_shared_queue(decoder: &EmberwaveDecoder) -> Result<(), Failure> { /// them in otherwise (decision D52). Blocks that did not arrive decode as /// zeros, or are concealed (`emberwave_decoder_set_concealment`). /// -/// Each texture is one for that plane, from `emberwave_texture_create_plane` -/// or `emberwave_texture_from_metal_plane`, on a decoder of the same context, +/// Each texture is one for that plane, from `emberwave_texture_create_plane`, +/// `emberwave_texture_from_metal_plane` or +/// `emberwave_texture_from_vulkan_plane`, on a decoder of the same context, /// picture size and chroma format, and no Metal texture serves two planes. /// Another, a texture of the RGB picture included, gives -/// `EMBERWAVE_STATUS_INVALID_ARGUMENT` and leaves the frame to decode. As +/// `EMBERWAVE_STATUS_INVALID_ARGUMENT` and leaves the frame to decode, as +/// does a texture shared from Direct3D, which takes +/// `emberwave_decoder_submit_plane_textures_d3d12`. As /// with `emberwave_decoder_decode_texture`, the call does not wait for the /// caller's GPU work that still reads the textures, and how many sets of /// three to keep in turn depends on the context and the textures: see @@ -5197,7 +5237,8 @@ fn rgb_order(format: u32) -> Result { /// the caller's, from `emberwave_texture_from_metal_source` or /// `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. A budget too small gives it too, as for +/// 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. ///