1use std::f32::consts::TAU;
7use std::sync::Arc;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use epics_pva_rs::nt::nd_array::{
11 NdAlarm, NdArrayBuffer, NdCodec, NdDimension, NdTimeStamp, NtNdArray, nt_nd_array_desc,
12 nt_nd_array_value,
13};
14use epics_pva_rs::server_native::SharedPV;
15
16pub const WIDTH: usize = 1024;
17pub const HEIGHT: usize = 1024;
18
19const FRINGE_PERIOD: usize = 128;
20const NOISE_LEN: usize = 1 << 16;
21const SPOT_RADIUS: usize = 120;
22const POOL: usize = 3;
23
24struct Spot {
25 sigma: f32,
26 amp: f32,
27 fx: f32,
29 fy: f32,
30 px: f32,
31 py: f32,
32}
33
34const SPOTS: [Spot; 3] = [
35 Spot {
36 sigma: 40.0,
37 amp: 170.0,
38 fx: 0.031,
39 fy: 0.047,
40 px: 0.0,
41 py: 1.0,
42 },
43 Spot {
44 sigma: 22.0,
45 amp: 150.0,
46 fx: 0.053,
47 fy: 0.029,
48 px: 2.0,
49 py: 0.5,
50 },
51 Spot {
52 sigma: 36.0,
53 amp: 130.0,
54 fx: 0.019,
55 fy: 0.037,
56 px: 4.0,
57 py: 3.0,
58 },
59];
60
61pub struct ImageSim {
62 fringe: Vec<u8>,
63 noise: Vec<u8>,
64 profiles: Vec<(Vec<u16>, Vec<u16>)>,
67 pool: Vec<Arc<[i8]>>,
68 rng: u32,
69}
70
71impl Default for ImageSim {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl ImageSim {
78 pub fn new() -> Self {
79 let fringe = (0..WIDTH + FRINGE_PERIOD)
80 .map(|i| {
81 let s = (TAU * i as f32 / FRINGE_PERIOD as f32).sin();
82 (50.0 + 35.0 * s) as u8
83 })
84 .collect();
85 let mut rng = 0x2545_f491;
86 let noise = (0..NOISE_LEN + WIDTH)
87 .map(|_| (xorshift(&mut rng) % 25) as u8)
88 .collect();
89 let profiles = SPOTS
90 .iter()
91 .map(|s| {
92 let g = |d: usize| {
93 let d = d as f32 - SPOT_RADIUS as f32;
94 (-d * d / (2.0 * s.sigma * s.sigma)).exp()
95 };
96 let unit = (0..=2 * SPOT_RADIUS)
97 .map(|d| (255.0 * g(d)) as u16)
98 .collect();
99 let scaled = (0..=2 * SPOT_RADIUS)
100 .map(|d| (s.amp * g(d)) as u16)
101 .collect();
102 (unit, scaled)
103 })
104 .collect();
105 ImageSim {
106 fringe,
107 noise,
108 profiles,
109 pool: Vec::new(),
110 rng,
111 }
112 }
113
114 pub fn frame(&mut self, n: u32) -> Arc<[i8]> {
117 let mut buf = match self.pool.iter().position(|b| Arc::strong_count(b) == 1) {
118 Some(i) => self.pool.swap_remove(i),
119 None => Arc::from(vec![0i8; WIDTH * HEIGHT]),
120 };
121 let px = Arc::get_mut(&mut buf).expect("unshared pool buffer");
122 let t = n as f32;
123 let shift = (n as usize * 3) % FRINGE_PERIOD;
124
125 for (y, row) in px.as_chunks_mut::<WIDTH>().0.iter_mut().enumerate() {
126 let f0 = (y + shift) % FRINGE_PERIOD;
127 let n0 = xorshift(&mut self.rng) as usize % NOISE_LEN;
128 let fringe = &self.fringe[f0..f0 + WIDTH];
129 let noise = &self.noise[n0..n0 + WIDTH];
130 for ((p, &f), &z) in row.iter_mut().zip(fringe).zip(noise) {
131 *p = (f + z) as i8;
132 }
133 }
134
135 for (s, (unit, scaled)) in SPOTS.iter().zip(&self.profiles) {
136 let margin = SPOT_RADIUS as f32 + 1.0;
137 let cx = WIDTH as f32 / 2.0 + (WIDTH as f32 / 2.0 - margin) * (s.fx * t + s.px).sin();
138 let cy = HEIGHT as f32 / 2.0 + (HEIGHT as f32 / 2.0 - margin) * (s.fy * t + s.py).sin();
139 let (x0, y0) = (cx as usize - SPOT_RADIUS, cy as usize - SPOT_RADIUS);
140 let xs = &unit[..];
141 for (dy, &gy) in scaled.iter().enumerate() {
142 let start = (y0 + dy) * WIDTH + x0;
143 let row = &mut px[start..start + xs.len()];
144 for (p, &gx) in row.iter_mut().zip(xs) {
145 let add = ((gx * gy) >> 8) as u8;
146 *p = (*p as u8).saturating_add(add) as i8;
147 }
148 }
149 }
150
151 self.pool.push(buf.clone());
152 if self.pool.len() > POOL {
153 self.pool.remove(0);
154 }
155 buf
156 }
157}
158
159fn xorshift(s: &mut u32) -> u32 {
160 *s ^= *s << 13;
161 *s ^= *s >> 17;
162 *s ^= *s << 5;
163 *s
164}
165
166fn now() -> NdTimeStamp {
167 let d = SystemTime::now()
168 .duration_since(UNIX_EPOCH)
169 .unwrap_or_default();
170 NdTimeStamp {
171 seconds_past_epoch: d.as_secs() as i64,
172 nanoseconds: d.subsec_nanos() as i32,
173 user_tag: 0,
174 }
175}
176
177fn nt(pixels: Arc<[i8]>, dims: &[usize], unique_id: i32) -> NtNdArray {
178 let ts = now();
179 let size = pixels.len() as i64;
180 NtNdArray {
181 value: NdArrayBuffer::Byte(pixels.into()),
182 codec: NdCodec::default(),
183 compressed_size: size,
184 uncompressed_size: size,
185 unique_id,
186 data_time_stamp: ts.clone(),
187 alarm: NdAlarm::default(),
188 time_stamp: ts,
189 dimension: dims
190 .iter()
191 .map(|&d| NdDimension {
192 size: d as i32,
193 full_size: d as i32,
194 binning: 1,
195 ..NdDimension::default()
196 })
197 .collect(),
198 attribute: Vec::new(),
199 }
200}
201
202pub fn open_pv() -> SharedPV {
204 let pv = SharedPV::build_readonly();
205 let empty = nt(Arc::from(Vec::new()), &[], 0);
206 pv.open(nt_nd_array_desc(), nt_nd_array_value(&empty))
207 .expect("NTNDArray value fits its descriptor");
208 pv
209}
210
211pub fn post(pv: &SharedPV, pixels: Arc<[i8]>, unique_id: i32) {
213 pv.try_post(nt_nd_array_value(&nt(pixels, &[WIDTH, HEIGHT], unique_id)));
214}