1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
//! Main thread housekeeping: GC drain, status-flags sync, pending model load, latency.
use super::NamClapMainThread;
use crate::common::spsc::{self, drain_gc_channels};
use crate::models::slimmable::slice_wavenet_model;
use crate::models::{NamModel, StaticModel};
use clack_extensions::tail::HostTail;
use std::sync::atomic::Ordering;
impl<'a> NamClapMainThread<'a> {
/// GC drain, status flag mirroring, hugepage sync, pending model load, latency notification.
pub(crate) fn housekeeping(&mut self) {
// Flush any model deferred by load_model() (F3 fix).
// Primary mechanism is activate(), this is a fallback for hosts
// that call state-load between activate() and the first process().
// Errors set RT_STATUS_MODEL_LOAD_FAILED internally; housekeeping is void.
let _ = self.flush_pending_model();
// Drain obsolete models to free memory outside RT.
let drained = drain_gc_channels(
&mut self.gc_rx,
&self.shared.cold.gc_overflow,
&self.shared.cold.rt_status,
);
self.shared
.cold
.rt_status
.drains
.fetch_add(drained as u32, Ordering::Relaxed);
let current_bits = self
.shared
.cold
.rt_status
.status_bits
.load(Ordering::Relaxed);
self.shared
.cold
.rt_status
.flags_seen
.fetch_or(current_bits, Ordering::Relaxed);
// Sync huge page status from mirror buffer (one-shot).
{
use std::sync::atomic::{AtomicBool, Ordering};
static HUGEPAGE_SYNCED: AtomicBool = AtomicBool::new(false);
if !HUGEPAGE_SYNCED.load(Ordering::Relaxed) {
crate::dsp::mirror_buf::sync_huge_page_flag(&self.shared.cold.rt_status);
HUGEPAGE_SYNCED.store(true, Ordering::Relaxed);
}
}
// Slimmable reset failure: RT thread sets flag, main thread emits log.
if self
.shared
.cold
.rt_status
.check_and_clear_flag(spsc::RT_STATUS_SLIMMABLE_RESET_FAILED)
{
log::error!("ContainerModel submodel reset failed — model may run in previous state.");
}
// WaveNet slimmable rebuild: main thread performs all allocation,
// prewarm, and mmap outside the audio-thread callback.
if self
.shared
.cold
.rt_status
.check_flag_acquire(spsc::RT_STATUS_NEEDS_SLIMMABLE_REBUILD)
{
let target_ch = self
.shared
.cold
.rt_status
.requested_slimmable_ch
.load(Ordering::Relaxed) as usize;
let buffer_size = self.shared.cold.buffer_size.load(Ordering::Relaxed) as usize;
if target_ch >= 4 {
let new_model = {
let storage = self
.shared
.cold
.full_wavenet_model
.lock()
.unwrap_or_else(|e| e.into_inner());
storage.as_ref().and_then(|m| {
if let StaticModel::WavenetDyn(w) = m.as_ref() {
match slice_wavenet_model(w, target_ch) {
Ok(mut slimmed) => {
slimmed.prewarm();
if buffer_size > 0 {
let _ = slimmed.set_max_buffer_size(buffer_size);
}
Some(Box::new(StaticModel::WavenetDyn(Box::new(slimmed))))
}
Err(_) => {
self.shared
.cold
.rt_status
.set_flag(spsc::RT_STATUS_SLIMMABLE_SLICE_FAILED);
None
}
}
} else {
None
}
})
};
if let Some(model) = new_model {
let _ = self.slimmable_tx.push(Some(model));
}
}
self.shared
.cold
.rt_status
.clear_flag_release(spsc::RT_STATUS_NEEDS_SLIMMABLE_REBUILD);
}
// Oversampling engine rebuild: main thread performs all allocation
// (OversampleEngine::new creates filters and buffers) off the audio-thread.
if self
.shared
.cold
.rt_status
.check_flag_acquire(spsc::RT_STATUS_NEEDS_OS_REBUILD)
{
use crate::dsp::oversample::{OversampleEngine, OversampleFactor};
use crate::dsp::pipeline::MAX_RESAMP_BUF;
let factor_val = self
.shared
.cold
.rt_status
.requested_os_factor
.load(Ordering::Relaxed);
let factor = OversampleFactor::from_f32(factor_val as f32);
if let (Ok(l), Ok(r)) = (
OversampleEngine::new(factor, MAX_RESAMP_BUF),
OversampleEngine::new(factor, MAX_RESAMP_BUF),
) {
let _ = self
.param_tx
.push(crate::clap::plugin::ClapParamPayload::SetOversample {
os_l: Box::new(l),
os_r: Box::new(r),
});
self.shared
.cold
.rt_status
.clear_flag_release(spsc::RT_STATUS_NEEDS_OS_REBUILD);
} else {
crate::common::diagnostics::NamDiagnostic::new(
crate::common::diagnostics::NamErrorCode::OutOfMemory,
&self.sys,
)
.message("Failed to rebuild oversample engine (OOM).")
.hint("The current oversampling state will be preserved.")
.emit_warning();
self.shared
.cold
.rt_status
.clear_flag_release(spsc::RT_STATUS_NEEDS_OS_REBUILD);
}
}
// Propagate dialog_state → ui_pending_model (R2: Arc-backed, UAF-safe)
if let Some(dialog_state) = self.shared.cold.dialog_state.as_ref() {
let mut dialog_guard = dialog_state.pending_model.lock().unwrap_or_else(|e| {
log::error!("PoisonError in dialog_state.pending_model lock: {e:?}");
e.into_inner()
});
if let Some(path) = dialog_guard.take() {
let mut ui_guard = self
.shared
.cold
.ui_pending_model
.lock()
.unwrap_or_else(|e| {
log::error!("PoisonError in ui_pending_model lock: {e:?}");
e.into_inner()
});
*ui_guard = Some(path);
}
}
// Check if there is a pending model sent by the UI
let pending_model = self
.shared
.cold
.ui_pending_model
.lock()
.unwrap_or_else(|e| {
log::error!("PoisonError in ui_pending_model lock: {e:?}");
e.into_inner()
})
.take();
if let Some(path) = pending_model {
let res = self.load_model(&path);
self.shared.cold.ui_loading.store(false, Ordering::Relaxed);
match res {
Ok(_) => {}
Err(e) => {
let err_msg = e.error_code().message();
let mut msg_guard =
self.shared
.cold
.ui_load_error_msg
.lock()
.unwrap_or_else(|e| {
log::error!("PoisonError in ui_load_error_msg lock: {e:?}");
e.into_inner()
});
*msg_guard = err_msg.to_string();
self.shared
.cold
.ui_load_error
.store(true, Ordering::Relaxed);
log::error!("Failed to load model from GUI: {e:?}");
}
}
}
// Check if there is a pending IR sent by the UI
#[cfg(any(feature = "standalone", feature = "clap-plugin", test))]
{
use crate::clap::plugin::ClapParamPayload;
// Propagate ir_dialog_state → ui_pending_ir (R2: Arc-backed, UAF-safe)
if let Some(ir_dialog_state) = self.shared.cold.ir_dialog_state.as_ref() {
let mut dialog_guard = ir_dialog_state.pending_ir.lock().unwrap_or_else(|e| {
log::error!("PoisonError in ir_dialog_state.pending_ir lock: {e:?}");
e.into_inner()
});
if let Some(path) = dialog_guard.take() {
let mut ui_guard = self.shared.cold.ui_pending_ir.lock().unwrap_or_else(|e| {
log::error!("PoisonError in ui_pending_ir lock: {e:?}");
e.into_inner()
});
*ui_guard = Some(path);
}
}
let pending_ir = self
.shared
.cold
.ui_pending_ir
.lock()
.unwrap_or_else(|e| {
log::error!("PoisonError in ui_pending_ir lock: {e:?}");
e.into_inner()
})
.take();
if let Some(path) = pending_ir {
let res = self.load_cabsim(&path);
self.shared
.cold
.ui_ir_loading
.store(false, Ordering::Relaxed);
match res {
Ok(_) => {}
Err(e) => {
let err_msg = e.error_code().message();
let mut msg_guard = self
.shared
.cold
.ui_ir_load_error_msg
.lock()
.unwrap_or_else(|e| {
log::error!("PoisonError in ui_ir_load_error_msg lock: {e:?}");
e.into_inner()
});
*msg_guard = err_msg.to_string();
self.shared
.cold
.ui_ir_load_error
.store(true, Ordering::Relaxed);
log::error!("Failed to load cab-sim IR from GUI: {e:?}");
}
}
}
if self.shared.cold.ui_clear_ir.swap(false, Ordering::Relaxed) {
let _ = self
.param_tx
.push(ClapParamPayload::LoadCabIr { engine: None });
{
let mut ir_guard = self.shared.cold.ir_path.lock().unwrap_or_else(|e| {
log::error!("PoisonError in ir_path lock: {e:?}");
e.into_inner()
});
*ir_guard = None;
}
{
let mut raw_guard =
self.shared.cold.ir_raw_samples.lock().unwrap_or_else(|e| {
log::error!("PoisonError in ir_raw_samples lock: {e:?}");
e.into_inner()
});
*raw_guard = None;
}
}
}
// Latency Monitoring: Notify the host if the value changed
let current_latency = self.shared.rt_to_ui.current_latency.load(Ordering::Relaxed);
if current_latency != self.last_reported_latency {
self.last_reported_latency = current_latency;
log::info!(
"[Housekeeping] Latency changed: {} samples reported to host.",
current_latency
);
if let Some(latency_ext) = self
.host
.get_extension::<clack_extensions::latency::HostLatency>()
{
latency_ext.changed(&mut self.host);
}
}
// Tail Monitoring: notify the host when the CabSim tail length changes.
let cabsim_tail = self
.shared
.rt_to_ui
.cabsim_tail_samples
.load(Ordering::Relaxed);
if cabsim_tail != self.last_reported_cabsim_tail {
self.last_reported_cabsim_tail = cabsim_tail;
log::info!(
"[Housekeeping] CabSim tail changed: {} samples reported to host.",
cabsim_tail
);
if let Some(tail_ext) = self.host.get_extension::<HostTail>() {
let raw = std::ptr::NonNull::from(self.host.as_raw());
// SAFETY: HostMainThreadHandle and HostAudioProcessorHandle are
// repr(transparent) wrappers around the same NonNull<clap_host>.
// Calling tail.changed() from the main thread is valid per the
// clap_plugin_tail specification — it posts an event to the
// host's main-thread event queue.
let mut audio_host =
unsafe { clack_plugin::host::HostAudioProcessorHandle::from_raw(raw) };
tail_ext.changed(&mut audio_host);
}
}
}
}