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
//! HintsShmem is responsible for writing precompile processed hints to shared memory.
//!
//! It implements the HintsSink trait to receive processed hints and write them to shared memory
//! using ShmemWriter instances.
use crate::{
sem_prec_available_name, sem_prec_read_name, shmem_control_output_name, shmem_precompile_name,
AsmService, AsmServices, ControlShmem, ShmemReader, ShmemWriter,
};
use anyhow::Result;
use named_sem::NamedSemaphore;
use std::sync::{
atomic::{fence, AtomicUsize, Ordering},
Arc, Mutex,
};
use tracing::debug;
use zisk_common::io::{StreamError, StreamSink};
/// Per-service control-output shmem (the C side's
/// `precompile_read_address`). Read by the parent for flow control in
/// `submit` (slowest-consumer wait); the C side resets it to 0 itself
/// in `server_reset_fast()` after every emulation.
struct SeparateShm {
control_reader: ShmemReader,
}
impl SeparateShm {
/// Opens and maps the control shared memory for a specific service.
pub fn new(shm_prefix: &str, service: AsmService) -> Result<Self> {
let name = shmem_control_output_name(shm_prefix, service);
Ok(Self {
control_reader: ShmemReader::new(&name, HintsShmem::CONTROL_PRECOMPILE_SIZE as usize)?,
})
}
}
/// Per-service semaphore resources.
struct SeparateSem {
/// Semaphore to signal data availability to this consumer
sem_available: NamedSemaphore,
/// Semaphore to wait for this consumer's data consumption
sem_read: NamedSemaphore,
}
// SAFETY: POSIX named semaphores are thread- and process-safe by spec.
unsafe impl Send for SeparateSem {}
unsafe impl Sync for SeparateSem {}
/// Unified resources shared across all asm services.
struct UnifiedResources {
/// Control shared memory writer (single write_pos)
control_writer: Arc<ControlShmem>,
/// One data writer per service — each C service has its own precompile shmem segment,
/// so Rust writes the same hint data to all of them to keep them in sync.
data_writer: ShmemWriter,
}
/// HintsShmem struct manages the writing of processed precompile hints to shared memory.
pub struct HintsShmem {
/// Number of active ASM services to notify on submit.
active_count: AtomicUsize,
/// Unified resources (single data buffer and control writer)
unified: Mutex<UnifiedResources>,
/// Per-service shmem.
separate_shm: Mutex<Vec<SeparateShm>>,
/// Per-program semaphores.
separate_sem: Mutex<Option<Vec<SeparateSem>>>,
}
impl HintsShmem {
const CONTROL_PRECOMPILE_SIZE: u64 = 0x1000; // 4KB
const MAX_PRECOMPILE_SIZE: u64 = 0x8000000; // 128MB
const BUFFER_CAPACITY_U64: u64 = Self::MAX_PRECOMPILE_SIZE >> 3;
/// Map shmem segments. Semaphores are NOT opened here; call `bind_semaphores` before use.
pub fn new(
shm_prefix: &str,
unlock_mapped_memory: bool,
control_writer: Arc<ControlShmem>,
active_services: &[AsmService],
) -> Result<Self> {
// Create unified resources (single data buffer and control writer)
let unified = Self::create_unified(shm_prefix, unlock_mapped_memory, control_writer)?;
unified.control_writer.reset()?;
// Create separate resources
let separate_shm = AsmServices::SERVICES
.iter()
.map(|service| SeparateShm::new(shm_prefix, *service))
.collect::<Result<Vec<_>>>()?;
Ok(Self {
unified: Mutex::new(unified),
separate_shm: Mutex::new(separate_shm),
separate_sem: Mutex::new(None),
active_count: AtomicUsize::new(active_services.len()),
})
}
/// Open per-service semaphores for the given program's `sem_prefix`.
/// Replaces any previously bound semaphores.
pub fn bind_semaphores(&self, sem_prefix: &str) -> Result<()> {
let sems = AsmServices::SERVICES
.iter()
.map(|service| {
let avail_name = sem_prec_available_name(sem_prefix, *service);
let read_name = sem_prec_read_name(sem_prefix, *service);
Ok(SeparateSem {
sem_available: NamedSemaphore::create(&avail_name, 0).map_err(|e| {
anyhow::anyhow!("Failed to create semaphore '{}': {}", avail_name, e)
})?,
sem_read: NamedSemaphore::create(&read_name, 0).map_err(|e| {
anyhow::anyhow!("Failed to create semaphore '{}': {}", read_name, e)
})?,
})
})
.collect::<Result<Vec<_>>>()?;
*self.separate_sem.lock().expect("separate_sem mutex poisoned") = Some(sems);
Ok(())
}
/// Drop the semaphore handles (does not unlink — the binary owns the names).
pub fn unbind_semaphores(&self) {
*self.separate_sem.lock().expect("separate_sem mutex poisoned") = None;
}
/// Update the number of active ASM services notified on each submit.
pub fn set_active_services(&self, services: &[AsmService]) -> Result<()> {
if services.len() > AsmServices::SERVICES.len() {
return Err(anyhow::anyhow!(
"active_services count {} exceeds allocated separate resources {}",
services.len(),
AsmServices::SERVICES.len()
));
}
self.active_count.store(services.len(), Ordering::SeqCst);
Ok(())
}
fn create_unified(
shm_prefix: &str,
unlock_mapped_memory: bool,
control_writer: Arc<ControlShmem>,
) -> Result<UnifiedResources> {
debug!("Initializing unified resources for precompile hints");
let name = shmem_precompile_name(shm_prefix);
let data_writer =
ShmemWriter::new(&name, Self::MAX_PRECOMPILE_SIZE as usize, unlock_mapped_memory)?;
Ok(UnifiedResources { control_writer, data_writer })
}
}
impl StreamSink for HintsShmem {
/// Writes processed precompile hints to the shared memory.
///
/// Data is written ONCE to the shared buffer, then all consumers are notified.
/// Flow control waits for the slowest consumer.
///
/// # Arguments
/// * `processed` - A vector of processed precompile hints as u64 values.
///
/// # Returns
/// * `Ok(())` - If hints were successfully written to shared memory
/// * `Err` - If writing to shared memory fails
#[inline]
fn submit(&self, processed: &[u64]) -> Result<(), StreamError> {
let data_size = processed.len() as u64;
if data_size == 0 {
return Ok(());
}
// Validate data size fits in buffer
if data_size > Self::BUFFER_CAPACITY_U64 {
return Err(StreamError::Invalid(format!(
"Processed data size ({} u64 elements) exceeds buffer capacity ({} u64 elements)",
data_size,
Self::BUFFER_CAPACITY_U64
)));
}
let mut unified = self.unified.lock().expect("unified mutex poisoned");
let separate_shm = self.separate_shm.lock().expect("separate_shm mutex poisoned");
let mut separate_sem_guard = self.separate_sem.lock().expect("separate_sem mutex poisoned");
debug_assert!(separate_sem_guard.is_some(), "submit called before bind_semaphores");
let active = self.active_count.load(Ordering::SeqCst);
let Some(separate_sem) = separate_sem_guard.as_mut() else {
return Ok(());
};
// Read current write position once
let write_pos = unified.control_writer.prec_hints_size();
// Flow control: wait until all consumers have advanced enough
// We need to wait for the slowest consumer (minimum read position)
loop {
// Ensure we observe the latest read positions
fence(Ordering::Acquire);
// Find the slowest consumer (minimum read position) and its index
let (slowest_idx, min_read_pos) = separate_shm[0..active]
.iter()
.enumerate()
.map(|(i, res)| (i, res.control_reader.read_u64_at(0)))
.min_by_key(|(_, pos)| *pos)
.unwrap();
// Calculate occupied space based on slowest consumer (saturating to avoid underflow)
debug_assert!(
write_pos >= min_read_pos,
"Write position ({}) is behind minimum read position ({})",
write_pos,
min_read_pos
);
let occupied_space = write_pos - min_read_pos;
debug_assert!(
occupied_space <= Self::BUFFER_CAPACITY_U64,
"Occupied space ({}) exceeds buffer capacity ({})",
occupied_space,
Self::BUFFER_CAPACITY_U64
);
let available_space = Self::BUFFER_CAPACITY_U64 - occupied_space;
// Flow control based on buffer occupancy
if available_space >= data_size {
break;
}
// Not enough space - wait for the SLOWEST consumer to signal progress
// Retry on interrupt (EINTR)
if separate_sem[slowest_idx].sem_read.wait().is_err() {
continue;
}
}
// Write data to each service's precompile buffer (same data, keeps all in sync)
unified.data_writer.write_ring_buffer(processed)?;
fence(Ordering::Release);
// Update write position ONCE in control memory
unified
.control_writer
.set_prec_hints_size(write_pos + data_size)
.map_err(StreamError::other)?;
fence(Ordering::Release);
// Notify ALL consumers that new data is available
for res in &mut separate_sem[0..active] {
res.sem_available.post().map_err(StreamError::other)?;
}
Ok(())
}
fn reset(&self) {
let mut unified = self.unified.lock().expect("unified mutex poisoned");
if let Err(e) = unified.control_writer.reset() {
tracing::error!("HintsShmem::reset: control flush failed: {e}");
}
unified.data_writer.reset();
// Drain any leftover semaphore counts from the previous run.
if let Some(sems) = self.separate_sem.lock().expect("separate_sem mutex poisoned").as_mut()
{
for res in sems.iter_mut() {
while res.sem_available.try_wait().is_ok() {}
while res.sem_read.try_wait().is_ok() {}
}
}
}
}