Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions crates/engine/src/realtime/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ impl PlanUnitEligibility {
/// implementations are policy-limited to `graph`.
#[doc(hidden)]
pub trait PreparedPlanExecutor: Send {
/// Whether the exclusively owned source consumer supports between-block seek preparation.
fn can_prepare_source_seek(&self, _source_index: usize) -> bool {
false
}
/// Apply an admitted source seek without rendering or advancing any sample clock.
fn prepare_source_seek(&mut self, _source_index: usize, _generation: u64, _frame: u64) -> bool {
false
}
/// Render one already-validated block using only preallocated state.
fn render(
&mut self,
Expand Down Expand Up @@ -535,6 +543,27 @@ impl PreparedRenderPlan {
.as_deref()
.map_or([0; 4], PreparedPlanExecutor::dispatch_counters)
}
/// Prevalidate source preparation before a host admits the producer-side seek.
/// Requires the plan owner; it provides no concurrent control-side consumer handle.
pub fn can_prepare_source_seek(&self, source_index: usize) -> bool {
self.executor
.as_ref()
.is_some_and(|executor| executor.can_prepare_source_seek(source_index))
}

/// Prepare an admitted seek on the exclusive render owner between blocks.
/// Does not render, mutate topology, or advance the plan's sample clock.
pub fn prepare_source_seek(
&mut self,
source_index: usize,
generation: u64,
frame: u64,
) -> bool {
self.executor
.as_mut()
.is_some_and(|executor| executor.prepare_source_seek(source_index, generation, frame))
}

/// The plan's internal executor, for the block-boundary hand-over in `plan_exchange`.
pub(crate) fn executor_mut(&mut self) -> Option<&mut (dyn PreparedPlanExecutor + 'static)> {
self.executor.as_deref_mut()
Expand Down
19 changes: 19 additions & 0 deletions crates/graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,12 @@ impl GraphSourceSetResourceReport {
/// Implementors own prepared source consumers and source-plane storage. The graph invokes this
/// only on its coordinator before ordinary nodes or native dependency waves begin.
pub trait GraphPreparedSourceSetDriver: Send {
fn can_prepare_source_seek(&self, _source_index: usize) -> bool {
false
}
fn prepare_source_seek(&mut self, _source_index: usize, _generation: u64, _frame: u64) -> bool {
false
}
fn claim_count(&self) -> usize;
fn begin_block(&mut self, first_sample: u64, frames: u32) -> Result<(), RenderError>;
fn copy_track_input(
Expand Down Expand Up @@ -1393,6 +1399,19 @@ impl GraphExecutor {
}

impl PreparedPlanExecutor for GraphExecutor {
fn can_prepare_source_seek(&self, source_index: usize) -> bool {
self.source_set
.as_ref()
.is_some_and(|set| set.driver.can_prepare_source_seek(source_index))
}

fn prepare_source_seek(&mut self, source_index: usize, generation: u64, frame: u64) -> bool {
self.source_set.as_mut().is_some_and(|set| {
set.driver
.prepare_source_seek(source_index, generation, frame)
})
}

// REALTIME_POLICY_BEGIN
fn render(
&mut self,
Expand Down
64 changes: 64 additions & 0 deletions crates/source/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,22 @@ pub struct PcmSourceConsumer {
}

impl PcmSourceConsumer {
/// Apply an already-admitted seek on the exclusive consumer owner between blocks.
/// Recycles stale storage and retains current-generation PCM without consuming a frame.
/// Native producers still only enqueue commands; this is not a shared controller handle.
pub fn prepare_seek(&mut self, generation: SourceGeneration, frame: SourceFrame) -> bool {
self.end_block();
self.flush_deferred_recycle();
// The prepared source command queue has one slot. Observe exactly that admitted
// command, then check the requested identity before granting readiness.
self.observe_seek_at_block_boundary();
if self.active_generation != generation || self.next_frame != frame {
return false;
}
self.acquire_current_block();
true
}

/// Immutable prepared ring shape shared with the producer endpoint.
#[must_use]
pub const fn shape(&self) -> PcmSourceShape {
Expand Down Expand Up @@ -1532,6 +1548,19 @@ fn source_set_retained_resources(
}

impl GraphPreparedSourceSetDriver for SourceGraphSourceSetDriver {
fn can_prepare_source_seek(&self, source_index: usize) -> bool {
source_index < self.sources.len()
}

fn prepare_source_seek(&mut self, source_index: usize, generation: u64, frame: u64) -> bool {
let Some(generation) = SourceGeneration::new(generation) else {
return false;
};
self.sources
.get_mut(source_index)
.is_some_and(|source| source.consumer.prepare_seek(generation, SourceFrame(frame)))
}

fn claim_count(&self) -> usize {
self.mappings.len()
}
Expand Down Expand Up @@ -1784,6 +1813,41 @@ mod tests {
assert!(report.largest_allocation_bytes >= 32);
}

#[test]
fn paused_seek_prepares_full_queues_without_consuming_target() {
for retained in [false, true] {
let (producer, mut consumer, _) = PcmSourceRing::prepare(config(1, 4, 8)).unwrap();
let mut host = producer.into_host_chunk_provider(RATE);
let old = [0.25; 4];
host.submit(chunk(1, 0, &[&old], 4, false)).unwrap();
host.submit(chunk(1, 4, &[&old], 4, false)).unwrap();
if retained {
consumer.acquire_current_block();
}
host.try_seek(SourceCommand::Seek {
generation: SourceGeneration(2),
frame: SourceFrame(100),
})
.unwrap();
assert!(consumer.prepare_seek(SourceGeneration(2), SourceFrame(100)));
assert_eq!(consumer.next_frame, SourceFrame(100));
assert_eq!(consumer.cumulative_read_frames, 0);
assert_eq!(consumer.underrun_frames, 0);
assert_eq!(consumer.underrun_events, 0);
assert_eq!(consumer.stale_generation_discard_count, 2);
let target = [1.0, 2.0, 3.0, 4.0];
host.submit(chunk(2, 100, &[&target], 4, false)).unwrap();
// Preparing again retains current-generation PCM and never consumes it.
assert!(consumer.prepare_seek(SourceGeneration(2), SourceFrame(100)));
assert!(!consumer.prepare_seek(SourceGeneration(1), SourceFrame(100)));
let mut output = [0.0; 4];
let report = consumer.read_block(&mut [&mut output]).unwrap();
assert_eq!(output, target);
assert_eq!(report.underrun_frames, 0);
assert_eq!(consumer.next_frame, SourceFrame(104));
}
}

#[test]
fn prepare_rejects_invalid_fixed_ring_shape() {
assert!(matches!(
Expand Down
2 changes: 1 addition & 1 deletion hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- Generated by qualification/generate-matrix.mjs from qualification/results.json. -->
# Browser deployment matrix

This matrix is generated from the pinned Playwright 1.62.1 headless Linux qualification run over candidate `fc01c534cce3d8c1464e489955bdab8bb45d9fe1` and the single shipped simd128 AudioWorklet artifact `22e4c25cba7f97b66db720ad8ac8cf653de0afcabe84101693f4fa166b90d4e6`. The version shown is the lowest version qualified by this run; older versions are unqualified, not implicitly supported.
This matrix is generated from the pinned Playwright 1.62.1 headless Linux qualification run over candidate `0d7102bfea894d746ec9d779f197918b1ed0bb54` and the single shipped simd128 AudioWorklet artifact `271a2bf3c8cf52f5156dadec091efd399324cd3f0c51aa7b4a2a08e632a648ce`. The version shown is the lowest version qualified by this run; older versions are unqualified, not implicitly supported.

| Browser engine | Qualified version floor | Attestation outcome | SIMD gate | AudioWorklet boot | Native corpus digest | Live console (#137) | Observation (#143) | 100 ms main-thread stall |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
Expand Down
4 changes: 2 additions & 2 deletions hosts/host-web/qualification/results.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema": "miso.web.qualification.matrix.v1",
"candidateCommit": "fc01c534cce3d8c1464e489955bdab8bb45d9fe1",
"wasmSha256": "22e4c25cba7f97b66db720ad8ac8cf653de0afcabe84101693f4fa166b90d4e6",
"candidateCommit": "0d7102bfea894d746ec9d779f197918b1ed0bb54",
"wasmSha256": "271a2bf3c8cf52f5156dadec091efd399324cd3f0c51aa7b4a2a08e632a648ce",
"playwrightVersion": "1.62.1",
"platform": "linux-headless",
"artifact": "single shipped simd128 AudioWorklet artifact",
Expand Down
28 changes: 26 additions & 2 deletions hosts/host-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,16 +1184,40 @@ impl AudioWorkletEngineHost {
self.record(code)
}

/// Queue one strictly increasing generation-tagged absolute source seek.
/// Apply one strictly increasing generation-tagged source seek between render blocks.
/// This web host owns both producer and render plan on one exclusive thread. Successful
/// admission also prepares its consumer so new PCM can enter even when old queues were full.
pub fn seek_source(&mut self, source_id: &[u8], generation: u64, source_frame: u64) -> u32 {
if self.status.state != STATE_READY {
return self.record(RESULT_WRONG_STATE);
}
let Some(ready) = self.ready.as_mut() else {
return self.fail(RESULT_INTERNAL, b"web.internal.ready\t$\n");
};
let Some(source_index) = ready
.session
.normalized_model()
.sources
.iter()
.position(|source| source.id.as_str().as_bytes() == source_id)
else {
return self.record(RESULT_INVALID_ARGUMENT);
};
if !ready.host.plan.can_prepare_source_seek(source_index) {
return self.record(RESULT_WRONG_STATE);
}
let code = match ready.host.sources.seek(source_id, generation, source_frame) {
Ok(()) => RESULT_OK,
Ok(()) => {
if ready
.host
.plan
.prepare_source_seek(source_index, generation, source_frame)
{
RESULT_OK
} else {
RESULT_INTERNAL
}
}
Err(error) => source_result(error),
};
self.record(code)
Expand Down
65 changes: 63 additions & 2 deletions hosts/host-web/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,12 +830,73 @@ fn source_backpressure_seek_render_and_stable_output_are_bounded() {
RESULT_OK
);
assert_eq!(host.seek_source(b"fixture-source", 2, 0), RESULT_OK);
assert_eq!(host.seek_source(b"fixture-source", 3, 0), RESULT_OK);
assert_eq!(host.render_next(), RESULT_OK);
assert_eq!(host.status().rendered_quanta, 2);
}

#[test]
fn paused_seek_recycles_full_internal_queue_before_first_target_quantum() {
let quantum = 128;
let document = identity_session(quantum, 512, 480_000);
let options = WebBootOptions {
source_ring_frames: 512,
..boot_options(quantum)
};
let mut host = AudioWorkletEngineHost::boot(document.as_bytes(), options).unwrap();
let old = [0.25; 128];
for block in 0..4 {
assert_eq!(
host.submit_source(
b"fixture-source",
1,
block * 128,
48_000,
&[&old, &old],
quantum,
false
),
RESULT_OK
);
}
assert_eq!(
host.seek_source(b"fixture-source", 3, 0),
host.submit_source(
b"fixture-source",
1,
512,
48_000,
&[&old, &old],
quantum,
false
),
RESULT_BACKPRESSURE
);
assert_eq!(
host.seek_source(b"unknown", 2, 10_000),
RESULT_INVALID_ARGUMENT
);
assert_eq!(host.seek_source(b"fixture-source", 2, 10_000), RESULT_OK);
assert_eq!(host.status().next_absolute_sample, 0);
assert_eq!(host.status().rendered_quanta, 0);
let left = core::array::from_fn::<_, 128, _>(|index| (index + 1) as f32 / 256.0);
let right = core::array::from_fn::<_, 128, _>(|index| -(index as f32 + 1.0) / 512.0);
assert_eq!(
host.submit_source(
b"fixture-source",
2,
10_000,
48_000,
&[&left, &right],
quantum,
false
),
RESULT_OK
);
assert_eq!(host.render_next(), RESULT_OK);
assert_eq!(host.status().rendered_quanta, 2);
let output = host.output_pcm().unwrap();
assert_eq!(&output[..128], &left);
assert_eq!(&output[128..256], &right);
assert_eq!(host.status().next_absolute_sample, 128);
}

#[test]
Expand Down
Loading