diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 829bc45..c54c42b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -487,7 +487,7 @@ PNG output is 8-bit RGB with fast compression. The Y4M writer takes range, chrom Nothing in a browser may block, so the player uses only the parts of the decoder that do not wait: - `GpuContext::from_adapter` creates the device on an adapter that can draw on the canvas. `GpuContext::new`, which blocks, does not exist in the browser build. -- `Decoder::encode_rgb_to_texture` records the decode and the colour conversion into the player's command encoder and puts the picture into the canvas's texture: by a copy where the canvas allows it and is a multiple of 64 pixels wide, and otherwise by a draw (`draw.wgsl`: one triangle over the canvas, which has the size of the picture, so a fragment reads one word; decisions D40 and D42). One submission per frame, and the picture never leaves the GPU. +- `Decoder::record_rgb_to_texture` records the decode and the colour conversion into the player's command encoder and puts the picture into the canvas's texture: by a copy where the canvas allows it and is a multiple of 64 pixels wide, and otherwise by a draw (`draw.wgsl`: one triangle over the canvas, which has the size of the picture, so a fragment reads one word; decisions D40 and D42). One submission per frame, and the picture never leaves the GPU. - Error scopes cannot be waited for, so in the browser build `with_error_scope` just runs its work. The player catches what the browser reports instead (`on_uncaptured_error`) and shows the first error on the page with the next frame. Pictures wider or taller than the browser can draw (`max_texture_dimension_2d`, 8192 by default) are refused when the file is loaded. - The methods that read a frame back or time it wait for the GPU, and are not for the browser. The decoder makes its read-back buffers only when a decode first reads back, so the player has none: 43.5 MiB fewer at 4K. - In a browser, dropping a wgpu buffer only lets go of the JavaScript object, and the GPU memory waits for the garbage collector. `Decoder` therefore destroys its large buffers when it is dropped, and the player drops the old stream before it loads the next file: about 110 MiB for a 4K stream. @@ -1452,7 +1452,7 @@ The figures of the prototype's runs above are marked as not kept. **Context.** A frame that lost packets is decoded in part once every block of the coarsest level and more than 90 percent of its blocks have arrived, the share upstream's decoder uses (D58). Losses on Wi-Fi come in bursts, and a burst takes far more than a tenth of the frame it falls on: in emberwave-stream, with blackouts of 4 ms five times a second at 200 Mbit/s, 60 of 1194 1080p frames were skipped. Decoded on the CPU, the benchmark scene's first picture at 150 kB keeps a luma PSNR of 31.8 dB against the source with 20 % of its packets lost in a burst and 29.2 dB with half lost, against 41.3 dB whole; the previous picture, which a skip leaves on screen, is 30.9 dB from it (#123). Which a person prefers, a part of the picture blurred or a frame repeated, is a matter of the stream and the eye. **Decision.** -- `gpu::Decoder::set_partial_share` sets the share for `is_ready(true)`, from 0 to 1, 0.9 by default: below 0 counts as 0, above 1 as 1, and NaN sets the default again. The decoder keeps it until it is set again; `FrameAssembler`'s own `is_ready` keeps upstream's share. +- `gpu::Decoder::set_partial_share` sets the share for `is_ready(true)`, from 0 to 1, 0.9 by default: below 0 counts as 0, above 1 as 1, and NaN sets the default again. The decoder keeps it until it is set again; `FrameAssembler`'s own `is_ready` keeps upstream's share. (Superseded by D70: a share outside 0 to 1, or NaN, is an error, as in the C API and the browser player.) - The browser player's `set_partial_share` sets it for the stream open, which `is_ready(true)` and `draw_assembled` follow; the C API's `emberwave_decoder_set_partial_share` sets it for `emberwave_decoder_is_ready`. Both refuse a share outside 0 to 1, NaN included, and change nothing then. - The demo page plays its packets with a share chosen beside the loss, 90 %, 70 % or 50 % (`?partial=0.7`). - The default stays: a client that sets nothing decodes as before. @@ -1475,7 +1475,7 @@ The queues wgpu makes itself have room for 4096 command buffers. - **Making takes a turn.** Making a context on such a queue, and a decoder or an encoder on one, takes a turn and submits wgpu's writes at once, so that no context holds a command buffer between calls. - **A failed call submits too.** A call that fails after it has written through the queue submits those writes as its turn ends. - **One context made at a time.** The C API makes the contexts on a client's queue one at a time. From the making of its device until its first turn, a context holds a command buffer outside any turn. A second review found that three contexts made at once, beside a call that waited for room, hung a queue of 20. -- **The caller's own work.** A caller that records emberwave's work into its own encoder, with `Decoder::encode_rgb` and the like, takes a turn for its submission with `QueueTurns::take`. +- **The caller's own work.** A caller that records emberwave's work into its own encoder, with `Decoder::encode_rgb` and the like (`record_rgb` since D70), takes a turn for its submission with `QueueTurns::take`. - **One set of turns per queue.** Contexts made on one `MTLCommandQueue` share their turns. The C API finds them by the queue's address while anything made on that queue lives; any client that makes devices on its own queue can use `GpuContext::with_queue_turns`. - **No turns on wgpu's queues.** They cannot run out of command buffers, and turns would cost the threads that share one context up to a tenth of their throughput. - **Docs.** The C API gives the measured counts and the room a queue needs: 20 command buffers besides those the caller holds uncommitted. It also says that nothing of emberwave may be called inside a completion handler of a command buffer of that queue. @@ -1791,3 +1791,38 @@ The last frame's detail, always, gains everything on a still picture and loses u **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. + +### 2026-10-31 — D70: The Rust API before 1.0 + +**Context.** The same audit (#152) found `emberwave-core`'s API hard to grow, and harder to use than it had to be: +- No enum or struct was `#[non_exhaustive]`, though six releases since 0.1 broke a `match` or a struct literal. +- `Decoder::encode_rgb` recorded a decode, while `Encoder::encode_rgb` encoded a picture. +- `GpuOptions::default()` read an environment variable. +- `set_partial_share` clamped a share that the C API and the browser player refuse. +- Items for tests were public. +- Calls that wait for the GPU were built for the browser, where they cannot work. + +**Decision.** +- **`#[non_exhaustive]`** on the enums that grow: `GpuError`, `EncodeError`, `ColorError`, `bitstream::Error`, `bitstream::Header`, `TexturePicture`, `TimedOutput`, `ColorMatrix` and `RgbOrder`. +- **The same on the structs that grow.** + - Callers read the timings, the frames and their planes. + - Callers make `GpuOptions`, `ColorParams` and `Signalling` from their `Default` and change them member by member. `ColorParams::new` takes its three members. `Timings` has a `Default` for that too. + - The one-bit signalling enums, `Component` and `Band` stay exhaustive, as the format fixes them. +- **Names.** The decoder's calls that record into the caller's encoder are `record_rgb`, `record_rgb_to_texture` and `record_to_plane_textures`. +- **No hidden input.** `GpuOptions::default()` is the defaults alone. `GpuOptions::from_env()` reads `EMBERWAVE_BACKEND`, for the command line tool and the C API's `NULL` options. +- **One rule for the share.** `set_partial_share` returns a `Result`. A share outside 0 to 1, or NaN, is `GpuError::InvalidArgument`, a new variant, as in the C API and the browser player. +- **One name each.** `pwv1::HEADER_BYTES` is now `FILE_HEADER_BYTES`, apart from the 8 bytes of a frame's header. +- **Hidden from the docs, and outside the API**, which the crate's docs say: + - `shader_sources` and `Decoder::read_band`; + - the CPU decoder's dequantiser and transform modules; + - the block parser's structures; + - the colour module's constants, siting taps and forward matrix; + - `GpuOptions`' `strict_webgpu`, `plane_stores` and `shared_uploads`. +- **Not in the browser build.** The calls that wait for the GPU are not built for wasm32: the decoder's `decode`, `decode_rgb`, `time_frame`, `decode_assembled` and its forms, and `read_band`, and every encode of the GPU encoder. Their helpers may go unused there, which the GPU module allows on wasm32 only. +- **`wgpu` re-exported** as `emberwave_core::wgpu`: the version emberwave is built with. + +**Not done.** About thirty public items take or return wgpu's types, so a new major version of wgpu breaks emberwave's Rust API. Two answers remain for 1.0: follow wgpu's major versions, or split a crate without wgpu types from the interop (#152). + +**Consequence.** +- Rust callers change: a `match` on those enums gains a wildcard arm, and a struct literal of those types becomes a default changed member by member. The recording calls take their new names, and `set_partial_share`'s result is handled. +- emberwave's own crates and tests have changed with it, and so does emberwave-stream when it moves to the next release. diff --git a/CHANGELOG.md b/CHANGELOG.md index 421cc22..e074ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to emberwave. The format follows [Keep a Changelog](https:// ### Added +- `emberwave_core::wgpu` is the wgpu that emberwave is built with, for sharing a device or a texture with it (decision D70). - `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. @@ -19,6 +20,17 @@ All notable changes to emberwave. The format follows [Keep a Changelog](https:// - `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. +- In Rust, the enums and structs that grow are `#[non_exhaustive]` (decision D70): + - the enums `GpuError`, `EncodeError`, `ColorError`, `bitstream::Error`, `bitstream::Header`, `TexturePicture`, `TimedOutput`, `ColorMatrix` and `RgbOrder`; + - the structs `GpuOptions`, `ColorParams`, `Signalling`, `Timings`, `StageTimings`, `EncodeTimings`, `EncodeStageTimings`, `Frame`, `RgbFrame` and `FramePlane`. + + A `match` on them needs a wildcard arm, and a pattern of those structs needs `..`. `GpuOptions`, `ColorParams`, `Signalling` and `Timings` are made from their `Default` and changed member by member; `ColorParams::new` takes its three members. The others, the frames, their planes and the stage timings, are what emberwave's calls give: other crates read them and no longer make them. +- `Decoder::encode_rgb`, `encode_rgb_to_texture` and `encode_to_plane_textures`, which record a decode into the caller's encoder, are now `record_rgb`, `record_rgb_to_texture` and `record_to_plane_textures`. +- `GpuOptions::default()` no longer reads `EMBERWAVE_BACKEND`; `GpuOptions::from_env()` does. +- `Decoder::set_partial_share` returns a `Result`. A share outside 0 to 1, or NaN, is `GpuError::InvalidArgument`, a new variant, as in the C API and the browser player; it was clamped before. +- `bitstream::pwv1::HEADER_BYTES` is now `FILE_HEADER_BYTES`. +- The calls that wait for the GPU are not built for wasm32, where they could not work. +- Items there for emberwave's own tests are hidden from the docs and not part of the API: `shader_sources`, `Decoder::read_band`, the CPU decoder's dequantiser and transform, the block parser's structures, the colour module's constants and siting helpers, and three of `GpuOptions`' members. ## [0.16.0] - 2026-10-31 diff --git a/crates/emberwave-cli/src/bench.rs b/crates/emberwave-cli/src/bench.rs index c139a44..dc46d83 100644 --- a/crates/emberwave-cli/src/bench.rs +++ b/crates/emberwave-cli/src/bench.rs @@ -197,10 +197,12 @@ pub fn run(args: &Args) -> Result<()> { decoder.decode_rgb(frame, &color)?; } } - Ok(Timings { - prepare: start.elapsed(), - gpu_wall: Duration::ZERO, - stages: None, + Ok({ + let mut timings = Timings::default(); + timings.prepare = start.elapsed(); + timings.gpu_wall = Duration::ZERO; + timings.stages = None; + timings }) } else { Ok(decoder.time_frame(frame, output)?) diff --git a/crates/emberwave-cli/src/encode.rs b/crates/emberwave-cli/src/encode.rs index add0665..e7d09b6 100644 --- a/crates/emberwave-cli/src/encode.rs +++ b/crates/emberwave-cli/src/encode.rs @@ -120,12 +120,14 @@ pub fn run(args: &Args) -> Result<()> { YcbcrTransform::Bt2020, ), }; - let signalling = Signalling { - color_primaries: primaries, - transfer_function: transfer, - ycbcr_transform: transform, - ycbcr_range: header.range, - chroma_siting: header.siting, + let signalling = { + let mut signalling = Signalling::default(); + signalling.color_primaries = primaries; + signalling.transfer_function = transfer; + signalling.ycbcr_transform = transform; + signalling.ycbcr_range = header.range; + signalling.chroma_siting = header.siting; + signalling }; let width = u32::try_from(header.width)?; let height = u32::try_from(header.height)?; diff --git a/crates/emberwave-cli/src/info.rs b/crates/emberwave-cli/src/info.rs index 430823c..4a0bf49 100644 --- a/crates/emberwave-cli/src/info.rs +++ b/crates/emberwave-cli/src/info.rs @@ -111,6 +111,10 @@ pub fn run(args: &Args) -> Result<()> { "frame {index:>6}: {:>9} bytes, no start of frame header first", frame.len() ), + Ok(_) => out!( + "frame {index:>6}: {:>9} bytes, a header this version does not know", + frame.len() + ), Err(error) => out!("frame {index:>6}: {:>9} bytes, {error}", frame.len()), } } diff --git a/crates/emberwave-cli/tests/cli.rs b/crates/emberwave-cli/tests/cli.rs index 6629ed7..e5122f3 100644 --- a/crates/emberwave-cli/tests/cli.rs +++ b/crates/emberwave-cli/tests/cli.rs @@ -378,9 +378,10 @@ fn decode_to_png_converts_the_decoded_planes_correctly() { // The same conversion done on the CPU from the Y4M the same decoder wrote. let (width, height, [y, cb, cr]) = first_frame(&dir.join(format!("{VECTOR}.y4m"))); - let params = ColorParams { - matrix: ColorMatrix::Bt709, - ..ColorParams::default() + let params = { + let mut params = ColorParams::default(); + params.matrix = ColorMatrix::Bt709; + params }; let expected = yuv_to_rgb8( [&y, &cb, &cr], diff --git a/crates/emberwave-core/src/bitstream/block.rs b/crates/emberwave-core/src/bitstream/block.rs index e2638d2..3490e7d 100644 --- a/crates/emberwave-core/src/bitstream/block.rs +++ b/crates/emberwave-core/src/bitstream/block.rs @@ -42,6 +42,7 @@ fn block8_plane_bytes(control_word: u16, qscale: u8) -> usize { /// Reads outside the block return 0. Callers check sizes first, and this /// keeps a malformed block from ever causing a panic. #[derive(Debug, Clone, Copy)] +#[doc(hidden)] pub struct BlockBytes<'a> { words: &'a [u32], } @@ -77,6 +78,7 @@ impl<'a> BlockBytes<'a> { /// An 8x8 block that carries data (its `ballot` bit is set). #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[doc(hidden)] pub struct ActiveBlock8 { /// Two bits per subblock: bit-planes on top of `base_planes`. pub control_word: u16, @@ -99,6 +101,7 @@ impl ActiveBlock8 { /// Where the parts of a block are, worked out from its header and tables. #[derive(Debug, Clone, PartialEq, Eq)] +#[doc(hidden)] pub struct BlockStructure { header: BlockHeader, blocks8: [Option; BLOCK8_PER_BLOCK], diff --git a/crates/emberwave-core/src/bitstream/error.rs b/crates/emberwave-core/src/bitstream/error.rs index 23f7a32..1ea750a 100644 --- a/crates/emberwave-core/src/bitstream/error.rs +++ b/crates/emberwave-core/src/bitstream/error.rs @@ -8,6 +8,7 @@ use super::header::ChromaSubsampling; /// /// Parsing untrusted input never panics; it returns one of these instead. #[derive(Debug, Clone, PartialEq, Eq, Error)] +#[non_exhaustive] pub enum Error { /// Width or height is outside `1..=16384`, or a 4:2:0 picture has an odd /// dimension. diff --git a/crates/emberwave-core/src/bitstream/header.rs b/crates/emberwave-core/src/bitstream/header.rs index 947cf05..76f2fdb 100644 --- a/crates/emberwave-core/src/bitstream/header.rs +++ b/crates/emberwave-core/src/bitstream/header.rs @@ -207,6 +207,7 @@ impl SequenceHeader { /// A parsed 8 byte header, told apart by its `extended` bit. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum Header { /// Header of a coded 32x32 block. Block(BlockHeader), diff --git a/crates/emberwave-core/src/bitstream/pwv1.rs b/crates/emberwave-core/src/bitstream/pwv1.rs index e615932..51b1552 100644 --- a/crates/emberwave-core/src/bitstream/pwv1.rs +++ b/crates/emberwave-core/src/bitstream/pwv1.rs @@ -12,7 +12,7 @@ use super::header::{SequenceHeader, read_words}; pub const MAGIC: [u8; 4] = *b"PWV1"; /// Size of the PWV1 header in bytes. -pub const HEADER_BYTES: usize = 24; +pub const FILE_HEADER_BYTES: usize = 24; /// The only header version there is. pub const HEADER_VERSION: u8 = 1; @@ -46,7 +46,7 @@ impl Pwv1Header { /// Parses the header at the start of `bytes`. pub fn parse(bytes: &[u8]) -> Result { let header = bytes - .first_chunk::() + .first_chunk::() .ok_or(Error::TruncatedFile)?; if header[..MAGIC.len()] != MAGIC { return Err(Error::BadMagic); @@ -76,8 +76,8 @@ impl Pwv1Header { /// The 24 header bytes, the inverse of [`parse`](Self::parse). The /// reserved fields of the stream description (`sequence`, /// `total_blocks`, `code`) are written as they are; a file has them zero. - pub fn to_bytes(&self) -> [u8; HEADER_BYTES] { - let mut bytes = [0u8; HEADER_BYTES]; + pub fn to_bytes(&self) -> [u8; FILE_HEADER_BYTES] { + let mut bytes = [0u8; FILE_HEADER_BYTES]; bytes[..MAGIC.len()].copy_from_slice(&MAGIC); let (word0, word1) = self.stream.stream_words(); bytes[STREAM_OFFSET..STREAM_OFFSET + 4].copy_from_slice(&word0.to_le_bytes()); @@ -105,7 +105,7 @@ impl<'a> Pwv1File<'a> { let header = Pwv1Header::parse(bytes)?; Ok(Self { header, - frames: &bytes[HEADER_BYTES..], + frames: &bytes[FILE_HEADER_BYTES..], }) } diff --git a/crates/emberwave-core/src/color.rs b/crates/emberwave-core/src/color.rs index dc7fc58..b296e2f 100644 --- a/crates/emberwave-core/src/color.rs +++ b/crates/emberwave-core/src/color.rs @@ -22,22 +22,28 @@ use crate::bitstream::{ /// Reciprocal of 255, so 8-bit samples are scaled by a multiplication (a /// division could be turned into a less exact multiplication by a GPU). +#[doc(hidden)] pub const INV_255: f32 = 1.0 / 255.0; /// The 8-bit video black level (limited range luma). +#[doc(hidden)] pub const LUMA_BLACK: f32 = 16.0 / 255.0; /// Stretches limited range luma (219 steps) to `[0, 1]`. +#[doc(hidden)] pub const LUMA_SCALE: f32 = 255.0 / 219.0; /// Stretches limited range chroma (224 steps) to `[-0.5, 0.5]`. +#[doc(hidden)] pub const CHROMA_SCALE: f32 = 255.0 / 224.0; /// The 8-bit value of zero chroma. +#[doc(hidden)] pub const CHROMA_CENTRE: f32 = 128.0 / 255.0; /// The set of coefficients that relates Y'CbCr to R'G'B'. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum ColorMatrix { /// ITU-R BT.601. Bt601, @@ -82,6 +88,7 @@ impl From for ColorMatrix { /// How to turn decoded planes into RGB. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub struct ColorParams { /// Matrix coefficients. pub matrix: ColorMatrix, @@ -104,6 +111,16 @@ impl Default for ColorParams { } impl ColorParams { + /// The conversion with `matrix`, `range` and `chroma_siting`. A member a + /// later version adds takes its default. + pub const fn new(matrix: ColorMatrix, range: YcbcrRange, chroma_siting: ChromaSiting) -> Self { + Self { + matrix, + range, + chroma_siting, + } + } + /// Takes the signalling of a stream, from a PWV1 file header or a start /// of frame header. The upstream encoder only fills these in in the PWV1 /// header, so prefer that one. @@ -122,6 +139,7 @@ impl ColorParams { /// Returns `(base, fraction)` where the value is /// `(4 - fraction) * chroma[base] + fraction * chroma[base + 1]`, divided by 4. /// `base` may be -1, which the caller clamps to the edge. +#[doc(hidden)] pub fn chroma_taps(p: u32, siting: AxisSiting) -> (i32, u32) { let half = (p / 2) as i32; match (siting, p % 2) { @@ -136,6 +154,7 @@ pub fn chroma_taps(p: u32, siting: AxisSiting) -> (i32, u32) { /// Why [`yuv_to_rgb8`] or [`rgb_to_ycbcr8`] refused its input. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[non_exhaustive] pub enum ColorError { /// A plane holds fewer samples than the picture size needs. #[error("plane {plane} has {actual} samples, but the picture needs {needed}")] @@ -170,6 +189,7 @@ pub enum ColorError { /// Siting along one axis. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[doc(hidden)] pub enum AxisSiting { /// Chroma sits between the two luma samples it covers. Centre, @@ -281,6 +301,7 @@ pub fn yuv_to_rgb8( /// The order of a pixel's four bytes in an RGB picture: R', G' and B', and /// a fourth that is not read, such as alpha. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum RgbOrder { /// R', G', B', then the fourth byte. Rgba, @@ -289,6 +310,7 @@ pub enum RgbOrder { } /// Fractional bits of [`ForwardMatrix`]'s factors. +#[doc(hidden)] pub const FORWARD_BITS: u32 = 16; /// RGB to YCbCr in integers, as [`rgb_to_ycbcr8`] and the encoder's @@ -303,6 +325,7 @@ pub const FORWARD_BITS: u32 = 16; /// 224 steps of 255 in the video range). Sums over up to 8 pixels stay /// well within 32 bits. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[doc(hidden)] pub struct ForwardMatrix { /// Factors of R', G' and B' for Y, Cb and Cr. pub rows: [[i32; 3]; 3], diff --git a/crates/emberwave-core/src/cpu/mod.rs b/crates/emberwave-core/src/cpu/mod.rs index 81c57e9..0daaf9c 100644 --- a/crates/emberwave-core/src/cpu/mod.rs +++ b/crates/emberwave-core/src/cpu/mod.rs @@ -5,7 +5,9 @@ //! GPU, and so that shader stages can be compared against something simple. //! It is not meant to be fast. +#[doc(hidden)] pub mod dequant; +#[doc(hidden)] pub mod idwt; use crate::bitstream::consts::{BLOCK_SIZE, DC_SHIFT, DECOMPOSITION_LEVELS, NUM_COMPONENTS}; @@ -54,6 +56,7 @@ pub struct DecodedFrame { /// component: level 4 comes from the bitstream and the others are produced by /// the level above. Row-major with the width of the level as stride. #[derive(Debug, Clone, Default)] +#[doc(hidden)] pub struct LlBands { levels: [[Option>; DECOMPOSITION_LEVELS]; NUM_COMPONENTS], } @@ -76,6 +79,7 @@ pub fn decode_frame(frame: &FrameView<'_>) -> Result { } /// Like [`decode_frame`], and also returns the intermediate LL bands. +#[doc(hidden)] pub fn decode_frame_with_ll(frame: &FrameView<'_>) -> Result<(DecodedFrame, LlBands), Error> { let layout = frame.layout(); let mut bands = dequant::dequantize(frame)?; @@ -118,8 +122,9 @@ impl Concealer { /// The coefficients the last decode left, concealment included, as the /// GPU decoder's pyramid holds them after the same decode - /// ([`gpu::Decoder::read_band`](crate::gpu::Decoder::read_band) reads - /// the bands that are not LL, and the coarsest LL). + /// (`gpu::Decoder::read_band` reads the bands that are not LL, and the + /// coarsest LL). For tests: its type is the dequantiser's. + #[doc(hidden)] pub fn bands(&self) -> Option<&Bands> { self.last.as_ref().map(|(_, bands)| bands) } diff --git a/crates/emberwave-core/src/encode/mod.rs b/crates/emberwave-core/src/encode/mod.rs index 5d59acf..5db9a31 100644 --- a/crates/emberwave-core/src/encode/mod.rs +++ b/crates/emberwave-core/src/encode/mod.rs @@ -40,6 +40,7 @@ use rate::BlockCost; /// What a stream says about its colours. It changes nothing in the coding, /// only the start of frame headers and a PWV1 file's header. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub struct Signalling { /// Colour primaries. pub color_primaries: ColorPrimaries, @@ -80,6 +81,7 @@ impl Default for Signalling { /// What can go wrong while encoding a frame. #[derive(Debug, Clone, PartialEq, Eq, Error)] +#[non_exhaustive] pub enum EncodeError { /// A plane does not have the size the stream's pictures have. #[error("the {component:?} plane has {actual} samples, {expected} expected")] @@ -414,6 +416,7 @@ pub(crate) fn check_planes(layout: &FrameLayout, planes: [&[u8]; 3]) -> Result<( /// least `(height - 1) * stride + width` bytes in the plane. For the GPU /// encoder's strided input. #[cfg(feature = "gpu")] +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub(crate) fn check_strided_planes( layout: &FrameLayout, planes: [&[u8]; 3], diff --git a/crates/emberwave-core/src/gpu/context.rs b/crates/emberwave-core/src/gpu/context.rs index f29b310..e114fb1 100644 --- a/crates/emberwave-core/src/gpu/context.rs +++ b/crates/emberwave-core/src/gpu/context.rs @@ -16,6 +16,7 @@ pub const BACKEND_ENV: &str = "EMBERWAVE_BACKEND"; /// How the adapter is chosen. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct GpuOptions { /// Backends to consider. pub backends: wgpu::Backends, @@ -26,6 +27,7 @@ pub struct GpuOptions { /// Restrict the device to what the WebGPU specification guarantees: the /// default limits and no optional features. Used by tests to show that the /// shaders are portable. + #[doc(hidden)] pub strict_webgpu: bool, /// Ask for GPU timestamp queries if the adapter has them, so that /// [`Decoder::time_frame`](super::Decoder::time_frame) can report the time @@ -36,6 +38,7 @@ pub struct GpuOptions { /// Metal, through the adapter's own format features (decision D52). /// Without it, and elsewhere, the decoders copy the planes into the /// textures (decision D41). Ignored with `strict_webgpu`. + #[doc(hidden)] pub plane_stores: bool, /// Let the decoders write a frame, and the encoders a picture's planes, /// where their shaders read them, on GPUs that share their memory with @@ -43,6 +46,7 @@ pub struct GpuOptions { /// (decisions D53, D54). Without it, and elsewhere, they go into an /// upload buffer that the GPU copies from (decisions D30, D34). Ignored /// with `strict_webgpu`. + #[doc(hidden)] pub shared_uploads: bool, } @@ -61,8 +65,7 @@ impl GpuOptions { } /// The default options with the backends taken from [`BACKEND_ENV`] if it - /// is set. A value that names no backend is an error here, where - /// [`Default`] would ignore it. + /// is set. A value that names no backend is an error. pub fn from_env() -> Result { let mut options = Self::base(); if let Some(value) = std::env::var_os(BACKEND_ENV) { @@ -78,11 +81,10 @@ impl GpuOptions { } impl Default for GpuOptions { - /// All native backends unless [`BACKEND_ENV`] names others, preferring the - /// high-performance GPU. An unusable value of the variable is ignored; use - /// [`GpuOptions::from_env`] to be told about it. + /// All native backends, preferring the high-performance GPU. The + /// environment has no say: [`GpuOptions::from_env`] gives it one. fn default() -> Self { - Self::from_env().unwrap_or_else(|_| Self::base()) + Self::base() } } @@ -256,7 +258,7 @@ impl GpuContext { /// caller's, with `wgpu::Adapter::create_device_from_hal`, whose room for /// command buffers they share (#126, decision D60). The calls that make /// and submit their own command buffers take turns; those that record - /// into the caller's encoder, such as `Decoder::encode_rgb`, leave the + /// into the caller's encoder, such as `Decoder::record_rgb`, leave the /// submission and the turn to the caller, which takes it with /// [`QueueTurns::take`]. /// @@ -388,8 +390,8 @@ impl GpuContext { /// Whether the decoders store the planes straight into the textures of /// decodes into plane textures, those that allow it, rather than copy - /// them in: on Metal, unless [`GpuOptions::plane_stores`] was off - /// (decision D52). Should the device fail to build the shader that + /// them in: on Metal, unless `GpuOptions::plane_stores` was off (decision + /// D52). Should the device fail to build the shader that /// stores, the decoders copy all the same, which /// [`Decoder::stores_planes`](super::Decoder::stores_planes) tells. pub fn stores_planes(&self) -> bool { @@ -399,7 +401,7 @@ impl GpuContext { /// Whether the decoders write a frame's data, and the encoders a /// picture's planes, where their shaders read them, without an upload /// buffer and the copies from it: on Metal on Apple silicon, unless - /// [`GpuOptions::shared_uploads`] was off (decisions D53, D54). + /// `GpuOptions::shared_uploads` was off (decisions D53, D54). pub fn shares_uploads(&self) -> bool { self.shared_uploads } @@ -581,7 +583,7 @@ impl QueueTurns { /// Takes a turn, waiting for it: for work of the caller's own that it /// records and submits to the queue alongside emberwave's, such as what - /// [`Decoder::encode_rgb`](super::Decoder::encode_rgb) records into the + /// [`Decoder::record_rgb`](super::Decoder::record_rgb) records into the /// caller's encoder. Hold it from the first command buffer to the /// submission, and not while waiting for the GPU. pub fn take(&self) -> QueueTurn<'_> { @@ -719,19 +721,21 @@ fn cached( pub(crate) fn test_context() -> Option<&'static GpuContext> { static CONTEXT: std::sync::OnceLock> = std::sync::OnceLock::new(); CONTEXT - .get_or_init(|| match GpuContext::new(&GpuOptions::default()) { - Ok(context) => Some(context), - Err(error) => { - let required = - std::env::var_os("EMBERWAVE_REQUIRE_GPU").is_some_and(|v| !v.is_empty()); - assert!( - !required, - "a GPU is required but none is available: {error}" - ); - eprintln!("no GPU available, skipping: {error}"); - None - } - }) + .get_or_init( + || match GpuContext::new(&GpuOptions::from_env().unwrap_or_default()) { + Ok(context) => Some(context), + Err(error) => { + let required = + std::env::var_os("EMBERWAVE_REQUIRE_GPU").is_some_and(|v| !v.is_empty()); + assert!( + !required, + "a GPU is required but none is available: {error}" + ); + eprintln!("no GPU available, skipping: {error}"); + None + } + }, + ) .as_ref() } diff --git a/crates/emberwave-core/src/gpu/decoder.rs b/crates/emberwave-core/src/gpu/decoder.rs index d46b5a5..2172947 100644 --- a/crates/emberwave-core/src/gpu/decoder.rs +++ b/crates/emberwave-core/src/gpu/decoder.rs @@ -37,6 +37,7 @@ const MIN_PAYLOAD_WORDS: usize = 16 * 1024; /// One decoded colour plane, 8 bits per sample, rows tightly packed. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct FramePlane { /// Width in samples. pub width: u32, @@ -51,6 +52,7 @@ pub struct FramePlane { /// Values are in the range of the stream: this is what the reference decoder /// writes to 8-bit UNORM storage, with no colour conversion applied. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct Frame { /// Chroma subsampling; chroma planes are half size in 4:2:0. pub chroma: ChromaSubsampling, @@ -60,6 +62,7 @@ pub struct Frame { /// A decoded frame converted to RGB. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct RgbFrame { /// Width in pixels. pub width: u32, @@ -71,6 +74,7 @@ pub struct RgbFrame { /// What [`Decoder::time_frame`] decodes to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum TimedOutput { /// 8-bit Y, Cb and Cr planes in GPU memory. Yuv, @@ -83,6 +87,7 @@ pub enum TimedOutput { /// Only available on devices that support them, and then only at the /// granularity of a pass: all five inverse transform levels are one pass. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub struct StageTimings { /// Coefficient decoding and dequantisation. pub dequant: Duration, @@ -101,7 +106,8 @@ impl StageTimings { } /// Where the time of one decode went. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] pub struct Timings { /// CPU time to assemble the packets and hand the data to the GPU. pub prepare: Duration, @@ -203,7 +209,7 @@ pub struct Decoder { payload_capacity_words: usize, block_offsets: wgpu::Buffer, /// Made on the first decode that waits for its frame, and made again - /// when the payload buffer grows; `encode_rgb`, which does not wait, + /// when the payload buffer grows; `record_rgb`, which does not wait, /// writes through the queue instead, as a browser has to. Never made /// where `shared_uploads` is. upload: Option, @@ -462,6 +468,7 @@ impl Decoder { /// /// Whatever was assembled before is discarded, so the same frame may be /// decoded repeatedly. + #[cfg(not(target_arch = "wasm32"))] pub fn decode(&mut self, frame: &[u8]) -> Result { self.assemble_complete(frame)?; self.decode_assembled() @@ -469,6 +476,7 @@ impl Decoder { /// Decodes one complete frame like [`decode`](Self::decode) and converts /// it to RGB on the GPU. + #[cfg(not(target_arch = "wasm32"))] pub fn decode_rgb(&mut self, frame: &[u8], color: &ColorParams) -> Result { self.assemble_complete(frame)?; self.decode_assembled_rgb(color) @@ -481,6 +489,7 @@ impl Decoder { /// The per-stage times are `Some` only if the context was created with /// [`GpuOptions::timestamps`](super::GpuOptions::timestamps) and the device /// supports them. + #[cfg(not(target_arch = "wasm32"))] pub fn time_frame(&mut self, frame: &[u8], output: TimedOutput) -> Result { let start = Instant::now(); self.assemble_complete(frame)?; @@ -502,7 +511,7 @@ impl Decoder { /// /// The frame's data goes to the GPU with the next submission on this /// decoder's queue, so record one frame per submission. - pub fn encode_rgb( + pub fn record_rgb( &mut self, frame: &[u8], color: &ColorParams, @@ -515,7 +524,7 @@ impl Decoder { Ok(()) } - /// The RGBA words that [`encode_rgb`](Self::encode_rgb) and + /// The RGBA words that [`record_rgb`](Self::record_rgb) and /// [`decode_rgb`](Self::decode_rgb) write, once one of them has run: /// `width * height` words, one per pixel, red in the low byte, alpha 255. /// After a decode copied into a BGRA texture they are BGRA words, blue in @@ -527,7 +536,7 @@ impl Decoder { } /// Records the decode of one complete frame and its conversion to RGB - /// into `encoder`, as [`encode_rgb`](Self::encode_rgb) does, and then + /// into `encoder`, as [`record_rgb`](Self::record_rgb) does, and then /// what puts the picture into `target`. Once the caller has submitted /// `encoder`, the texture holds the picture. /// @@ -546,7 +555,7 @@ impl Decoder { /// draw needs the view in the texture's own format, which cannot be /// checked: a view in another format the texture allows, such as its sRGB /// form, fails wgpu's validation when the picture is drawn. - pub fn encode_rgb_to_texture( + pub fn record_rgb_to_texture( &mut self, frame: &[u8], color: &ColorParams, @@ -563,11 +572,12 @@ impl Decoder { /// Decodes the frame assembled from [`push_packet`](Self::push_packet) /// calls, converts it to RGB and draws it into `target`, as - /// [`encode_rgb_to_texture`](Self::encode_rgb_to_texture) does, and waits + /// [`record_rgb_to_texture`](Self::record_rgb_to_texture) does, and waits /// for the GPU. When this returns the texture holds the picture, for any /// queue of the device to read; a texture another queue may still be /// reading must not be passed. Blocks that did not arrive decode as zeros, /// or are concealed ([`set_concealment`](Self::set_concealment)). + #[cfg(not(target_arch = "wasm32"))] pub fn decode_assembled_to_texture( &mut self, color: &ColorParams, @@ -617,7 +627,7 @@ impl Decoder { self.submit_assembled(Some(color), Read::Texture(target), Some(hooks)) } - /// A texture that [`encode_rgb_to_texture`](Self::encode_rgb_to_texture) + /// A texture that [`record_rgb_to_texture`](Self::record_rgb_to_texture) /// and [`decode_assembled_to_texture`](Self::decode_assembled_to_texture) /// can put the picture into: the picture's size, in `format`, one of /// [`TEXTURE_FORMATS`], for drawing and copying into, sampling and copying @@ -641,7 +651,7 @@ impl Decoder { } /// Records the decode of one complete frame into `encoder`, as - /// [`encode_rgb_to_texture`](Self::encode_rgb_to_texture) does but + /// [`record_rgb_to_texture`](Self::record_rgb_to_texture) does but /// without the colour conversion, with its Y, Cb and Cr planes going /// into `targets`, in that order. Once the caller has submitted /// `encoder`, the textures hold the planes: the samples of the stream, as @@ -658,7 +668,7 @@ impl Decoder { /// the `STORAGE_BINDING` usage, the inverse transform stores the planes /// into the views, which must then allow that usage. Otherwise the planes /// are copied into the textures. - pub fn encode_to_plane_textures( + pub fn record_to_plane_textures( &mut self, frame: &[u8], targets: [&wgpu::TextureView; 3], @@ -674,11 +684,12 @@ impl Decoder { /// Decodes the frame assembled from [`push_packet`](Self::push_packet) /// calls into its planes in `targets`, as - /// [`encode_to_plane_textures`](Self::encode_to_plane_textures) does, and + /// [`record_to_plane_textures`](Self::record_to_plane_textures) does, and /// waits for the GPU. When this returns the textures hold the planes, for /// any queue of the device to read; textures another queue may still be /// reading must not be passed. Blocks that did not arrive decode as zeros, /// or are concealed ([`set_concealment`](Self::set_concealment)). + #[cfg(not(target_arch = "wasm32"))] pub fn decode_assembled_to_plane_textures( &mut self, targets: [&wgpu::TextureView; 3], @@ -721,7 +732,7 @@ impl Decoder { self.submit_assembled(None, Read::Planes(targets), Some(hooks)) } - /// A texture that [`encode_to_plane_textures`](Self::encode_to_plane_textures) + /// A texture that [`record_to_plane_textures`](Self::record_to_plane_textures) /// and [`decode_assembled_to_plane_textures`](Self::decode_assembled_to_plane_textures) /// can put the plane of `component` into: the plane's size, in /// [`PLANE_FORMAT`], for copying into, sampling and copying from @@ -813,14 +824,18 @@ impl Decoder { /// to take a frame that lost packets: 0.9 by default, as upstream's /// decoder has it. A lower share takes frames that lost more, with more /// of their detail blank, where a client would skip them; 1 takes whole - /// frames only. Below 0 counts as 0 and above 1 as 1; NaN sets the - /// default again. The share stays until it is set again. - pub fn set_partial_share(&mut self, share: f32) { - self.partial_share = if share.is_nan() { - DEFAULT_MINIMUM_PACKET_RATIO - } else { - share.clamp(0.0, 1.0) - }; + /// frames only. A share below 0 or above 1, or NaN, is + /// [`GpuError::InvalidArgument`], as in the C API and the browser + /// player, and leaves the share as it was. The share stays until it is + /// set again. + pub fn set_partial_share(&mut self, share: f32) -> Result<(), GpuError> { + if !(0.0..=1.0).contains(&share) { + return Err(GpuError::InvalidArgument(format!( + "the share is {share}, not a number from 0 to 1" + ))); + } + self.partial_share = share; + Ok(()) } /// The share of [`set_partial_share`](Self::set_partial_share). @@ -961,6 +976,7 @@ impl Decoder { /// Decodes the frame assembled from [`push_packet`](Self::push_packet) /// calls. Blocks that did not arrive decode as zeros, or are concealed /// ([`set_concealment`](Self::set_concealment)). + #[cfg(not(target_arch = "wasm32"))] pub fn decode_assembled(&mut self) -> Result { let layout = self.layout(); let chroma = layout.chroma(); @@ -981,6 +997,7 @@ impl Decoder { } /// Like [`decode_assembled`](Self::decode_assembled), converted to RGB. + #[cfg(not(target_arch = "wasm32"))] pub fn decode_assembled_rgb(&mut self, color: &ColorParams) -> Result { let (width, height) = (self.layout().width(), self.layout().height()); let mut data = vec![0; width as usize * height as usize * 3]; @@ -1002,6 +1019,7 @@ impl Decoder { /// the memory the GPU wrote, so a caller can put them where it wants /// them, with its own row strides, at the cost of one copy. A panic in /// `row` goes on to the caller, and the decoder can still be used. + #[cfg(not(target_arch = "wasm32"))] pub fn decode_assembled_rows( &mut self, mut row: impl FnMut(Component, u32, &[u8]), @@ -1020,6 +1038,7 @@ impl Decoder { /// Like [`decode_assembled_rows`](Self::decode_assembled_rows), converted /// to RGB: `row` gets the row's index from the top and its pixels, three /// bytes each with red first. + #[cfg(not(target_arch = "wasm32"))] pub fn decode_assembled_rgb_rows( &mut self, color: &ColorParams, @@ -1718,6 +1737,8 @@ impl Decoder { /// results of the inverse transform, and the other bands hold the /// dequantised coefficients. Returns `None` if the component has no such /// level. + #[cfg(not(target_arch = "wasm32"))] + #[doc(hidden)] pub fn read_band( &self, component: Component, @@ -1914,7 +1935,7 @@ impl Decoder { } /// Uploads the block table and the payload of the assembled frame through - /// the queue, for `encode_rgb`, which does not wait for its frame and so + /// the queue, for `record_rgb`, which does not wait for its frame and so /// cannot map the upload buffer. fn upload(&self) { let frame = self.assembler.frame(); @@ -2397,7 +2418,8 @@ mod tests { if test_context().is_none() { return; } - let context = GpuContext::new(&crate::gpu::GpuOptions::default()).expect("a context"); + let context = GpuContext::new(&crate::gpu::GpuOptions::from_env().unwrap_or_default()) + .expect("a context"); let chroma = ChromaSubsampling::Yuv420; let mut decoder = Decoder::new(&context, 64, 32, chroma).unwrap(); decoder diff --git a/crates/emberwave-core/src/gpu/encoder.rs b/crates/emberwave-core/src/gpu/encoder.rs index c5fd7b6..2191592 100644 --- a/crates/emberwave-core/src/gpu/encoder.rs +++ b/crates/emberwave-core/src/gpu/encoder.rs @@ -52,6 +52,7 @@ struct FdwtStage { /// GPU time of each stage of an encode, measured with timestamp queries at /// the boundaries of its compute passes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub struct EncodeStageTimings { /// The forward transform, all levels, and for a picture in textures /// the packing of its samples before. @@ -73,6 +74,7 @@ impl EncodeStageTimings { /// Where the time of one encode went. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub struct EncodeTimings { /// CPU time to check the planes and put them where wgpu uploads them /// from, and to prepare the GPU work. @@ -96,6 +98,7 @@ pub struct EncodeTimings { /// another mip level goes unnoticed. So does, at times, a texture of another /// device, which wgpu may take for one of this device's. #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum TexturePicture<'a> { /// Y, Cb and Cr, each in an `R8Unorm` texture. Planes([&'a wgpu::TextureView; 3]), @@ -599,6 +602,7 @@ impl Encoder { /// the sizes of [`FrameLayout::plane_size`]. A picture that cannot fit /// the budget is [`GpuError::Encode`]. It waits for the GPU, so it cannot /// run in a browser, where nothing may block. + #[cfg(not(target_arch = "wasm32"))] pub fn encode(&mut self, planes: [&[u8]; 3], budget: usize) -> Result, GpuError> { self.run_encode(Input::Planes(planes, None), budget, false, None) .map(|(frame, _)| frame) @@ -610,6 +614,7 @@ impl Encoder { /// bytes at the sizes of [`FrameLayout::plane_size`], and the bytes /// between the rows are not read. A stride below the width is /// [`EncodeError::PlaneStride`]. + #[cfg(not(target_arch = "wasm32"))] pub fn encode_strided( &mut self, planes: [&[u8]; 3], @@ -624,6 +629,7 @@ impl Encoder { /// for benchmarks. The per-stage times are `Some` only if the context was /// created with [`GpuOptions::timestamps`](super::GpuOptions::timestamps) /// and the device supports them. + #[cfg(not(target_arch = "wasm32"))] pub fn encode_timed( &mut self, planes: [&[u8]; 3], @@ -639,6 +645,7 @@ impl Encoder { /// /// The GPU reads the textures after the work submitted to the queue /// before, and is done with them when this returns. + #[cfg(not(target_arch = "wasm32"))] pub fn encode_textures( &mut self, picture: TexturePicture<'_>, @@ -652,6 +659,7 @@ impl Encoder { /// the submission: for what the submission must carry for the caller's /// graphics API, such as a Vulkan semaphore to wait on before the /// textures are read (decision D62). + #[cfg(not(target_arch = "wasm32"))] pub fn encode_textures_with( &mut self, picture: TexturePicture<'_>, @@ -664,6 +672,7 @@ impl Encoder { /// Encodes like [`encode_textures`](Self::encode_textures) and says /// where the time went, as [`encode_timed`](Self::encode_timed) does. + #[cfg(not(target_arch = "wasm32"))] pub fn encode_textures_timed( &mut self, picture: TexturePicture<'_>, @@ -678,6 +687,7 @@ impl Encoder { /// keeps, and is converted there as [`TexturePicture::Rgb`] says, into /// the frame of the same picture in a texture (decision D65). A picture /// smaller than its size and stride say is [`EncodeError::Picture`]. + #[cfg(not(target_arch = "wasm32"))] pub fn encode_rgb( &mut self, rgb: &[u8], @@ -692,6 +702,7 @@ impl Encoder { /// Encodes like [`encode_rgb`](Self::encode_rgb) and says where the /// time went, as [`encode_timed`](Self::encode_timed) does: the upload /// of the picture into the encoder's texture counts as preparation. + #[cfg(not(target_arch = "wasm32"))] pub fn encode_rgb_timed( &mut self, rgb: &[u8], diff --git a/crates/emberwave-core/src/gpu/error.rs b/crates/emberwave-core/src/gpu/error.rs index 3a889a3..5c92448 100644 --- a/crates/emberwave-core/src/gpu/error.rs +++ b/crates/emberwave-core/src/gpu/error.rs @@ -5,6 +5,7 @@ use thiserror::Error; /// Everything that can go wrong while setting up the GPU or decoding or /// encoding on it. #[derive(Debug, Error)] +#[non_exhaustive] pub enum GpuError { /// No adapter matched the requested backends. #[error("no suitable GPU adapter found: {0}")] @@ -29,6 +30,10 @@ pub enum GpuError { #[error("the frame is too large for 32-bit buffer offsets")] TooLarge, + /// A value the call cannot take, such as a share outside 0 to 1. + #[error("{0}")] + InvalidArgument(String), + /// The GPU or the driver reported an error. #[error("the GPU reported an error: {0}")] Device(String), diff --git a/crates/emberwave-core/src/gpu/mod.rs b/crates/emberwave-core/src/gpu/mod.rs index 60a251c..7d0792d 100644 --- a/crates/emberwave-core/src/gpu/mod.rs +++ b/crates/emberwave-core/src/gpu/mod.rs @@ -7,6 +7,14 @@ //! inverse transform, which stores the planes into 8-bit textures, runs only //! where the adapter allows it, and the portable one everywhere else //! (decision D52). +//! +//! In a browser nothing may wait for the GPU, so the calls that do, such as +//! [`Decoder::decode`] and the [`Encoder`]'s encodes, are not built for +//! wasm32; the decoder's `record_*` and `submit_*` calls are. + +// The calls left out of the browser build leave their helpers unused there; +// the native builds check for code that is dead everywhere. +#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] mod context; mod decoder; @@ -40,6 +48,7 @@ const _: () = { }; /// The WGSL sources of the pipeline stages, for offline validation. +#[doc(hidden)] pub mod shader_sources { /// Coefficient decoding and dequantisation. pub const DEQUANT: &str = include_str!("shaders/dequant.wgsl"); diff --git a/crates/emberwave-core/src/gpu/shaders/draw.wgsl b/crates/emberwave-core/src/gpu/shaders/draw.wgsl index 33ef0c0..be57b8e 100644 --- a/crates/emberwave-core/src/gpu/shaders/draw.wgsl +++ b/crates/emberwave-core/src/gpu/shaders/draw.wgsl @@ -10,7 +10,7 @@ // are packed tightly, as the read-back to the CPU takes them, and a texture // without the COPY_DST usage takes none. It also fills BGRA targets. The // browser player and the C API put pictures into textures through -// Decoder::encode_rgb_to_texture (decision D40). +// Decoder::record_rgb_to_texture (decision D40). struct Picture { width: u32, diff --git a/crates/emberwave-core/src/lib.rs b/crates/emberwave-core/src/lib.rs index 2984cab..cb004b1 100644 --- a/crates/emberwave-core/src/lib.rs +++ b/crates/emberwave-core/src/lib.rs @@ -9,6 +9,18 @@ //! encoder. //! //! See `ARCHITECTURE.md` for the design. +//! +//! Items hidden from this documentation are there for emberwave's own tests +//! and tools: they are not part of the API, and any release may change them. +//! With the `gpu` feature, [`wgpu`] is the version emberwave is built with, +//! whose types the GPU calls take: use it to share a device or a texture. + +// The docs link to the calls that wait for the GPU, which the browser build +// leaves out (decision D70). +#![cfg_attr(target_arch = "wasm32", allow(rustdoc::broken_intra_doc_links))] + +#[cfg(feature = "gpu")] +pub use wgpu; pub mod bitstream; pub mod color; diff --git a/crates/emberwave-core/tests/common/mod.rs b/crates/emberwave-core/tests/common/mod.rs index f64c1c4..4275496 100644 --- a/crates/emberwave-core/tests/common/mod.rs +++ b/crates/emberwave-core/tests/common/mod.rs @@ -44,15 +44,21 @@ pub fn live_resources(context: &emberwave_core::gpu::GpuContext) -> String { format!("{:?}", context.device().get_internal_counters().hal) } +/// The options the tests start from: the defaults, with the backends that +/// `EMBERWAVE_BACKEND` names, so that a run can force a backend, as the +/// command line tool does. A value that names none is ignored. +#[cfg(feature = "gpu")] +pub fn options() -> emberwave_core::gpu::GpuOptions { + emberwave_core::gpu::GpuOptions::from_env().unwrap_or_default() +} + /// The GPU context shared by the tests of one test binary, or `None` if /// there is no GPU and none is required. #[cfg(feature = "gpu")] pub fn shared_context() -> Option<&'static emberwave_core::gpu::GpuContext> { static CONTEXT: std::sync::OnceLock> = std::sync::OnceLock::new(); - CONTEXT - .get_or_init(|| create_context(&emberwave_core::gpu::GpuOptions::default())) - .as_ref() + CONTEXT.get_or_init(|| create_context(&options())).as_ref() } /// Directory with the committed vectors and their reference decodes. diff --git a/crates/emberwave-core/tests/gpu.rs b/crates/emberwave-core/tests/gpu.rs index 57f8d35..e8e94c7 100644 --- a/crates/emberwave-core/tests/gpu.rs +++ b/crates/emberwave-core/tests/gpu.rs @@ -21,9 +21,7 @@ use emberwave_core::bitstream::{ }; use emberwave_core::color::{ColorMatrix, ColorParams, yuv_to_rgb8}; use emberwave_core::cpu; -use emberwave_core::gpu::{ - Decoder, Frame, GpuContext, GpuError, GpuOptions, TEXTURE_FORMATS, TimedOutput, -}; +use emberwave_core::gpu::{Decoder, Frame, GpuContext, GpuError, TEXTURE_FORMATS, TimedOutput}; /// Skips the current test when there is no GPU. macro_rules! gpu_or_return { @@ -410,7 +408,7 @@ fn a_picture_drawn_into_a_texture_is_the_rgb_picture() { view_formats: &[], }); let mut encoder = context.device().create_command_encoder(&Default::default()); - let outcome = decoder.encode_rgb_to_texture( + let outcome = decoder.record_rgb_to_texture( &frames[1], &color, &target.create_view(&Default::default()), @@ -483,7 +481,7 @@ fn only_a_texture_the_picture_fits_takes_it() { // The frame is still there to decode. assert_eq!(decoder.decode_assembled().expect("decode"), frame); let mut encoder = context.device().create_command_encoder(&Default::default()); - let outcome = decoder.encode_rgb_to_texture(&frames[0], &color, &view, &mut encoder); + let outcome = decoder.record_rgb_to_texture(&frames[0], &color, &view, &mut encoder); assert!(matches!(outcome, Err(GpuError::Texture(_))), "{outcome:?}"); } } @@ -547,7 +545,7 @@ fn planes_drawn_into_textures_are_the_decoded_planes() { .map(|texture| texture.create_view(&Default::default())); let mut encoder = context.device().create_command_encoder(&Default::default()); decoder - .encode_to_plane_textures(&frames[1], views.each_ref(), &mut encoder) + .record_to_plane_textures(&frames[1], views.each_ref(), &mut encoder) .expect("recorded decode into plane textures"); context.queue().submit([encoder.finish()]); for (component, texture) in Component::ALL.into_iter().zip(&textures) { @@ -749,7 +747,7 @@ fn planes_stored_into_textures_are_the_copied_planes() { } else { let mut encoder = context.device().create_command_encoder(&Default::default()); decoder - .encode_to_plane_textures(frame, targets, &mut encoder) + .record_to_plane_textures(frame, targets, &mut encoder) .expect("recorded decode into plane textures"); context.queue().submit([encoder.finish()]); } @@ -776,9 +774,10 @@ fn frames_written_in_place_decode_as_copied_ones() { let shares = info.backend == wgpu::Backend::Metal && info.device_type == wgpu::DeviceType::IntegratedGpu; assert_eq!(context.shares_uploads(), shares); - let options = GpuOptions { - shared_uploads: false, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.shared_uploads = false; + options }; let Some(copied) = create_context(&options) else { return; @@ -807,7 +806,7 @@ fn frames_written_in_place_decode_as_copied_ones() { ); let mut encoder = context.device().create_command_encoder(&Default::default()); shared - .encode_rgb(frame, &color, &mut encoder) + .record_rgb(frame, &color, &mut encoder) .expect("recorded decode"); context.queue().submit([encoder.finish()]); } @@ -822,9 +821,10 @@ fn frames_written_in_place_wait_for_the_frame_before() { // frames are large enough to keep the GPU busy, and the first one grows // the payload buffer. let context = gpu_or_return!(); - let options = GpuOptions { - shared_uploads: false, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.shared_uploads = false; + options }; let Some(copied) = create_context(&options) else { return; @@ -842,7 +842,7 @@ fn frames_written_in_place_wait_for_the_frame_before() { let (first, second) = (&pair[0], &pair[1]); let mut encoder = device.create_command_encoder(&Default::default()); decoder - .encode_rgb(first, &color, &mut encoder) + .record_rgb(first, &color, &mut encoder) .expect("recorded decode"); let rgba = decoder.rgba_buffer().expect("RGBA buffer"); let readback = device.create_buffer(&wgpu::BufferDescriptor { @@ -878,9 +878,10 @@ fn frames_written_in_place_wait_for_the_frame_before() { #[test] fn without_plane_stores_the_planes_are_copied() { - let options = GpuOptions { - plane_stores: false, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.plane_stores = false; + options }; let Some(context) = create_context(&options) else { return; @@ -920,7 +921,7 @@ fn decoding_into_plane_textures_frame_after_frame_leaves_nothing_behind() { // A context of its own: the counters belong to the device. Where the // planes are stored, every decode makes the bind groups of its textures, // which must go again. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let (layout, frames) = vector_frames("noise_192x136_420"); @@ -1002,7 +1003,7 @@ fn only_textures_the_planes_fit_take_them() { // The frame is still there to decode. assert_eq!(decoder.decode_assembled().expect("decode"), frame); let mut encoder = context.device().create_command_encoder(&Default::default()); - let outcome = decoder.encode_to_plane_textures(&frames[0], views.each_ref(), &mut encoder); + let outcome = decoder.record_to_plane_textures(&frames[0], views.each_ref(), &mut encoder); assert!(matches!(outcome, Err(GpuError::Texture(_))), "{outcome:?}"); } @@ -1574,7 +1575,7 @@ fn a_partial_frame_needs_the_share_of_blocks_its_decoder_asks() { match Header::parse(&packet[at..]) { Ok(Header::StartOfFrame(_)) => at += 8, Ok(Header::Block(block)) => break Some(block.block_index), - Err(_) => break None, + Ok(_) | Err(_) => break None, } }; if first_block.is_some_and(|block| block >= coarsest) && number % 2 == 1 { @@ -1588,22 +1589,26 @@ fn a_partial_frame_needs_the_share_of_blocks_its_decoder_asks() { assert_eq!(decoder.partial_share(), 0.9); assert!(!decoder.is_ready(true), "90 % by default"); - decoder.set_partial_share(share - 0.05); + decoder.set_partial_share(share - 0.05).unwrap(); assert!(decoder.is_ready(true)); assert!(!decoder.is_ready(false)); // A share above what arrived; the edge itself, where exactly the share // is not enough, is the assembler's test. - decoder.set_partial_share(share + 0.01); + decoder.set_partial_share(share + 0.01).unwrap(); assert!(!decoder.is_ready(true)); - decoder.set_partial_share(1.0); + decoder.set_partial_share(1.0).unwrap(); assert!(!decoder.is_ready(true), "1 takes whole frames only"); - decoder.set_partial_share(-1.0); - assert_eq!(decoder.partial_share(), 0.0); - decoder.set_partial_share(f32::NAN); - assert_eq!(decoder.partial_share(), 0.9); + // Outside 0 to 1, or NaN: refused, and the share stays. + for refused in [-1.0, 1.01, f32::NAN, f32::INFINITY] { + assert!(matches!( + decoder.set_partial_share(refused), + Err(GpuError::InvalidArgument(_)) + )); + assert_eq!(decoder.partial_share(), 1.0); + } // What arrived decodes, as it always could. - decoder.set_partial_share(0.0); + decoder.set_partial_share(0.0).unwrap(); assert!(decoder.is_ready(true)); let frame = decoder.decode_assembled().unwrap(); assert_eq!(frame.planes[0].data.len(), 200 * 120); @@ -1614,9 +1619,10 @@ fn a_device_limited_to_webgpu_guarantees_decodes_the_same() { // The portable subset: default limits and no optional features. If the // shaders needed anything beyond it, creating the pipelines or running // them would fail here. - let options = GpuOptions { - strict_webgpu: true, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.strict_webgpu = true; + options }; let Some(context) = create_context(&options) else { return; @@ -1877,11 +1883,7 @@ fn rgb_conversion_on_the_gpu_matches_the_cpu() { for matrix in matrices { for range in ranges { for chroma_siting in sitings { - let params = ColorParams { - matrix, - range, - chroma_siting, - }; + let params = ColorParams::new(matrix, range, chroma_siting); let rgb = decoder .decode_rgb(&frames[0], ¶ms) .expect("GPU RGB decode"); @@ -1931,11 +1933,7 @@ fn rgb_output_changes_with_the_conversion_settings() { // The parameters are read for every call: the same frame converted twice // with different settings must differ, and back again must repeat. let a = ColorParams::default(); - let b = ColorParams { - matrix: ColorMatrix::Bt601, - range: YcbcrRange::Full, - chroma_siting: ChromaSiting::Left, - }; + let b = ColorParams::new(ColorMatrix::Bt601, YcbcrRange::Full, ChromaSiting::Left); let first = decoder.decode_rgb(&frames[0], &a).unwrap(); let other = decoder.decode_rgb(&frames[0], &b).unwrap(); assert_ne!(first, other); @@ -1949,9 +1947,10 @@ fn rgb_output_changes_with_the_conversion_settings() { #[test] fn timing_a_frame_reports_where_the_time_goes() { // Timestamp queries are optional, so ask for them and accept their absence. - let options = GpuOptions { - timestamps: true, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.timestamps = true; + options }; let Some(context) = create_context(&options) else { return; @@ -2038,9 +2037,10 @@ fn resources_around( fn decoding_frame_after_frame_allocates_nothing() { // A context of its own: the counters belong to the device, and the shared // context is busy with the other tests. - let options = GpuOptions { - timestamps: true, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.timestamps = true; + options }; let Some(context) = create_context(&options) else { return; @@ -2055,7 +2055,7 @@ fn decoding_frame_after_frame_allocates_nothing() { #[test] fn a_decoder_makes_its_read_back_buffers_when_a_decode_first_reads_back() { // A context of its own: the counters belong to the device. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let buffers = || context.device().get_internal_counters().hal.buffers.read(); @@ -2067,7 +2067,7 @@ fn a_decoder_makes_its_read_back_buffers_when_a_decode_first_reads_back() { // reading back. The 4K decoder of a browser saves 43.5 MiB this way. let mut encoder = context.device().create_command_encoder(&Default::default()); decoder - .encode_rgb(&frames[0], &color, &mut encoder) + .record_rgb(&frames[0], &color, &mut encoder) .expect("recorded decode"); context.queue().submit([encoder.finish()]); decoder @@ -2088,7 +2088,7 @@ fn a_decoder_makes_its_read_back_buffers_when_a_decode_first_reads_back() { #[test] fn a_lost_device_is_an_error_rather_than_a_stale_result() { // A context of its own, since destroying the device loses it for good. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let (layout, frames) = vector_frames("gradient_200x120_420"); @@ -2149,9 +2149,10 @@ fn a_thousand_4k_frames_leave_no_trace() { let bytes = std::fs::read(&path).unwrap_or_else(|error| { panic!("{}: {error}; run scripts/gen_bench_data.sh", path.display()) }); - let options = GpuOptions { - timestamps: true, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.timestamps = true; + options }; let context = create_context(&options).expect("this test needs a GPU"); let (layout, frames) = frames_of("scene_4k_420", &bytes); @@ -2181,8 +2182,8 @@ fn encoding_into_a_callers_encoder_gives_the_same_picture() { assert!(other.rgba_buffer().is_none()); let device = context.device(); let mut encoder = device.create_command_encoder(&Default::default()); - other.encode_rgb(&frames[1], &color, &mut encoder).unwrap(); - let rgba = other.rgba_buffer().expect("RGBA buffer after encode_rgb"); + other.record_rgb(&frames[1], &color, &mut encoder).unwrap(); + let rgba = other.rgba_buffer().expect("RGBA buffer after record_rgb"); let readback = device.create_buffer(&wgpu::BufferDescriptor { label: Some("test readback"), size: rgba.size(), @@ -2206,7 +2207,7 @@ fn encoding_into_a_callers_encoder_gives_the_same_picture() { assert_eq!(rgb, expected.data); // The same decoder, back to the decodes that wait, which upload through a - // buffer of their own (decision D34) where encode_rgb writes through the + // buffer of their own (decision D34) where record_rgb writes through the // queue: each path finds the payload the other left and replaces it. for frame in [&frames[0], &frames[1]] { let from_both = other.decode_rgb(frame, &color).unwrap(); diff --git a/crates/emberwave-core/tests/gpu_encoder.rs b/crates/emberwave-core/tests/gpu_encoder.rs index 32fdad7..ee7ba16 100644 --- a/crates/emberwave-core/tests/gpu_encoder.rs +++ b/crates/emberwave-core/tests/gpu_encoder.rs @@ -21,7 +21,7 @@ use emberwave_core::bitstream::{ use emberwave_core::color::{ColorError, RgbOrder, rgb_to_ycbcr8, yuv_to_rgb8}; use emberwave_core::cpu::decode_frame; use emberwave_core::encode::{self, EncodeError, Signalling}; -use emberwave_core::gpu::{Decoder, Encoder, GpuContext, GpuError, GpuOptions, TexturePicture}; +use emberwave_core::gpu::{Decoder, Encoder, GpuContext, GpuError, TexturePicture}; /// Skips the current test when there is no GPU. macro_rules! gpu_or_return { @@ -324,9 +324,10 @@ fn errors_are_the_cpu_encoders() { #[test] fn a_device_limited_to_webgpu_guarantees_encodes_the_same() { let context = gpu_or_return!(); - let options = GpuOptions { - strict_webgpu: true, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.strict_webgpu = true; + options }; let Some(strict) = create_context(&options) else { return; @@ -345,7 +346,7 @@ fn a_device_limited_to_webgpu_guarantees_encodes_the_same() { #[test] fn encoding_frame_after_frame_allocates_nothing() { // A context of its own: the counters belong to the device. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let vector = vectors() @@ -381,7 +382,7 @@ fn encoding_frame_after_frame_allocates_nothing() { #[test] fn a_lost_device_is_an_error_rather_than_a_stale_result() { // A context of its own, since destroying the device loses it for good. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let vector = vectors() @@ -404,9 +405,10 @@ fn a_lost_device_is_an_error_rather_than_a_stale_result() { #[test] fn timing_an_encode_reports_where_the_time_goes() { // Timestamp queries are optional, so ask for them and accept their absence. - let options = GpuOptions { - timestamps: true, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.timestamps = true; + options }; let Some(context) = create_context(&options) else { return; @@ -454,7 +456,7 @@ fn nothing_is_made_on_a_lost_device() { // the context compiled its pipelines, whose failure is then the first. for compiled in [false, true] { let lost = || { - let context = create_context(&GpuOptions::default())?; + let context = create_context(&common::options())?; if compiled { Decoder::new(&context, 64, 64, ChromaSubsampling::Yuv420).expect("a decoder"); Encoder::new( @@ -501,9 +503,10 @@ fn pictures_in_textures_encode_as_the_same_planes_do() { // limited to what WebGPU guarantees (decision D32). let context = gpu_or_return!(); let mut contexts = vec![(context.clone(), usize::MAX)]; - let strict = GpuOptions { - strict_webgpu: true, - ..GpuOptions::default() + let strict = { + let mut options = common::options(); + options.strict_webgpu = true; + options }; if let Some(strict) = create_context(&strict) { contexts.push((strict, 3)); @@ -541,9 +544,10 @@ fn planes_written_in_place_encode_as_uploaded_ones() { // upload buffer (decision D54). One encoder takes every kind of picture // in turn, and its frames are those of an encoder on the upload path. let context = gpu_or_return!(); - let options = GpuOptions { - shared_uploads: false, - ..GpuOptions::default() + let options = { + let mut options = common::options(); + options.shared_uploads = false; + options }; let Some(copied) = create_context(&options) else { return; @@ -739,7 +743,7 @@ fn textures_that_cannot_hold_their_planes_are_errors() { fn encoding_from_textures_frame_after_frame_allocates_nothing() { // A context of its own: the counters belong to the device. The bind // group of each frame's textures goes when the frame is done. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let vector = vectors() @@ -797,6 +801,7 @@ fn rgb_of(layout: &FrameLayout, source: &[Vec; 3], order: RgbOrder, stride: let ordered = match order { RgbOrder::Rgba => [r, g, b, fourth], RgbOrder::Bgra => [b, g, r, fourth], + other => panic!("{other:?}"), }; bytes[y * stride + 4 * x..][..4].copy_from_slice(&ordered); } @@ -811,9 +816,10 @@ fn pictures_in_rgb_encode_as_the_planes_they_convert_to_do() { // a device limited to what WebGPU guarantees (decision D65). let context = gpu_or_return!(); let mut contexts = vec![(context.clone(), usize::MAX)]; - let strict = GpuOptions { - strict_webgpu: true, - ..GpuOptions::default() + let strict = { + let mut options = common::options(); + options.strict_webgpu = true; + options }; if let Some(strict) = create_context(&strict) { contexts.push((strict, 2)); @@ -968,7 +974,7 @@ fn rgb_pictures_that_do_not_fit_are_errors() { fn encoding_rgb_frame_after_frame_allocates_nothing() { // A context of its own, as for textures: the texture that RGB in CPU // memory goes into is kept, and made again only for the other order. - let Some(context) = create_context(&GpuOptions::default()) else { + let Some(context) = create_context(&common::options()) else { return; }; let vector = vectors() diff --git a/crates/emberwave-ffi/src/lib.rs b/crates/emberwave-ffi/src/lib.rs index 0256e21..c0e28a3 100644 --- a/crates/emberwave-ffi/src/lib.rs +++ b/crates/emberwave-ffi/src/lib.rs @@ -693,7 +693,11 @@ impl From for Failure { 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, + GpuError::Encode(_) | GpuError::Texture(_) | GpuError::InvalidArgument(_) => { + EmberwaveStatus::InvalidArgument + } + // A failure this version does not know of yet. + _ => EmberwaveStatus::Internal, }; Self(status, error.to_string()) } @@ -1038,13 +1042,14 @@ fn creation_failure(error: GpuError) -> Failure { } fn signalling(value: &EmberwaveSignalling) -> Result { - Ok(Signalling { - color_primaries: match value.primaries { + Ok({ + let mut signalling = Signalling::default(); + signalling.color_primaries = match value.primaries { EMBERWAVE_PRIMARIES_SRGB => ColorPrimaries::Srgb, EMBERWAVE_PRIMARIES_BT2020 => ColorPrimaries::Bt2020, other => return Err(Failure::argument(format!("unknown primaries {other}"))), - }, - transfer_function: match value.transfer { + }; + signalling.transfer_function = match value.transfer { EMBERWAVE_TRANSFER_SRGB => TransferFunction::Srgb, EMBERWAVE_TRANSFER_PQ => TransferFunction::Pq, other => { @@ -1052,8 +1057,8 @@ fn signalling(value: &EmberwaveSignalling) -> Result { "unknown transfer function {other}" ))); } - }, - ycbcr_transform: match value.matrix { + }; + signalling.ycbcr_transform = match value.matrix { EMBERWAVE_MATRIX_BT709 => YcbcrTransform::Bt709, EMBERWAVE_MATRIX_BT2020 => YcbcrTransform::Bt2020, EMBERWAVE_MATRIX_BT601 => { @@ -1062,17 +1067,18 @@ fn signalling(value: &EmberwaveSignalling) -> Result { )); } other => return Err(Failure::argument(format!("unknown matrix {other}"))), - }, - ycbcr_range: match value.range { + }; + signalling.ycbcr_range = match value.range { EMBERWAVE_RANGE_LIMITED => YcbcrRange::Limited, EMBERWAVE_RANGE_FULL => YcbcrRange::Full, other => return Err(Failure::argument(format!("unknown range {other}"))), - }, - chroma_siting: match value.chroma_siting { + }; + signalling.chroma_siting = match value.chroma_siting { EMBERWAVE_SITING_CENTER => ChromaSiting::Center, EMBERWAVE_SITING_LEFT => ChromaSiting::Left, other => return Err(Failure::argument(format!("unknown chroma siting {other}"))), - }, + }; + signalling }) } @@ -1125,9 +1131,9 @@ fn chroma_code(chroma: ChromaSubsampling) -> u32 { } } -fn stream_info(stream: &SequenceHeader) -> EmberwaveStreamInfo { +fn stream_info(stream: &SequenceHeader) -> Result { let color = ColorParams::from_stream(stream); - EmberwaveStreamInfo { + Ok(EmberwaveStreamInfo { struct_size: std::mem::size_of::() as u32, width: stream.width, height: stream.height, @@ -1136,6 +1142,16 @@ fn stream_info(stream: &SequenceHeader) -> EmberwaveStreamInfo { ColorMatrix::Bt601 => EMBERWAVE_MATRIX_BT601, ColorMatrix::Bt709 => EMBERWAVE_MATRIX_BT709, ColorMatrix::Bt2020 => EMBERWAVE_MATRIX_BT2020, + // A stream signals BT.709 or BT.2020, one bit: a matrix the C API + // has no constant for cannot come from one. Should a later + // version of the core add one without a constant here, the call + // fails as a bug of emberwave's, not with a wrong matrix. + other => { + return Err(Failure( + EmberwaveStatus::Internal, + format!("the stream's matrix {other:?} has no C constant"), + )); + } }, range: match color.range { YcbcrRange::Limited => EMBERWAVE_RANGE_LIMITED, @@ -1147,7 +1163,7 @@ fn stream_info(stream: &SequenceHeader) -> EmberwaveStreamInfo { }, frame_rate_num: 0, frame_rate_den: 0, - } + }) } fn gpu_options(options: &EmberwaveContextOptions) -> Result { @@ -1178,36 +1194,32 @@ fn gpu_options(options: &EmberwaveContextOptions) -> Result if backends.is_empty() { backends = wgpu::Backends::PRIMARY; } - Ok(GpuOptions { - backends, - power_preference: wgpu::PowerPreference::HighPerformance, - force_fallback_adapter: options.flags & EMBERWAVE_CONTEXT_SOFTWARE != 0, - strict_webgpu: false, - timestamps: false, - plane_stores: true, - shared_uploads: true, - }) + let mut chosen = GpuOptions::default(); + chosen.backends = backends; + chosen.power_preference = wgpu::PowerPreference::HighPerformance; + chosen.force_fallback_adapter = options.flags & EMBERWAVE_CONTEXT_SOFTWARE != 0; + Ok(chosen) } fn color_params(color: &EmberwaveColor) -> Result { - Ok(ColorParams { - matrix: match color.matrix { + Ok(ColorParams::new( + match color.matrix { EMBERWAVE_MATRIX_BT601 => ColorMatrix::Bt601, EMBERWAVE_MATRIX_BT709 => ColorMatrix::Bt709, EMBERWAVE_MATRIX_BT2020 => ColorMatrix::Bt2020, other => return Err(Failure::argument(format!("unknown matrix {other}"))), }, - range: match color.range { + match color.range { EMBERWAVE_RANGE_LIMITED => YcbcrRange::Limited, EMBERWAVE_RANGE_FULL => YcbcrRange::Full, other => return Err(Failure::argument(format!("unknown range {other}"))), }, - chroma_siting: match color.chroma_siting { + match color.chroma_siting { EMBERWAVE_SITING_CENTER => ChromaSiting::Center, EMBERWAVE_SITING_LEFT => ChromaSiting::Left, other => return Err(Failure::argument(format!("unknown chroma siting {other}"))), }, - }) + )) } /// Checks that `target` takes `rows` rows of `row_bytes` bytes: its pointer is @@ -1320,7 +1332,7 @@ pub unsafe extern "C" fn emberwave_probe_pwv1( let described = EmberwaveStreamInfo { frame_rate_num: header.frame_rate_num, frame_rate_den: header.frame_rate_den, - ..stream_info(&header.stream) + ..stream_info(&header.stream)? }; // SAFETY: as the caller promises. unsafe { write_struct(info, "info", described) } @@ -1348,12 +1360,16 @@ pub unsafe extern "C" fn emberwave_probe_packet( match Header::parse(bytes)? { // SAFETY: as the caller promises. Header::StartOfFrame(stream) => unsafe { - write_struct(info, "info", stream_info(&stream)) + 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(), )), + _ => Err(Failure( + EmberwaveStatus::Bitstream, + "the packet starts with a header this version does not know".into(), + )), } }) } @@ -1386,7 +1402,9 @@ pub unsafe extern "C" fn emberwave_context_create( // SAFETY: as the caller promises. let gpu_options = match unsafe { read_struct(options, "options") }? { Some(options) => gpu_options(&options)?, - None => GpuOptions::default(), + // As the command line tool: the environment may narrow the + // backends, and a value that names none is ignored. + None => GpuOptions::from_env().unwrap_or_default(), }; let created = GpuContext::new(&gpu_options)?; // SAFETY: checked by the first output() above. @@ -4228,13 +4246,7 @@ pub unsafe extern "C" fn emberwave_decoder_set_partial_share( run(|| { // SAFETY: as the caller promises. let decoder = unsafe { live_mut(decoder) }?; - if !(0.0..=1.0).contains(&share) { - return Err(Failure::argument(format!( - "a share of {share} is not from 0 to 1" - ))); - } - decoder.decoder.set_partial_share(share); - Ok(()) + Ok(decoder.decoder.set_partial_share(share)?) }) } diff --git a/crates/emberwave-ffi/tests/api.rs b/crates/emberwave-ffi/tests/api.rs index 9b45edd..37b2a0d 100644 --- a/crates/emberwave-ffi/tests/api.rs +++ b/crates/emberwave-ffi/tests/api.rs @@ -290,7 +290,7 @@ fn planes_through_the_c_api_equal_the_rust_decoder() { ) }; - let rust_context = GpuContext::new(&GpuOptions::default()).unwrap(); + let rust_context = GpuContext::new(&GpuOptions::from_env().unwrap_or_default()).unwrap(); let header = *Pwv1File::parse(&bytes).unwrap().header(); let mut rust = Decoder::new( &rust_context, @@ -346,7 +346,7 @@ fn rgb_through_the_c_api_follows_the_stream_or_the_caller() { let decoder = decoder_for(context, &bytes); let frame = frames(&bytes)[0]; let header = *Pwv1File::parse(&bytes).unwrap().header(); - let rust_context = GpuContext::new(&GpuOptions::default()).unwrap(); + let rust_context = GpuContext::new(&GpuOptions::from_env().unwrap_or_default()).unwrap(); let mut rust = Decoder::new(&rust_context, 200, 120, header.stream.chroma).unwrap(); let decode = |color: *const EmberwaveColor| { @@ -397,11 +397,11 @@ fn rgb_through_the_c_api_follows_the_stream_or_the_caller() { let expected = rust .decode_rgb( frame, - &ColorParams { - matrix: emberwave_core::color::ColorMatrix::Bt601, - range: emberwave_core::bitstream::YcbcrRange::Full, - chroma_siting: emberwave_core::bitstream::ChromaSiting::Left, - }, + &ColorParams::new( + emberwave_core::color::ColorMatrix::Bt601, + emberwave_core::bitstream::YcbcrRange::Full, + emberwave_core::bitstream::ChromaSiting::Left, + ), ) .unwrap(); let rgb = decode(&chosen); @@ -557,7 +557,7 @@ fn a_partial_frame_needs_the_share_the_caller_sets() { match Header::parse(&packet[at..]) { Ok(Header::StartOfFrame(_)) => at += 8, Ok(Header::Block(block)) => break Some(block.block_index), - Err(_) => break None, + Ok(_) | Err(_) => break None, } }; if first_block.is_some_and(|block| block >= coarsest) && number % 2 == 1 { diff --git a/crates/emberwave-ffi/tests/encoder.rs b/crates/emberwave-ffi/tests/encoder.rs index b34eda1..e90b612 100644 --- a/crates/emberwave-ffi/tests/encoder.rs +++ b/crates/emberwave-ffi/tests/encoder.rs @@ -191,7 +191,8 @@ fn frames_through_the_c_api_equal_the_rust_encoder() { let Some(context) = context() else { return; }; - let rust_context = GpuContext::new(&GpuOptions::default()).expect("a GPU, as the C API's"); + let rust_context = GpuContext::new(&GpuOptions::from_env().unwrap_or_default()) + .expect("a GPU, as the C API's"); for (width, height, chroma) in SHAPES { let layout = FrameLayout::new(width, height, chroma).unwrap(); let tight = encoder_for(context, &layout, None); @@ -964,6 +965,7 @@ fn rgb_picture(width: u32, height: u32, stride: usize, order: RgbOrder) -> Vec [r, g, b, (state >> 24) as u8], RgbOrder::Bgra => [b, g, r, (state >> 24) as u8], + other => panic!("{other:?}"), }; bytes[y * stride + 4 * x..][..4].copy_from_slice(&pixel); } @@ -1010,11 +1012,7 @@ fn pictures_in_rgb_encode_as_the_planes_they_convert_to() { transfer: EMBERWAVE_TRANSFER_PQ, ..Default::default() }; - let other_params = ColorParams { - matrix: ColorMatrix::Bt2020, - range: YcbcrRange::Full, - chroma_siting: ChromaSiting::Left, - }; + let other_params = ColorParams::new(ColorMatrix::Bt2020, YcbcrRange::Full, ChromaSiting::Left); for (width, height, chroma) in SHAPES { let layout = FrameLayout::new(width, height, chroma).unwrap(); for (signalling, params) in [(None, ColorParams::default()), (Some(&other), other_params)] { diff --git a/crates/emberwave-web/src/bench.rs b/crates/emberwave-web/src/bench.rs index b671e47..bc98435 100644 --- a/crates/emberwave-web/src/bench.rs +++ b/crates/emberwave-web/src/bench.rs @@ -146,7 +146,7 @@ impl Bench { }); stream .decoder - .encode_rgb( + .record_rgb( &stream.frames[stream.next], &ColorParams::default(), &mut encoder, diff --git a/crates/emberwave-web/src/demo.rs b/crates/emberwave-web/src/demo.rs index 3df2774..600d1a1 100644 --- a/crates/emberwave-web/src/demo.rs +++ b/crates/emberwave-web/src/demo.rs @@ -56,7 +56,7 @@ impl Demo { frame_rate_den: 1, reference_bit_depth: 8, }; - let mut file = Vec::with_capacity(pwv1::HEADER_BYTES + FRAMES as usize * (4 + BUDGET)); + let mut file = Vec::with_capacity(pwv1::FILE_HEADER_BYTES + FRAMES as usize * (4 + BUDGET)); file.extend_from_slice(&header.to_bytes()); Ok(Self { encoder, diff --git a/crates/emberwave-web/src/lib.rs b/crates/emberwave-web/src/lib.rs index e0de586..293eb8b 100644 --- a/crates/emberwave-web/src/lib.rs +++ b/crates/emberwave-web/src/lib.rs @@ -126,7 +126,7 @@ pub fn packet_ends(frame: &[u8], max_bytes: usize) -> Vec { /// header that every frame begins with, ends them, so that playing never /// meets a frame it cannot read. fn playable_frames(file: &Pwv1File<'_>) -> (usize, usize, Option) { - let mut end = pwv1::HEADER_BYTES; + let mut end = pwv1::FILE_HEADER_BYTES; let mut count = 0; for frame in file.frames() { let reason = match frame { @@ -263,7 +263,7 @@ impl Player { } let decoder = self.new_decoder(stream.width, stream.height, stream.chroma)?; let mut frames = Vec::with_capacity(count); - let mut at = pwv1::HEADER_BYTES; + let mut at = pwv1::FILE_HEADER_BYTES; while at < end { let size = u32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes")); frames.push(at + 4..at + 4 + size as usize); @@ -286,7 +286,7 @@ impl Player { file: bytes, end, frames, - next: pwv1::HEADER_BYTES, + next: pwv1::FILE_HEADER_BYTES, decoder, color: ColorParams::from_stream(&stream), follows_frames: false, @@ -457,17 +457,14 @@ impl Player { /// the stream open. Throws for a share outside 0 to 1, and without a /// stream. pub fn set_partial_share(&mut self, share: f32) -> Result<(), JsError> { - if !(0.0..=1.0).contains(&share) { - return Err(JsError::new(&format!( - "a share of {share} is not from 0 to 1" - ))); - } let stream = self .stream .as_mut() .ok_or_else(|| JsError::new("no stream is open"))?; - stream.decoder.set_partial_share(share); - Ok(()) + stream + .decoder + .set_partial_share(share) + .map_err(|error| JsError::new(&error.to_string())) } /// Sets whether a frame drawn in part conceals the blocks of detail it @@ -647,10 +644,10 @@ impl Player { let view = target.texture.create_view(&Default::default()); stream .decoder - .encode_rgb_to_texture(frame, &stream.color, &view, &mut encoder) + .record_rgb_to_texture(frame, &stream.color, &view, &mut encoder) .map_err(js_error)?; stream.next = if after >= stream.end { - pwv1::HEADER_BYTES + pwv1::FILE_HEADER_BYTES } else { after };