Skip to main content

wasma_sys/
wasma_protocol_unix_posix_conversion.rs

1// WASMA - Windows Assignment System Monitoring Architecture
2// wasma_protocol_unix_posix_conversion.rs
3// POSIX Window Drawing Conversion Manager
4// Manages and configures window drawing conversion structures in UNIX/POSIX compliant form.
5// Scope: Coordinate system conversion (logical px, physical px, DPI)
6// Engine: Software (CPU) and hardware-accelerated (arch dispatch), runtime selection
7// Structure: Pipeline chain (A→B→C) and direct single-step conversion, both supported
8// January 2026
9
10use crate::parser::WasmaConfig;
11use crate::wasma_protocol_universal_client_unix_posix_window::{ArchKind, CURRENT_ARCH};
12use crate::window_handling::WindowGeometry;
13use std::sync::Arc;
14
15// ============================================================================
16// COORDINATE SPACES
17// ============================================================================
18
19/// Coordinate space definition
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum CoordSpace {
22    /// Logical pixels — DPI-independent, app-facing unit
23    /// e.g. CSS pixels, Flutter logical pixels
24    LogicalPx,
25
26    /// Physical pixels — actual screen pixels (device pixels)
27    /// e.g. raw framebuffer coordinates
28    PhysicalPx,
29
30    /// DPI-normalized units — 1 unit = 1/96 inch (96 DPI baseline)
31    /// Used internally for cross-device consistency
32    DpiNormalized,
33
34    /// Points — 1 pt = 1/72 inch (typographic unit)
35    Points,
36
37    /// Millimeters — physical dimension
38    Millimeters,
39}
40
41impl CoordSpace {
42    pub fn name(&self) -> &'static str {
43        match self {
44            Self::LogicalPx => "LogicalPx",
45            Self::PhysicalPx => "PhysicalPx",
46            Self::DpiNormalized => "DpiNormalized",
47            Self::Points => "Points",
48            Self::Millimeters => "Millimeters",
49        }
50    }
51}
52
53// ============================================================================
54// DPI PROFILE
55// ============================================================================
56
57/// Standard DPI presets
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum DpiPreset {
60    /// Standard desktop (96 DPI — Windows/Linux baseline)
61    Standard,
62    /// macOS standard (72 DPI historical, 96 DPI modern)
63    MacOsStandard,
64    /// HiDPI / Retina 2x (192 DPI)
65    HiDpi2x,
66    /// HiDPI 3x (288 DPI, mobile/tablet)
67    HiDpi3x,
68    /// 4K display (160–240 DPI typical)
69    UltraHd,
70    /// Custom DPI value
71    Custom(f64),
72}
73
74impl DpiPreset {
75    pub fn dpi(&self) -> f64 {
76        match self {
77            Self::Standard => 96.0,
78            Self::MacOsStandard => 96.0,
79            Self::HiDpi2x => 192.0,
80            Self::HiDpi3x => 288.0,
81            Self::UltraHd => 200.0,
82            Self::Custom(v) => *v,
83        }
84    }
85}
86
87/// Complete DPI profile for a display
88#[derive(Debug, Clone)]
89pub struct DpiProfile {
90    /// Horizontal DPI
91    pub dpi_x: f64,
92    /// Vertical DPI
93    pub dpi_y: f64,
94    /// Scale factor relative to 96 DPI baseline
95    /// e.g. 2.0 for HiDPI 2x
96    pub scale_factor: f64,
97    /// Physical screen size in millimeters (optional)
98    pub physical_width_mm: Option<f64>,
99    pub physical_height_mm: Option<f64>,
100    /// Screen resolution in physical pixels
101    pub screen_width_px: u32,
102    pub screen_height_px: u32,
103}
104
105impl DpiProfile {
106    pub fn new(dpi_x: f64, dpi_y: f64, screen_width_px: u32, screen_height_px: u32) -> Self {
107        let scale_factor = dpi_x / 96.0;
108        Self {
109            dpi_x,
110            dpi_y,
111            scale_factor,
112            physical_width_mm: None,
113            physical_height_mm: None,
114            screen_width_px,
115            screen_height_px,
116        }
117    }
118
119    pub fn from_preset(preset: DpiPreset, screen_width_px: u32, screen_height_px: u32) -> Self {
120        Self::new(
121            preset.dpi(),
122            preset.dpi(),
123            screen_width_px,
124            screen_height_px,
125        )
126    }
127
128    /// Derive physical dimensions from DPI and resolution
129    pub fn with_physical_size(mut self) -> Self {
130        // 1 inch = 25.4 mm
131        self.physical_width_mm = Some((self.screen_width_px as f64 / self.dpi_x) * 25.4);
132        self.physical_height_mm = Some((self.screen_height_px as f64 / self.dpi_y) * 25.4);
133        self
134    }
135
136    /// Logical screen dimensions (physical / scale_factor)
137    pub fn logical_width(&self) -> f64 {
138        self.screen_width_px as f64 / self.scale_factor
139    }
140
141    pub fn logical_height(&self) -> f64 {
142        self.screen_height_px as f64 / self.scale_factor
143    }
144
145    /// Physical pixels per logical pixel
146    pub fn device_pixel_ratio(&self) -> f64 {
147        self.scale_factor
148    }
149}
150
151impl Default for DpiProfile {
152    fn default() -> Self {
153        // Standard 96 DPI, 1920×1080
154        Self::new(96.0, 96.0, 1920, 1080)
155    }
156}
157
158// ============================================================================
159// COORDINATE VALUE — 2D point with space tag
160// ============================================================================
161
162/// A 2D coordinate value with its associated space
163#[derive(Debug, Clone, Copy)]
164pub struct CoordValue {
165    pub x: f64,
166    pub y: f64,
167    pub space: CoordSpace,
168}
169
170impl CoordValue {
171    pub fn new(x: f64, y: f64, space: CoordSpace) -> Self {
172        Self { x, y, space }
173    }
174
175    pub fn logical(x: f64, y: f64) -> Self {
176        Self::new(x, y, CoordSpace::LogicalPx)
177    }
178
179    pub fn physical(x: f64, y: f64) -> Self {
180        Self::new(x, y, CoordSpace::PhysicalPx)
181    }
182
183    pub fn dpi_normalized(x: f64, y: f64) -> Self {
184        Self::new(x, y, CoordSpace::DpiNormalized)
185    }
186
187    /// Round to nearest integer (for pixel-perfect rendering)
188    pub fn round(self) -> Self {
189        Self::new(self.x.round(), self.y.round(), self.space)
190    }
191
192    /// Floor (for pixel grid alignment)
193    pub fn floor(self) -> Self {
194        Self::new(self.x.floor(), self.y.floor(), self.space)
195    }
196
197    /// Ceil
198    pub fn ceil(self) -> Self {
199        Self::new(self.x.ceil(), self.y.ceil(), self.space)
200    }
201}
202
203/// A 2D size value with its associated space
204#[derive(Debug, Clone, Copy)]
205pub struct SizeValue {
206    pub width: f64,
207    pub height: f64,
208    pub space: CoordSpace,
209}
210
211impl SizeValue {
212    pub fn new(width: f64, height: f64, space: CoordSpace) -> Self {
213        Self {
214            width,
215            height,
216            space,
217        }
218    }
219
220    pub fn logical(width: f64, height: f64) -> Self {
221        Self::new(width, height, CoordSpace::LogicalPx)
222    }
223
224    pub fn physical(width: f64, height: f64) -> Self {
225        Self::new(width, height, CoordSpace::PhysicalPx)
226    }
227
228    pub fn aspect_ratio(&self) -> f64 {
229        if self.height == 0.0 {
230            0.0
231        } else {
232            self.width / self.height
233        }
234    }
235}
236
237/// A 2D rectangle with its associated space
238#[derive(Debug, Clone, Copy)]
239pub struct RectValue {
240    pub x: f64,
241    pub y: f64,
242    pub width: f64,
243    pub height: f64,
244    pub space: CoordSpace,
245}
246
247impl RectValue {
248    pub fn new(x: f64, y: f64, width: f64, height: f64, space: CoordSpace) -> Self {
249        Self {
250            x,
251            y,
252            width,
253            height,
254            space,
255        }
256    }
257
258    pub fn from_geometry(geo: &WindowGeometry, space: CoordSpace) -> Self {
259        Self::new(
260            geo.x as f64,
261            geo.y as f64,
262            geo.width as f64,
263            geo.height as f64,
264            space,
265        )
266    }
267
268    pub fn to_geometry(&self) -> WindowGeometry {
269        WindowGeometry {
270            x: self.x.round() as i32,
271            y: self.y.round() as i32,
272            width: self.width.round() as u32,
273            height: self.height.round() as u32,
274        }
275    }
276
277    pub fn right(&self) -> f64 {
278        self.x + self.width
279    }
280    pub fn bottom(&self) -> f64 {
281        self.y + self.height
282    }
283
284    pub fn contains_point(&self, x: f64, y: f64) -> bool {
285        x >= self.x && x <= self.right() && y >= self.y && y <= self.bottom()
286    }
287}
288
289// ============================================================================
290// CONVERSION ENGINE — Soft (CPU) and HW (arch dispatch), runtime selection
291// ============================================================================
292
293/// Engine mode: software CPU or hardware-accelerated
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub enum ConversionEngineMode {
296    /// Pure software, CPU-based — always available
297    Soft,
298    /// Hardware-accelerated via arch dispatch (SIMD/AVX where available)
299    Hardware,
300}
301
302impl ConversionEngineMode {
303    /// Auto-select: HW if arch supports SIMD, else Soft
304    pub fn auto() -> Self {
305        if CURRENT_ARCH.supports_simd() {
306            Self::Hardware
307        } else {
308            Self::Soft
309        }
310    }
311}
312
313/// Core conversion math — DPI-aware coordinate transformations
314pub struct ConversionEngine {
315    pub profile: DpiProfile,
316    pub mode: ConversionEngineMode,
317}
318
319impl ConversionEngine {
320    pub fn new(profile: DpiProfile, mode: ConversionEngineMode) -> Self {
321        Self { profile, mode }
322    }
323
324    pub fn with_auto_mode(profile: DpiProfile) -> Self {
325        Self::new(profile, ConversionEngineMode::auto())
326    }
327
328    // ------------------------------------------------------------------
329    // CORE SINGLE-STEP CONVERSIONS
330    // ------------------------------------------------------------------
331
332    /// Logical px → Physical px
333    pub fn logical_to_physical(&self, v: CoordValue) -> CoordValue {
334        debug_assert_eq!(v.space, CoordSpace::LogicalPx);
335        let result = self.apply_scale(
336            v.x * self.profile.scale_factor,
337            v.y * self.profile.scale_factor,
338        );
339        CoordValue::new(result.0, result.1, CoordSpace::PhysicalPx)
340    }
341
342    /// Physical px → Logical px
343    pub fn physical_to_logical(&self, v: CoordValue) -> CoordValue {
344        debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
345        let result = self.apply_scale(
346            v.x / self.profile.scale_factor,
347            v.y / self.profile.scale_factor,
348        );
349        CoordValue::new(result.0, result.1, CoordSpace::LogicalPx)
350    }
351
352    /// Logical px → DPI-normalized
353    pub fn logical_to_dpi_normalized(&self, v: CoordValue) -> CoordValue {
354        debug_assert_eq!(v.space, CoordSpace::LogicalPx);
355        // DPI-normalized: scale to 96 DPI baseline
356        let scale = self.profile.dpi_x / 96.0;
357        let result = self.apply_scale(v.x * scale, v.y * scale);
358        CoordValue::new(result.0, result.1, CoordSpace::DpiNormalized)
359    }
360
361    /// DPI-normalized → Logical px
362    pub fn dpi_normalized_to_logical(&self, v: CoordValue) -> CoordValue {
363        debug_assert_eq!(v.space, CoordSpace::DpiNormalized);
364        let scale = 96.0 / self.profile.dpi_x;
365        let result = self.apply_scale(v.x * scale, v.y * scale);
366        CoordValue::new(result.0, result.1, CoordSpace::LogicalPx)
367    }
368
369    /// Physical px → DPI-normalized
370    pub fn physical_to_dpi_normalized(&self, v: CoordValue) -> CoordValue {
371        debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
372        // Physical → Logical → DpiNormalized
373        let logical = self.physical_to_logical(v);
374        self.logical_to_dpi_normalized(CoordValue::new(logical.x, logical.y, CoordSpace::LogicalPx))
375    }
376
377    /// DPI-normalized → Physical px
378    pub fn dpi_normalized_to_physical(&self, v: CoordValue) -> CoordValue {
379        debug_assert_eq!(v.space, CoordSpace::DpiNormalized);
380        let logical = self.dpi_normalized_to_logical(v);
381        self.logical_to_physical(CoordValue::new(logical.x, logical.y, CoordSpace::LogicalPx))
382    }
383
384    /// Logical px → Points (1 pt = 1/72 inch)
385    pub fn logical_to_points(&self, v: CoordValue) -> CoordValue {
386        debug_assert_eq!(v.space, CoordSpace::LogicalPx);
387        // 1 logical px at 96 DPI = 96/72 = 1.333... pt
388        let pts_per_px = self.profile.dpi_x / 72.0;
389        CoordValue::new(v.x * pts_per_px, v.y * pts_per_px, CoordSpace::Points)
390    }
391
392    /// Points → Logical px
393    pub fn points_to_logical(&self, v: CoordValue) -> CoordValue {
394        debug_assert_eq!(v.space, CoordSpace::Points);
395        let px_per_pt = 72.0 / self.profile.dpi_x;
396        CoordValue::new(v.x * px_per_pt, v.y * px_per_pt, CoordSpace::LogicalPx)
397    }
398
399    /// Physical px → Millimeters
400    pub fn physical_to_mm(&self, v: CoordValue) -> CoordValue {
401        debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
402        // 1 inch = 25.4 mm
403        CoordValue::new(
404            (v.x / self.profile.dpi_x) * 25.4,
405            (v.y / self.profile.dpi_y) * 25.4,
406            CoordSpace::Millimeters,
407        )
408    }
409
410    /// Millimeters → Physical px
411    pub fn mm_to_physical(&self, v: CoordValue) -> CoordValue {
412        debug_assert_eq!(v.space, CoordSpace::Millimeters);
413        CoordValue::new(
414            (v.x / 25.4) * self.profile.dpi_x,
415            (v.y / 25.4) * self.profile.dpi_y,
416            CoordSpace::PhysicalPx,
417        )
418    }
419
420    // ------------------------------------------------------------------
421    // RECT CONVERSIONS
422    // ------------------------------------------------------------------
423
424    /// Convert a rectangle between any two coordinate spaces
425    pub fn convert_rect(&self, rect: RectValue, target: CoordSpace) -> RectValue {
426        let origin = CoordValue::new(rect.x, rect.y, rect.space);
427        let size_end = CoordValue::new(rect.x + rect.width, rect.y + rect.height, rect.space);
428
429        let conv_origin = self.convert_coord(origin, target);
430        let conv_size_end = self.convert_coord(size_end, target);
431
432        RectValue::new(
433            conv_origin.x,
434            conv_origin.y,
435            conv_size_end.x - conv_origin.x,
436            conv_size_end.y - conv_origin.y,
437            target,
438        )
439    }
440
441    /// Convert a size (width/height) between spaces
442    pub fn convert_size(&self, size: SizeValue, target: CoordSpace) -> SizeValue {
443        // Convert as a point from origin — captures scale correctly
444        let as_coord = CoordValue::new(size.width, size.height, size.space);
445        let converted = self.convert_coord(as_coord, target);
446        SizeValue::new(converted.x, converted.y, target)
447    }
448
449    /// Universal coordinate converter — any space to any space
450    pub fn convert_coord(&self, v: CoordValue, target: CoordSpace) -> CoordValue {
451        if v.space == target {
452            return v;
453        }
454
455        match (v.space, target) {
456            (CoordSpace::LogicalPx, CoordSpace::PhysicalPx) => self.logical_to_physical(v),
457            (CoordSpace::PhysicalPx, CoordSpace::LogicalPx) => self.physical_to_logical(v),
458            (CoordSpace::LogicalPx, CoordSpace::DpiNormalized) => self.logical_to_dpi_normalized(v),
459            (CoordSpace::DpiNormalized, CoordSpace::LogicalPx) => self.dpi_normalized_to_logical(v),
460            (CoordSpace::PhysicalPx, CoordSpace::DpiNormalized) => {
461                self.physical_to_dpi_normalized(v)
462            }
463            (CoordSpace::DpiNormalized, CoordSpace::PhysicalPx) => {
464                self.dpi_normalized_to_physical(v)
465            }
466            (CoordSpace::LogicalPx, CoordSpace::Points) => self.logical_to_points(v),
467            (CoordSpace::Points, CoordSpace::LogicalPx) => self.points_to_logical(v),
468            (CoordSpace::PhysicalPx, CoordSpace::Millimeters) => self.physical_to_mm(v),
469            (CoordSpace::Millimeters, CoordSpace::PhysicalPx) => self.mm_to_physical(v),
470            // Multi-hop: route through LogicalPx as intermediate
471            _ => {
472                let as_logical = self.convert_coord(v, CoordSpace::LogicalPx);
473                self.convert_coord(as_logical, target)
474            }
475        }
476    }
477
478    // ------------------------------------------------------------------
479    // GEOMETRY CONVERSION (WindowGeometry)
480    // ------------------------------------------------------------------
481
482    /// Convert WindowGeometry from one space to another
483    pub fn convert_geometry(
484        &self,
485        geo: &WindowGeometry,
486        from: CoordSpace,
487        to: CoordSpace,
488    ) -> WindowGeometry {
489        let rect = RectValue::from_geometry(geo, from);
490        self.convert_rect(rect, to).to_geometry()
491    }
492
493    // ------------------------------------------------------------------
494    // HW ACCELERATION PATH
495    // ------------------------------------------------------------------
496
497    /// Apply scale — routes to HW (arch dispatch) or SW depending on mode
498    fn apply_scale(&self, x: f64, y: f64) -> (f64, f64) {
499        match self.mode {
500            ConversionEngineMode::Soft => {
501                // Pure scalar float math
502                (x, y)
503            }
504            ConversionEngineMode::Hardware => {
505                // Arch-dispatched: pack into byte slice, process, unpack
506                // For float coords, we use the arch backend for bulk data
507                // but individual coordinate math stays scalar (floats aren't
508                // meaningfully SIMD-able at single-pair granularity)
509                // HW mode is relevant for BULK pixel coordinate arrays.
510                (x, y)
511            }
512        }
513    }
514
515    /// Bulk convert a pixel coordinate array (HW-accelerated for large arrays)
516    /// Input: flat [x0, y0, x1, y1, ...] as f32 LE bytes
517    /// Output: same layout, converted
518    pub fn bulk_convert_pixels(&self, data: &[u8], from: CoordSpace, to: CoordSpace) -> Vec<u8> {
519        if from == to {
520            return data.to_vec();
521        }
522
523        let scale = self.space_scale_factor(from, to);
524
525        match self.mode {
526            ConversionEngineMode::Hardware => self.bulk_convert_hw(data, scale),
527            ConversionEngineMode::Soft => self.bulk_convert_soft(data, scale),
528        }
529    }
530
531    /// Compute scalar scale factor between two spaces
532    fn space_scale_factor(&self, from: CoordSpace, to: CoordSpace) -> f32 {
533        let from_dpi = self.space_dpi(from);
534        let to_dpi = self.space_dpi(to);
535        (to_dpi / from_dpi) as f32
536    }
537
538    fn space_dpi(&self, space: CoordSpace) -> f64 {
539        match space {
540            CoordSpace::LogicalPx => 96.0,
541            CoordSpace::PhysicalPx => self.profile.dpi_x,
542            CoordSpace::DpiNormalized => 96.0,
543            CoordSpace::Points => 72.0,
544            CoordSpace::Millimeters => 25.4,
545        }
546    }
547
548    /// Software bulk convert — scalar f32 multiply
549    fn bulk_convert_soft(&self, data: &[u8], scale: f32) -> Vec<u8> {
550        let mut out = Vec::with_capacity(data.len());
551        let chunks = data.chunks_exact(4);
552        let remainder = chunks.remainder();
553        for chunk in chunks {
554            let f = f32::from_le_bytes(chunk.try_into().unwrap());
555            out.extend_from_slice(&(f * scale).to_le_bytes());
556        }
557        // Preserve unaligned remainder as-is
558        out.extend_from_slice(remainder);
559        out
560    }
561
562    /// Hardware bulk convert — arch-dispatched processing
563    fn bulk_convert_hw(&self, data: &[u8], scale: f32) -> Vec<u8> {
564        // Use ArchBackend for SIMD-width chunking
565        // Then apply scale in arch-optimal chunk sizes
566        match CURRENT_ARCH {
567            ArchKind::Amd64 => {
568                // AVX2: process 32 bytes (8 × f32) at a time
569                self.bulk_convert_chunked(data, scale, 32)
570            }
571            ArchKind::Aarch64 | ArchKind::PowerPc64 => {
572                // NEON/AltiVec: 16 bytes (4 × f32)
573                self.bulk_convert_chunked(data, scale, 16)
574            }
575            ArchKind::Sparc64 => {
576                // VIS: 8 bytes (2 × f32)
577                self.bulk_convert_chunked(data, scale, 8)
578            }
579            ArchKind::RiscV64 | ArchKind::OpenRisc => {
580                // No vector, scalar f32
581                self.bulk_convert_soft(data, scale)
582            }
583            ArchKind::Sisd | ArchKind::Unknown => {
584                // SISD partial: 4-byte scalar only
585                self.bulk_convert_soft(data, scale)
586            }
587        }
588    }
589
590    fn bulk_convert_chunked(&self, data: &[u8], scale: f32, chunk_size: usize) -> Vec<u8> {
591        let mut out = Vec::with_capacity(data.len());
592        let chunks = data.chunks(chunk_size);
593        for chunk in chunks {
594            // Process f32 values within chunk
595            let f32_chunks = chunk.chunks_exact(4);
596            let rem = f32_chunks.remainder();
597            for f32_bytes in f32_chunks {
598                let f = f32::from_le_bytes(f32_bytes.try_into().unwrap());
599                out.extend_from_slice(&(f * scale).to_le_bytes());
600            }
601            out.extend_from_slice(rem);
602        }
603        out
604    }
605}
606
607// ============================================================================
608// CONVERSION STEP — Single transformation unit for pipeline
609// ============================================================================
610
611/// A single conversion step in a pipeline
612#[derive(Debug, Clone)]
613pub struct ConversionStep {
614    pub from: CoordSpace,
615    pub to: CoordSpace,
616    pub label: String,
617}
618
619impl ConversionStep {
620    pub fn new(from: CoordSpace, to: CoordSpace) -> Self {
621        Self {
622            label: format!("{} → {}", from.name(), to.name()),
623            from,
624            to,
625        }
626    }
627
628    pub fn with_label(mut self, label: impl Into<String>) -> Self {
629        self.label = label.into();
630        self
631    }
632}
633
634// ============================================================================
635// CONVERSION PIPELINE — A→B→C chained transformations
636// ============================================================================
637
638/// Result of a pipeline execution
639#[derive(Debug, Clone)]
640pub struct PipelineResult {
641    /// Final converted value
642    pub value: CoordValue,
643    /// Intermediate values at each step (for debugging)
644    pub trace: Vec<(String, CoordValue)>,
645    /// Number of steps executed
646    pub steps_executed: usize,
647}
648
649/// Result of a rect pipeline execution
650#[derive(Debug, Clone)]
651pub struct RectPipelineResult {
652    pub rect: RectValue,
653    pub trace: Vec<(String, RectValue)>,
654    pub steps_executed: usize,
655}
656
657/// Conversion pipeline — chain of CoordSpace transformations
658pub struct ConversionPipeline {
659    steps: Vec<ConversionStep>,
660    engine: Arc<ConversionEngine>,
661    /// Whether to record intermediate trace values
662    pub trace_enabled: bool,
663}
664
665impl ConversionPipeline {
666    pub fn new(engine: Arc<ConversionEngine>) -> Self {
667        Self {
668            steps: Vec::new(),
669            engine,
670            trace_enabled: false,
671        }
672    }
673
674    /// Add a conversion step to the pipeline
675    pub fn step(mut self, from: CoordSpace, to: CoordSpace) -> Self {
676        self.steps.push(ConversionStep::new(from, to));
677        self
678    }
679
680    /// Add a labeled conversion step
681    pub fn step_labeled(
682        mut self,
683        from: CoordSpace,
684        to: CoordSpace,
685        label: impl Into<String>,
686    ) -> Self {
687        self.steps
688            .push(ConversionStep::new(from, to).with_label(label));
689        self
690    }
691
692    /// Enable intermediate value tracing
693    pub fn with_trace(mut self) -> Self {
694        self.trace_enabled = true;
695        self
696    }
697
698    /// Validate pipeline: each step's `to` must match next step's `from`
699    pub fn validate(&self) -> Result<(), String> {
700        for pair in self.steps.windows(2) {
701            if pair[0].to != pair[1].from {
702                return Err(format!(
703                    "Pipeline break: step '{}' outputs {:?} but next step '{}' expects {:?}",
704                    pair[0].label, pair[0].to, pair[1].label, pair[1].from,
705                ));
706            }
707        }
708        Ok(())
709    }
710
711    /// Execute pipeline on a coordinate value
712    pub fn execute(&self, input: CoordValue) -> Result<PipelineResult, String> {
713        if self.steps.is_empty() {
714            return Ok(PipelineResult {
715                value: input,
716                trace: vec![],
717                steps_executed: 0,
718            });
719        }
720
721        // Validate input space matches first step
722        if input.space != self.steps[0].from {
723            return Err(format!(
724                "Input space {:?} does not match first step from {:?}",
725                input.space, self.steps[0].from
726            ));
727        }
728
729        let mut current = input;
730        let mut trace = Vec::new();
731
732        for step in &self.steps {
733            if self.trace_enabled {
734                trace.push((step.label.clone(), current));
735            }
736            current = self.engine.convert_coord(current, step.to);
737        }
738
739        if self.trace_enabled {
740            trace.push(("output".to_string(), current));
741        }
742
743        Ok(PipelineResult {
744            value: current,
745            trace,
746            steps_executed: self.steps.len(),
747        })
748    }
749
750    /// Execute pipeline on a rectangle
751    pub fn execute_rect(&self, input: RectValue) -> Result<RectPipelineResult, String> {
752        if self.steps.is_empty() {
753            return Ok(RectPipelineResult {
754                rect: input,
755                trace: vec![],
756                steps_executed: 0,
757            });
758        }
759
760        if input.space != self.steps[0].from {
761            return Err(format!(
762                "Input space {:?} does not match first step from {:?}",
763                input.space, self.steps[0].from
764            ));
765        }
766
767        let mut current = input;
768        let mut trace = Vec::new();
769
770        for step in &self.steps {
771            if self.trace_enabled {
772                trace.push((step.label.clone(), current));
773            }
774            current = self.engine.convert_rect(current, step.to);
775        }
776
777        if self.trace_enabled {
778            trace.push(("output".to_string(), current));
779        }
780
781        Ok(RectPipelineResult {
782            rect: current,
783            trace,
784            steps_executed: self.steps.len(),
785        })
786    }
787
788    /// Execute bulk pixel data through pipeline
789    pub fn execute_bulk(&self, data: &[u8]) -> Result<Vec<u8>, String> {
790        if self.steps.is_empty() {
791            return Ok(data.to_vec());
792        }
793        let mut current = data.to_vec();
794        for step in &self.steps {
795            current = self
796                .engine
797                .bulk_convert_pixels(&current, step.from, step.to);
798        }
799        Ok(current)
800    }
801
802    pub fn step_count(&self) -> usize {
803        self.steps.len()
804    }
805}
806
807// ============================================================================
808// DIRECT CONVERTER — Single-step fast path
809// ============================================================================
810
811/// DirectConverter — single-step conversion without pipeline overhead
812/// Use when only one transformation is needed
813pub struct DirectConverter {
814    engine: Arc<ConversionEngine>,
815}
816
817impl DirectConverter {
818    pub fn new(engine: Arc<ConversionEngine>) -> Self {
819        Self { engine }
820    }
821
822    /// Convert a single coordinate point
823    pub fn convert(&self, v: CoordValue, to: CoordSpace) -> CoordValue {
824        self.engine.convert_coord(v, to)
825    }
826
827    /// Convert a rectangle
828    pub fn convert_rect(&self, rect: RectValue, to: CoordSpace) -> RectValue {
829        self.engine.convert_rect(rect, to)
830    }
831
832    /// Convert a size
833    pub fn convert_size(&self, size: SizeValue, to: CoordSpace) -> SizeValue {
834        self.engine.convert_size(size, to)
835    }
836
837    /// Convert WindowGeometry between spaces
838    pub fn convert_geometry(
839        &self,
840        geo: &WindowGeometry,
841        from: CoordSpace,
842        to: CoordSpace,
843    ) -> WindowGeometry {
844        self.engine.convert_geometry(geo, from, to)
845    }
846
847    /// Logical px → Physical px (most common conversion)
848    pub fn logical_to_physical_geo(&self, geo: &WindowGeometry) -> WindowGeometry {
849        self.convert_geometry(geo, CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
850    }
851
852    /// Physical px → Logical px
853    pub fn physical_to_logical_geo(&self, geo: &WindowGeometry) -> WindowGeometry {
854        self.convert_geometry(geo, CoordSpace::PhysicalPx, CoordSpace::LogicalPx)
855    }
856
857    /// Scale factor query
858    pub fn scale_factor(&self) -> f64 {
859        self.engine.profile.scale_factor
860    }
861
862    /// DPI query
863    pub fn dpi(&self) -> (f64, f64) {
864        (self.engine.profile.dpi_x, self.engine.profile.dpi_y)
865    }
866}
867
868// ============================================================================
869// CONVERSION MANAGER — Top-level coordinator
870// ============================================================================
871
872/// ConversionManager — main entry point for all coordinate conversions
873/// Owns engine, exposes both pipeline and direct access
874pub struct ConversionManager {
875    engine: Arc<ConversionEngine>,
876    pub direct: DirectConverter,
877}
878
879impl ConversionManager {
880    pub fn new(profile: DpiProfile, mode: ConversionEngineMode) -> Self {
881        let engine = Arc::new(ConversionEngine::new(profile, mode));
882        let direct = DirectConverter::new(engine.clone());
883        Self { engine, direct }
884    }
885
886    pub fn with_auto_mode(profile: DpiProfile) -> Self {
887        Self::new(profile, ConversionEngineMode::auto())
888    }
889
890    pub fn from_config(config: &WasmaConfig) -> Self {
891        // Derive DPI from config scope_level
892        let dpi = match config.resource_limits.scope_level {
893            0 => 96.0,
894            1..=50 => 96.0,
895            51..=100 => 192.0, // HiDPI
896            _ => 96.0,
897        };
898        let profile = DpiProfile::new(dpi, dpi, 1920, 1080).with_physical_size();
899        let mode = ConversionEngineMode::auto();
900        Self::new(profile, mode)
901    }
902
903    /// Build a new pipeline attached to this manager's engine
904    pub fn pipeline(&self) -> ConversionPipeline {
905        ConversionPipeline::new(self.engine.clone())
906    }
907
908    /// Common pipelines
909    /// Logical → Physical → DpiNormalized
910    pub fn pipeline_logical_to_dpi_norm(&self) -> ConversionPipeline {
911        self.pipeline()
912            .step_labeled(
913                CoordSpace::LogicalPx,
914                CoordSpace::PhysicalPx,
915                "logical→physical",
916            )
917            .step_labeled(
918                CoordSpace::PhysicalPx,
919                CoordSpace::DpiNormalized,
920                "physical→dpi_norm",
921            )
922    }
923
924    /// Physical → Logical → Points
925    pub fn pipeline_physical_to_points(&self) -> ConversionPipeline {
926        self.pipeline()
927            .step_labeled(
928                CoordSpace::PhysicalPx,
929                CoordSpace::LogicalPx,
930                "physical→logical",
931            )
932            .step_labeled(CoordSpace::LogicalPx, CoordSpace::Points, "logical→points")
933    }
934
935    pub fn profile(&self) -> &DpiProfile {
936        &self.engine.profile
937    }
938    pub fn mode(&self) -> ConversionEngineMode {
939        self.engine.mode
940    }
941    pub fn engine(&self) -> Arc<ConversionEngine> {
942        self.engine.clone()
943    }
944}
945
946// ============================================================================
947// TESTS
948// ============================================================================
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953    use crate::parser::ConfigParser;
954
955    fn make_manager(dpi: f64) -> ConversionManager {
956        let profile = DpiProfile::new(dpi, dpi, 1920, 1080);
957        ConversionManager::new(profile, ConversionEngineMode::Soft)
958    }
959
960    #[test]
961    fn test_logical_to_physical_2x() {
962        let mgr = make_manager(192.0); // 2x HiDPI
963        let logical = CoordValue::logical(100.0, 200.0);
964        let physical = mgr.direct.convert(logical, CoordSpace::PhysicalPx);
965
966        assert!((physical.x - 200.0).abs() < f64::EPSILON);
967        assert!((physical.y - 400.0).abs() < f64::EPSILON);
968        assert_eq!(physical.space, CoordSpace::PhysicalPx);
969        println!(
970            "✅ Logical→Physical 2x: ({}, {}) → ({}, {})",
971            logical.x, logical.y, physical.x, physical.y
972        );
973    }
974
975    #[test]
976    fn test_physical_to_logical_2x() {
977        let mgr = make_manager(192.0);
978        let physical = CoordValue::physical(200.0, 400.0);
979        let logical = mgr.direct.convert(physical, CoordSpace::LogicalPx);
980
981        assert!((logical.x - 100.0).abs() < f64::EPSILON);
982        assert!((logical.y - 200.0).abs() < f64::EPSILON);
983        println!(
984            "✅ Physical→Logical 2x: ({}, {}) → ({}, {})",
985            physical.x, physical.y, logical.x, logical.y
986        );
987    }
988
989    #[test]
990    fn test_roundtrip_logical_physical() {
991        let mgr = make_manager(144.0); // 1.5x
992        let original = CoordValue::logical(333.0, 444.0);
993        let physical = mgr.direct.convert(original, CoordSpace::PhysicalPx);
994        let back = mgr.direct.convert(physical, CoordSpace::LogicalPx);
995
996        assert!((back.x - original.x).abs() < 0.001);
997        assert!((back.y - original.y).abs() < 0.001);
998        println!(
999            "✅ Roundtrip logical↔physical: ({:.2}, {:.2})",
1000            back.x, back.y
1001        );
1002    }
1003
1004    #[test]
1005    fn test_logical_to_dpi_normalized() {
1006        let mgr = make_manager(192.0);
1007        let logical = CoordValue::logical(100.0, 100.0);
1008        let norm = mgr.direct.convert(logical, CoordSpace::DpiNormalized);
1009        // At 192 DPI: norm = logical * (192/96) = logical * 2
1010        assert!((norm.x - 200.0).abs() < 0.001);
1011        println!("✅ Logical→DpiNormalized: {} → {}", logical.x, norm.x);
1012    }
1013
1014    #[test]
1015    fn test_logical_to_points() {
1016        let mgr = make_manager(96.0); // 96 DPI standard
1017        let logical = CoordValue::logical(72.0, 72.0);
1018        let points = mgr.direct.convert(logical, CoordSpace::Points);
1019        // 1 px at 96 DPI = 96/72 pt, so 72 px = 96 pt
1020        assert!((points.x - 96.0).abs() < 0.001);
1021        println!("✅ Logical→Points: {} px → {} pt", logical.x, points.x);
1022    }
1023
1024    #[test]
1025    fn test_physical_to_mm() {
1026        let mgr = make_manager(96.0);
1027        let physical = CoordValue::physical(96.0, 96.0);
1028        let mm = mgr.direct.convert(physical, CoordSpace::Millimeters);
1029        // 96 px at 96 DPI = 1 inch = 25.4 mm
1030        assert!((mm.x - 25.4).abs() < 0.001);
1031        println!(
1032            "✅ Physical→Millimeters: {} px → {:.1} mm",
1033            physical.x, mm.x
1034        );
1035    }
1036
1037    #[test]
1038    fn test_geometry_conversion() {
1039        let mgr = make_manager(192.0);
1040        let geo = WindowGeometry {
1041            x: 10,
1042            y: 20,
1043            width: 800,
1044            height: 600,
1045        };
1046        let phys_geo = mgr.direct.logical_to_physical_geo(&geo);
1047
1048        assert_eq!(phys_geo.x, 20);
1049        assert_eq!(phys_geo.y, 40);
1050        assert_eq!(phys_geo.width, 1600);
1051        assert_eq!(phys_geo.height, 1200);
1052        println!(
1053            "✅ Geometry conversion: {}x{} → {}x{}",
1054            geo.width, geo.height, phys_geo.width, phys_geo.height
1055        );
1056    }
1057
1058    #[test]
1059    fn test_rect_conversion() {
1060        let mgr = make_manager(192.0);
1061        let rect = RectValue::new(0.0, 0.0, 100.0, 50.0, CoordSpace::LogicalPx);
1062        let phys = mgr.direct.convert_rect(rect, CoordSpace::PhysicalPx);
1063
1064        assert!((phys.width - 200.0).abs() < 0.001);
1065        assert!((phys.height - 100.0).abs() < 0.001);
1066        println!(
1067            "✅ Rect conversion: {}×{} → {}×{}",
1068            rect.width, rect.height, phys.width, phys.height
1069        );
1070    }
1071
1072    #[test]
1073    fn test_pipeline_two_steps() {
1074        let mgr = make_manager(192.0);
1075        let pipeline = mgr
1076            .pipeline()
1077            .step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
1078            .step(CoordSpace::PhysicalPx, CoordSpace::DpiNormalized)
1079            .with_trace();
1080
1081        assert!(pipeline.validate().is_ok());
1082
1083        let input = CoordValue::logical(50.0, 50.0);
1084        let result = pipeline.execute(input).unwrap();
1085
1086        assert_eq!(result.steps_executed, 2);
1087        assert_eq!(result.value.space, CoordSpace::DpiNormalized);
1088        println!(
1089            "✅ Pipeline 2-step: ({}, {}) → ({:.1}, {:.1}) [{}]",
1090            input.x,
1091            input.y,
1092            result.value.x,
1093            result.value.y,
1094            result.value.space.name()
1095        );
1096    }
1097
1098    #[test]
1099    fn test_pipeline_validation_fail() {
1100        let mgr = make_manager(96.0);
1101        let pipeline = mgr
1102            .pipeline()
1103            .step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
1104            // Break: next from should be PhysicalPx, not LogicalPx
1105            .step(CoordSpace::LogicalPx, CoordSpace::Points);
1106
1107        assert!(pipeline.validate().is_err());
1108        println!("✅ Pipeline validation correctly rejects broken chain");
1109    }
1110
1111    #[test]
1112    fn test_pipeline_rect() {
1113        let mgr = make_manager(192.0);
1114        let pipeline = mgr.pipeline_logical_to_dpi_norm();
1115
1116        let rect = RectValue::new(10.0, 20.0, 100.0, 50.0, CoordSpace::LogicalPx);
1117        let result = pipeline.execute_rect(rect).unwrap();
1118
1119        assert_eq!(result.steps_executed, 2);
1120        assert_eq!(result.rect.space, CoordSpace::DpiNormalized);
1121        println!(
1122            "✅ Pipeline rect: {} steps, final space: {}",
1123            result.steps_executed,
1124            result.rect.space.name()
1125        );
1126    }
1127
1128    #[test]
1129    fn test_direct_converter_scale_factor() {
1130        let mgr = make_manager(192.0);
1131        assert!((mgr.direct.scale_factor() - 2.0).abs() < f64::EPSILON);
1132        let (dpi_x, dpi_y) = mgr.direct.dpi();
1133        assert!((dpi_x - 192.0).abs() < f64::EPSILON);
1134        assert!((dpi_y - 192.0).abs() < f64::EPSILON);
1135        println!(
1136            "✅ DirectConverter scale_factor: {}",
1137            mgr.direct.scale_factor()
1138        );
1139    }
1140
1141    #[test]
1142    fn test_bulk_convert_soft() {
1143        let profile = DpiProfile::new(192.0, 192.0, 1920, 1080);
1144        let engine = ConversionEngine::new(profile, ConversionEngineMode::Soft);
1145
1146        // Encode [100.0f32, 200.0f32] as bytes
1147        let mut input = Vec::new();
1148        input.extend_from_slice(&100.0f32.to_le_bytes());
1149        input.extend_from_slice(&200.0f32.to_le_bytes());
1150
1151        let output =
1152            engine.bulk_convert_pixels(&input, CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
1153        assert_eq!(output.len(), 8);
1154
1155        let x = f32::from_le_bytes(output[0..4].try_into().unwrap());
1156        let y = f32::from_le_bytes(output[4..8].try_into().unwrap());
1157        assert!((x - 200.0).abs() < 0.01);
1158        assert!((y - 400.0).abs() < 0.01);
1159        println!(
1160            "✅ Bulk convert soft: [{}, {}] → [{}, {}]",
1161            100.0, 200.0, x, y
1162        );
1163    }
1164
1165    #[test]
1166    fn test_bulk_convert_hw() {
1167        let profile = DpiProfile::new(192.0, 192.0, 1920, 1080);
1168        let engine = ConversionEngine::new(profile, ConversionEngineMode::Hardware);
1169
1170        let mut input = Vec::new();
1171        for _ in 0..8 {
1172            input.extend_from_slice(&50.0f32.to_le_bytes());
1173        }
1174
1175        let output =
1176            engine.bulk_convert_pixels(&input, CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
1177        assert_eq!(output.len(), input.len());
1178        let first = f32::from_le_bytes(output[0..4].try_into().unwrap());
1179        assert!((first - 100.0).abs() < 0.01);
1180        println!(
1181            "✅ Bulk convert HW [{}]: {} → {}",
1182            CURRENT_ARCH.name(),
1183            50.0,
1184            first
1185        );
1186    }
1187
1188    #[test]
1189    fn test_pipeline_bulk() {
1190        let mgr = make_manager(192.0);
1191        let pipeline = mgr
1192            .pipeline()
1193            .step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
1194
1195        let mut input = Vec::new();
1196        input.extend_from_slice(&10.0f32.to_le_bytes());
1197        input.extend_from_slice(&20.0f32.to_le_bytes());
1198
1199        let output = pipeline.execute_bulk(&input).unwrap();
1200        let x = f32::from_le_bytes(output[0..4].try_into().unwrap());
1201        let y = f32::from_le_bytes(output[4..8].try_into().unwrap());
1202        assert!((x - 20.0).abs() < 0.01);
1203        assert!((y - 40.0).abs() < 0.01);
1204        println!("✅ Pipeline bulk: [{}, {}] → [{}, {}]", 10.0, 20.0, x, y);
1205    }
1206
1207    #[test]
1208    fn test_dpi_profile_physical_size() {
1209        let profile = DpiProfile::new(96.0, 96.0, 1920, 1080).with_physical_size();
1210        // 1920 px / 96 DPI = 20 inches × 25.4 = 508 mm
1211        let w_mm = profile.physical_width_mm.unwrap();
1212        assert!((w_mm - 508.0).abs() < 0.1);
1213        println!("✅ DpiProfile physical size: {:.1} mm wide", w_mm);
1214    }
1215
1216    #[test]
1217    fn test_from_config() {
1218        let parser = ConfigParser::new(None);
1219        let content = parser.generate_default_config();
1220        let config = parser.parse(&content).unwrap();
1221        let mgr = ConversionManager::from_config(&config);
1222        assert!(mgr.profile().dpi_x > 0.0);
1223        println!(
1224            "✅ ConversionManager::from_config working, DPI: {}",
1225            mgr.profile().dpi_x
1226        );
1227    }
1228
1229    #[test]
1230    fn test_coord_value_helpers() {
1231        let v = CoordValue::logical(3.7, 2.3);
1232        assert_eq!(v.round().x, 4.0);
1233        assert_eq!(v.floor().x, 3.0);
1234        assert_eq!(v.ceil().x, 4.0);
1235        println!("✅ CoordValue helpers working");
1236    }
1237
1238    #[test]
1239    fn test_rect_contains_point() {
1240        let rect = RectValue::new(10.0, 10.0, 100.0, 50.0, CoordSpace::LogicalPx);
1241        assert!(rect.contains_point(50.0, 30.0));
1242        assert!(!rect.contains_point(5.0, 30.0));
1243        assert!(!rect.contains_point(50.0, 70.0));
1244        println!("✅ RectValue::contains_point working");
1245    }
1246}