Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
a5b04bb
perf(j2k): pack 8-bit RGB region rows in one vectorizable loop
jcwal1516 Oct 30, 2026
b1c74b1
perf(j2k-native): stream classic region rows from all components' poo…
jcwal1516 Oct 30, 2026
afab6a0
perf(j2k-native): synthesize streamed region rows with the SIMD row k…
jcwal1516 Oct 30, 2026
b87178f
perf(j2k-native): reconstruct region components on the pool, pack RGB…
jcwal1516 Oct 30, 2026
a421fdf
perf(j2k-native): finish region batches on every reader's thread
jcwal1516 Oct 30, 2026
4e8354f
perf(j2k-native): finish region batches while the next ones reconstruct
jcwal1516 Oct 30, 2026
37831cc
perf(j2k-native): deepen band rings for classic tiles read per component
jcwal1516 Oct 30, 2026
d8ad4f5
perf(j2k): pack 8-bit RGB region rows with the j2k-native RGB8 pack
jcwal1516 Oct 30, 2026
28365aa
docs(changelog): note the region row speedups on the pool bands
jcwal1516 Oct 30, 2026
d97dc21
fix(j2k): stream over-budget Gray8/RGB8 batch items instead of reject…
jcwal1516 Oct 31, 2026
6b30a0c
fix(j2k): split owned batch groups whose output exceeds one allocation
jcwal1516 Oct 31, 2026
ba58f11
perf(j2k-native): decode multi-layer classic plans' code-block rows i…
jcwal1516 Oct 31, 2026
85c10ac
fix(j2k): stream large Gray8/RGB8 batch items instead of holding whol…
jcwal1516 Oct 31, 2026
02f332a
fix(j2k-native): reconstruct region tiles whose coarse levels are empty
jcwal1516 Oct 31, 2026
a18efc1
perf(j2k-native): gather multi-segment block payloads as the blocks d…
jcwal1516 Oct 31, 2026
a553abd
perf(j2k-native): decode classic pool bands in parts when the rings h…
jcwal1516 Oct 31, 2026
4802de7
perf(j2k-native): keep layer records in 8 bytes and route strips to t…
jcwal1516 Oct 31, 2026
a4d3693
perf(j2k-native): halve the streamed row batch of tiles with deep ban…
jcwal1516 Oct 31, 2026
c60942d
docs(changelog): note the streamed-core memory and empty-coarse-level…
jcwal1516 Oct 31, 2026
189891d
fix(j2k): decode 8-bit full images past the decode budget as a stream…
jcwal1516 Oct 31, 2026
109d640
perf(j2k): stream full-image decodes by default where streaming is fa…
jcwal1516 Oct 31, 2026
9a0a839
perf(j2k): stream reduced HTJ2K decodes and keep low-rate classic ima…
jcwal1516 Oct 31, 2026
e7c5e0f
perf(j2k): stream tiled classic images tile by tile when their tiles …
jcwal1516 Oct 31, 2026
a31a603
perf(j2k): route small images by their measured streamed speed
jcwal1516 Oct 31, 2026
5f36f23
perf(j2k): stream large gray images and one-thread decodes on the mer…
jcwal1516 Oct 31, 2026
1039132
docs(changelog): note the plan-time full-image route
jcwal1516 Oct 31, 2026
6024e44
feat(j2k-native): pack native component planes from a region decode's…
jcwal1516 Oct 31, 2026
18db9e2
fix(j2k): stream 16-bit region decodes and over-budget native compone…
jcwal1516 Oct 31, 2026
866af5b
fix(j2k): decode 16-bit full images past the decode budget as a strea…
jcwal1516 Oct 31, 2026
cea9ab6
perf(j2k): convert 16-bit rows in pixels of a constant size
jcwal1516 Oct 31, 2026
1a7f159
perf(j2k-native): pack interleaved native samples a block of pixels a…
jcwal1516 Oct 31, 2026
7532ee2
perf(j2k): widen 8-bit native samples to 16 bits from a table
jcwal1516 Oct 31, 2026
53aa54b
perf(j2k): keep gray region decodes within the budget on their planes
jcwal1516 Oct 31, 2026
fe8ea22
refactor(j2k): route native component decodes with their own enum
jcwal1516 Oct 31, 2026
f5b39c8
fix(j2k): convert samples deeper than 16 bits by value in 8- and 16-b…
jcwal1516 Oct 31, 2026
edbf6e0
test(j2k): check the last row of tight buffers on the streamed routes
jcwal1516 Oct 31, 2026
bdb3e83
docs(changelog): note streamed 16-bit and native component outputs
jcwal1516 Oct 31, 2026
23e4100
fix(j2k): keep native region decodes that cannot stream on the planes…
jcwal1516 Oct 31, 2026
4ac96d3
fix(j2k-native): stream full images whose tiles have empty coarse levels
jcwal1516 Oct 31, 2026
94f85a4
fix(j2k-native): keep native component planes out of the decode budget
jcwal1516 Oct 31, 2026
5a1df49
fix(j2k-native): allocate streamed native planes when the first rows …
jcwal1516 Oct 31, 2026
14198f3
docs(changelog): note native component planes as decode output
jcwal1516 Oct 31, 2026
9daffe4
perf(j2k-native): read wide empty band rows through the streamed IDWT…
jcwal1516 Oct 31, 2026
39db1c3
perf(j2k-native): reconstruct large classic tiles on a reader per com…
jcwal1516 Oct 31, 2026
86601a8
perf(j2k-native): hold 32 rows per batch when one reader reconstructs…
jcwal1516 Oct 31, 2026
7006a04
perf(j2k-native): hold deep band rings only for whole-row full-resolu…
jcwal1516 Oct 31, 2026
8f5f578
fix(j2k-native): charge the row batches of large classic tiles on 10 …
jcwal1516 Nov 1, 2026
edcc998
perf(j2k-native): keep streamed classic bands on the pool when it has…
jcwal1516 Oct 31, 2026
9b61729
perf(j2k-native): pack streamed RGB8 rows through the fused ICT pack
jcwal1516 Oct 31, 2026
84df53a
perf(j2k): stream large low-rate classic images on small and large pools
jcwal1516 Oct 31, 2026
e62675f
perf(j2k-native): look streamed sub-bands up by index
jcwal1516 Oct 31, 2026
0fdb933
fix(j2k-native): charge the route list and release executor records b…
jcwal1516 Nov 1, 2026
2e76ce3
docs(changelog): note the streamed-route speedups and the one decode …
jcwal1516 Nov 1, 2026
eeb9700
perf(j2k-native): take fresh decode buffers zeroed from the allocator
jcwal1516 Oct 31, 2026
f963f0e
docs(changelog): note decode buffers zeroed by the allocator
jcwal1516 Oct 31, 2026
fd77ee3
fix(j2k-native): stream rows from serial bands when the planes would …
jcwal1516 Nov 1, 2026
d548621
fix(j2k-native): reserve each pool thread's payload scratch once, as …
jcwal1516 Nov 1, 2026
f3b255c
fix(j2k-native): charge the pool band threads' scratch once per strea…
jcwal1516 Nov 1, 2026
1e856a9
perf(j2k-native): decode multi-segment classic blocks on the pool routes
jcwal1516 Nov 1, 2026
ef07930
test(j2k-native): code the multi-segment fixtures past their first ra…
jcwal1516 Nov 1, 2026
6cd63f3
fix(j2k-native): hold the pool threads' scratch to its charge
jcwal1516 Nov 1, 2026
eabe712
test(j2k-native): check each budget step of streamed pool bands and c…
jcwal1516 Nov 1, 2026
ca9c14c
perf(j2k-native): reserve HTJ2K pool band threads no whole-block scratch
jcwal1516 Nov 1, 2026
4874018
fix(j2k-native): charge the band executor's records of each reader an…
jcwal1516 Nov 1, 2026
b4a5d2c
test(j2k-alloc-probe): track the live heap bytes and their peak
jcwal1516 Oct 31, 2026
47ddf9f
fix(j2k-native): charge the band executor's every allocation before i…
jcwal1516 Nov 1, 2026
5fcca63
fix(j2k-native): charge the pool's retiring jobs, first-use locks and…
jcwal1516 Nov 1, 2026
8ff3756
fix(j2k-native): charge what the pool takes for a decode entered from…
jcwal1516 Nov 1, 2026
2bc8d3a
fix(j2k-native): decode a component in serial bands when its executor…
jcwal1516 Nov 1, 2026
323851e
docs(changelog): note multi-segment classic blocks on the pool routes
jcwal1516 Nov 1, 2026
1535444
perf(j2k): decode streamed batch images on a pool of the explicit wor…
jcwal1516 Oct 31, 2026
14b91c8
perf(j2k): keep classic batch plans on the staged route at one worker
jcwal1516 Oct 31, 2026
f20c62e
fix(j2k): stream batch images only where the region decode streams rows
jcwal1516 Oct 31, 2026
7c9ceab
perf(j2k): flatten batch payloads only for groups on the staged route
jcwal1516 Oct 31, 2026
7003943
perf(j2k): stream every batch image whose rows stream
jcwal1516 Oct 31, 2026
938ff8b
docs(changelog): note the batch worker pool and streamed batch images
jcwal1516 Oct 31, 2026
ef020bd
perf(j2k): stream one-thread full-image decodes where streaming is ne…
jcwal1516 Nov 1, 2026
2342422
docs(changelog): note the one-thread streamed routes
jcwal1516 Nov 1, 2026
7c8533b
perf(j2k): decode bounded rows in one streamed pass
jcwal1516 Nov 1, 2026
9af9848
docs(changelog): note the one-pass bounded row decodes
jcwal1516 Nov 1, 2026
b45b8a2
perf(j2k-native): hold two finest bands for classic tiles read per co…
jcwal1516 Nov 1, 2026
7469685
docs(changelog): note the two finest-level bands of shallow classic r…
jcwal1516 Nov 1, 2026
fa47901
perf(j2k): hold two bands per ring for images that share a batch's pool
jcwal1516 Nov 1, 2026
4749ed4
docs(changelog): note the two-band rings of images that share a batch…
jcwal1516 Nov 1, 2026
e1f41dd
fix(j2k-native): keep a tile's reader batches within what the tile is…
jcwal1516 Nov 1, 2026
98fbf23
fix(j2k-native): size the clipped-block scratch with the serial Tier-…
jcwal1516 Nov 1, 2026
f5dfdee
docs(changelog): note the streamed accounting fixes
jcwal1516 Nov 1, 2026
f8121cc
perf(j2k-native): lift the streamed IDWT's rows with the banded schedule
jcwal1516 Nov 1, 2026
5283986
perf(j2k-native): finish streamed rows in place in the IDWT's ring
jcwal1516 Nov 1, 2026
aa4c1ce
perf(j2k-native): finish a single pool reader's rows as serial bands do
jcwal1516 Nov 1, 2026
87d3dc4
fix(j2k-native): count packed rows as handed over when the next batch…
jcwal1516 Nov 1, 2026
e1d401f
perf(j2k-native): keep pipelined row batches to 256 rows
jcwal1516 Nov 1, 2026
7b4528d
docs(api): record streamed decoding behavior and 0.13.2 review
jcwal1516 Nov 1, 2026
0133362
refactor(j2k): consolidate decode planning and output helpers
jcwal1516 Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 351 additions & 3 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ objc2-metal-performance-shaders-graph = { version = "=0.3.2", default-features =
] }
block2 = { version = "=0.6.2", default-features = false, features = ["std"] }
rayon = "1"
bytemuck = { version = "1.25", default-features = false }
cc = "1.2"
libc = "0.2"
windows-sys = "0.61"
Expand Down
54 changes: 53 additions & 1 deletion crates/j2k-alloc-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
//! delayed allocator call from reporting into a later measurement, and each
//! measurement waits for already-admitted reporters before taking its
//! snapshot.
//!
//! The allocator also keeps the bytes live at any moment and their high
//! water, always on and independent of measurements, for probes that bound
//! working memory rather than allocation calls (see [`reset_peak`]).

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
use std::sync::atomic::{AtomicU64, AtomicU8, AtomicUsize, Ordering};

#[cfg(test)]
use std::sync::atomic::AtomicBool;
Expand All @@ -33,6 +37,8 @@ static ALLOCATIONS: AtomicU64 = AtomicU64::new(0);
static REALLOCATIONS: AtomicU64 = AtomicU64::new(0);
static DEALLOCATIONS: AtomicU64 = AtomicU64::new(0);
static REQUESTED_BYTES: AtomicU64 = AtomicU64::new(0);
static LIVE_BYTES: AtomicUsize = AtomicUsize::new(0);
static PEAK_BYTES: AtomicUsize = AtomicUsize::new(0);

#[cfg(test)]
static TEST_HOOK: AtomicU8 = AtomicU8::new(TEST_HOOK_IDLE);
Expand Down Expand Up @@ -60,6 +66,7 @@ unsafe impl GlobalAlloc for CountingAllocator {
let pointer = unsafe { System.alloc(layout) };
if !pointer.is_null() {
reporter.record_allocation(layout.size());
grow_live(layout.size());
}
pointer
}
Expand All @@ -70,13 +77,15 @@ unsafe impl GlobalAlloc for CountingAllocator {
let pointer = unsafe { System.alloc_zeroed(layout) };
if !pointer.is_null() {
reporter.record_allocation(layout.size());
grow_live(layout.size());
}
pointer
}

unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
let reporter = Reporter::begin();
reporter.record_deallocation();
shrink_live(layout.size());
// SAFETY: forwards the pointer and its original layout unchanged.
unsafe { System.dealloc(pointer, layout) };
}
Expand All @@ -88,6 +97,11 @@ unsafe impl GlobalAlloc for CountingAllocator {
let replacement = unsafe { System.realloc(pointer, layout, new_size) };
if !replacement.is_null() {
reporter.record_reallocation(new_size);
if new_size >= layout.size() {
grow_live(new_size - layout.size());
} else {
shrink_live(layout.size() - new_size);
}
}
replacement
}
Expand Down Expand Up @@ -191,6 +205,44 @@ pub fn assert_allocations<R>(label: &str, budget: Budget, operation: impl FnOnce
result
}

/// Bytes held through the global allocator right now, by every thread.
#[must_use]
pub fn live_bytes() -> usize {
LIVE_BYTES.load(Ordering::Relaxed)
}

/// Start a new high-water window at the bytes live now and return them.
///
/// The window is process-wide like a measurement: allocations on every
/// thread raise [`peak_bytes`], so unrelated threads must stay idle while it
/// is open. Windows need no guard and never block the allocator.
pub fn reset_peak() -> usize {
let live = LIVE_BYTES.load(Ordering::Relaxed);
PEAK_BYTES.store(live, Ordering::Relaxed);
// An allocation between the load and the store raised the live bytes
// past `live`; keep it in the new window.
PEAK_BYTES.fetch_max(LIVE_BYTES.load(Ordering::Relaxed), Ordering::Relaxed);
live
}

/// The most bytes live at once since the last [`reset_peak`] (or since the
/// process started).
#[must_use]
pub fn peak_bytes() -> usize {
PEAK_BYTES.load(Ordering::Relaxed)
}

fn grow_live(bytes: usize) {
let live = LIVE_BYTES
.fetch_add(bytes, Ordering::Relaxed)
.wrapping_add(bytes);
PEAK_BYTES.fetch_max(live, Ordering::Relaxed);
}

fn shrink_live(bytes: usize) {
LIVE_BYTES.fetch_sub(bytes, Ordering::Relaxed);
}

struct MeasurementGuard {
finished: bool,
}
Expand Down
44 changes: 43 additions & 1 deletion crates/j2k-alloc-probe/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::hint::black_box;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::{mpsc, OnceLock};

use j2k_alloc_probe::{assert_allocations, measure, Budget};
use j2k_alloc_probe::{assert_allocations, live_bytes, measure, peak_bytes, reset_peak, Budget};
use j2k_native::{
decode_ht_code_block_scalar_with_workspace, encode,
encode_ht_code_block_scalar_with_passes_and_workspace,
Expand Down Expand Up @@ -32,6 +32,10 @@ fn main() {
harness_counts_allocation_and_deallocation,
),
("harness_counts_reallocation", harness_counts_reallocation),
(
"harness_tracks_live_bytes_and_their_peak",
harness_tracks_live_bytes_and_their_peak,
),
(
"preexisting_deallocation_cannot_credit_budget",
preexisting_deallocation_cannot_credit_budget,
Expand Down Expand Up @@ -131,6 +135,44 @@ fn harness_counts_reallocation() {
assert!(stats.requested_bytes >= 4096);
}

fn harness_tracks_live_bytes_and_their_peak() {
const BLOCK: usize = 64 * 1024;
let base = reset_peak();
assert_eq!(peak_bytes(), base, "a new window starts at the live bytes");

let first = black_box(Box::<[u8]>::new_uninit_slice(BLOCK));
let second = black_box(Box::<[u8]>::new_uninit_slice(BLOCK / 2));
assert_eq!(live_bytes(), base + BLOCK + BLOCK / 2);
drop(first);
assert_eq!(
live_bytes(),
base + BLOCK / 2,
"a free lowers the live bytes"
);
assert_eq!(
peak_bytes(),
base + BLOCK + BLOCK / 2,
"a free never lowers the peak"
);

let mut grown: Vec<u8> = Vec::new();
grown
.try_reserve_exact(BLOCK / 4)
.expect("small allocation must succeed");
grown
.try_reserve_exact(2 * BLOCK)
.expect("growth allocation must succeed");
assert_eq!(live_bytes(), base + BLOCK / 2 + grown.capacity());
assert_eq!(peak_bytes(), base + BLOCK / 2 + grown.capacity());
// Nothing was pushed: shrinking the empty vector frees its buffer.
grown.shrink_to_fit();
drop(second);
assert_eq!(live_bytes(), base, "every byte allocated here was freed");

assert_eq!(reset_peak(), base);
assert_eq!(peak_bytes(), base, "a reset forgets the earlier peak");
}

fn preexisting_deallocation_cannot_credit_budget() {
let preexisting = black_box(Box::new([0_u8; 8192]));
let violation = without_panic_output(|| {
Expand Down
3 changes: 3 additions & 0 deletions crates/j2k-native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ name = "j2k_native"
path = "src/lib.rs"

[dependencies]
bytemuck = { workspace = true, features = ["extern_crate_alloc"] }
fearless_simd = { workspace = true, optional = true }
libm = { workspace = true }
log = { workspace = true, optional = true }
Expand All @@ -36,6 +37,8 @@ j2k-types = { path = "../j2k-types", version = "=0.13.2" }

[dev-dependencies]
criterion = { workspace = true }
# Counts the bytes the band executor allocates, against what it charges.
j2k-alloc-probe = { path = "../j2k-alloc-probe" }
# Lets SIMD kernel tests also run on the portable fallback level.
fearless_simd = { workspace = true, features = ["force_support_fallback"] }

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions crates/j2k-native/src/color/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub use output_planes::{
DecodedComponentsParts, DecodedNativeComponents, DecodedNativeComponentsParts,
NativeComponentPlane, NativeComponentPlaneParts,
};
pub use packing::pack_rgb8;
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
pub(crate) use packing::store_rgb8_x8;
pub(crate) use packing::{
Expand Down
11 changes: 11 additions & 0 deletions crates/j2k-native/src/color/output_planes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ impl<'a> ComponentPlane<'a> {
pub trait ComponentRowSink {
/// Receive the samples of one rectangle of the region.
fn write_rows(&mut self, x: u32, y: u32, planes: &[ComponentPlane<'_>]);

/// The sink's destination for a `width` x `height` region of three 8-bit
/// components, when it is interleaved RGB8 rows of `3 * width` bytes back
/// to back that the sink would fill from such planes by rounding and
/// saturating each sample. A decode may then pack its rows straight into
/// this buffer instead of calling [`Self::write_rows`]. The default,
/// `None`, always uses `write_rows`.
fn rgb8_region(&mut self, width: u32, height: u32) -> Option<&mut [u8]> {
let _ = (width, height);
None
}
}

/// Borrowed decoded component planes for an image.
Expand Down
2 changes: 1 addition & 1 deletion crates/j2k-native/src/color/packing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub(crate) fn interleave_and_convert(
}

/// Pack 8-bit RGB planes as interleaved bytes, rounded and saturated.
fn pack_rgb8(planes: [&[f32]; 3], output: &mut [u8]) {
pub fn pack_rgb8(planes: [&[f32]; 3], output: &mut [u8]) {
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
{
use fearless_simd::{dispatch, Level};
Expand Down
22 changes: 20 additions & 2 deletions crates/j2k-native/src/direct_cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,21 @@ pub struct J2kDirectCpuScratch {
ht_task_workspaces: Vec<HtCodeBlockDecodeWorkspace>,
/// Extra classic workspaces for parallel referenced classic execution,
/// one per additional Rayon worker.
classic_task_workspaces: Vec<J2kCodeBlockDecodeWorkspace>,
classic_task_workspaces: Vec<ClassicTaskWorkspace>,
/// Whether referenced execution may spread work over the Rayon pool.
parallel: bool,
staged_state: Option<StagedDirectState>,
}

/// One extra Rayon worker's state for parallel referenced classic execution:
/// its code-block workspace and the buffer it joins a code block's payload
/// fragments into when a multi-layer codestream splits them.
#[derive(Debug, Default)]
struct ClassicTaskWorkspace {
decode: J2kCodeBlockDecodeWorkspace,
payload: Vec<u8>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StagedDirectRoute {
Classic,
Expand Down Expand Up @@ -170,12 +179,21 @@ impl J2kDirectCpuScratch {
.iter()
.try_fold(
self.classic_workspace.allocated_bytes().ok(),
|bytes, workspace| Some(bytes?.checked_add(workspace.allocated_bytes().ok()?)),
|bytes, task| Some(bytes?.checked_add(task.decode.allocated_bytes().ok()?)),
)
.flatten()
.unwrap_or(usize::MAX)
}

/// Length and capacity of each extra classic task's payload buffer.
#[cfg(test)]
fn classic_task_payloads(&self) -> Vec<(usize, usize)> {
self.classic_task_workspaces
.iter()
.map(|task| (task.payload.len(), task.payload.capacity()))
.collect()
}

/// Number of coefficient/IDWT buffer owners retained by this scratch.
///
/// Referenced multi-tile execution retains the maximum live tile shape,
Expand Down
50 changes: 35 additions & 15 deletions crates/j2k-native/src/direct_cpu/allocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,9 @@ fn parallel_ht_task_workspace_count(
}

/// Largest code block and most code-block rows of any sub-band a parallel
/// referenced classic execution decodes. One dispatch decodes at most one
/// row per Rayon worker, so it never needs more workspaces than rows.
/// referenced classic execution decodes, and its largest payload split over
/// several input fragments. One dispatch decodes at most one row per Rayon
/// worker, so it never needs more workspaces than rows.
#[derive(Debug, Clone, Copy)]
struct ClassicTaskShape {
dimensions: (u32, u32),
Expand All @@ -462,6 +463,9 @@ struct ClassicTaskShape {
expect(dead_code, reason = "rows bound parallel dispatch only")
)]
rows: usize,
/// Bytes each task joins one fragmented payload into; zero when every
/// payload is one input slice.
fragmented_payload_bytes: usize,
}

/// Extra classic workspaces beside the main one: one per additional Rayon
Expand All @@ -478,36 +482,44 @@ fn classic_task_workspace_count(parallel: bool, classic_tasks: Option<ClassicTas
}

/// Charge the extra classic workspaces of parallel referenced classic
/// execution. `classic_tasks` is `None` for every other route.
/// execution and their payload buffers. `classic_tasks` is `None` for every
/// other route.
fn include_classic_task_workspaces(
budget: &mut DirectAllocationBudget,
classic_tasks: Option<ClassicTaskShape>,
scratch: &J2kDirectCpuScratch,
) -> Result<()> {
let target_count = classic_task_workspace_count(scratch.parallel, classic_tasks);
budget.include_capacity::<crate::J2kCodeBlockDecodeWorkspace>(
budget.include_capacity::<super::ClassicTaskWorkspace>(
scratch.classic_task_workspaces.capacity().max(target_count),
)?;
let target_bytes = classic_tasks.map_or(Ok(0), |shape| {
let target_workspace_bytes = classic_tasks.map_or(Ok(0), |shape| {
bitplane::classic_decode_workspace_bytes(shape.dimensions.0, shape.dimensions.1)
})?;
let target_payload_bytes = classic_tasks.map_or(0, |shape| shape.fragmented_payload_bytes);
for index in 0..scratch.classic_task_workspaces.len().max(target_count) {
let retained = scratch
let (retained_workspace, retained_payload) = scratch
.classic_task_workspaces
.get(index)
.map_or(Ok(0), crate::J2kCodeBlockDecodeWorkspace::allocated_bytes)?;
let target = if index < target_count {
target_bytes
.map_or(Ok((0, 0)), |task| {
task.decode
.allocated_bytes()
.map(|bytes| (bytes, task.payload.capacity()))
})?;
let (target_workspace, target_payload) = if index < target_count {
(target_workspace_bytes, target_payload_bytes)
} else {
0
(0, 0)
};
budget.include_bytes(retained.max(target))?;
budget.include_bytes(retained_workspace.max(target_workspace))?;
budget.include_bytes(retained_payload.max(target_payload))?;
}
Ok(())
}

/// Keep exactly the extra classic workspaces parallel referenced classic
/// execution uses, each prepared for the plan's largest code block.
/// execution uses, each prepared for the plan's largest code block and
/// holding a buffer for its largest fragmented payload.
fn reserve_classic_task_workspaces(
classic_tasks: Option<ClassicTaskShape>,
scratch: &mut J2kDirectCpuScratch,
Expand All @@ -518,15 +530,23 @@ fn reserve_classic_task_workspaces(
while scratch.classic_task_workspaces.len() < count {
scratch
.classic_task_workspaces
.push(crate::J2kCodeBlockDecodeWorkspace::default());
.push(super::ClassicTaskWorkspace::default());
}
if let Some(ClassicTaskShape {
dimensions: (width, height),
fragmented_payload_bytes,
..
}) = classic_tasks
{
for workspace in &mut scratch.classic_task_workspaces {
workspace.prepare(width, height)?;
for task in &mut scratch.classic_task_workspaces {
task.decode.prepare(width, height)?;
task.payload.clear();
if task.payload.capacity() < fragmented_payload_bytes {
// Release before growing so the old and new buffers never
// coexist beyond the charged maximum.
task.payload = Vec::new();
}
try_reserve_decode_elements(&mut task.payload, fragmented_payload_bytes)?;
}
}
Ok(())
Expand Down
Loading
Loading