alice-sdf 1.13.0

ALICE-SDF: 3D/Spatial Data Specialist — Store laws, not polygons. GLSL/WGSL/HLSL transpile, SVO, GPU marching cubes, texture-fit, procedural CSG. Optional bridges to alice-physics / alice-codec / libasp / alice-cache behind features.
Documentation
//! ALICE-Streaming-Protocol bridge: SDF scene delivery via ASP packets
//!
//! Packages SDF trees into ASP I-packets (full scene) and D-packets
//! (delta updates) for real-time procedural scene streaming.
//!
//! # Example
//!
//! ```ignore
//! use alice_sdf::asp_bridge::*;
//! use alice_sdf::prelude::*;
//!
//! let tree = SdfTree::new(SdfNode::sphere(1.0));
//! let packet = create_sdf_i_packet(&tree, 1).unwrap();
//! let bytes = packet.to_bytes().unwrap();
//! // Send `bytes` over network
//!
//! // On receiver:
//! let recovered = decode_sdf_i_packet(&bytes).unwrap();
//! ```

use libasp::{
    AspPacket, AspResult, Color, ColorPalette, DPacketPayload, IPacketPayload, QualityLevel, Rect,
    RegionDescriptor,
};

use crate::types::SdfTree;

/// Create an ASP I-packet containing a full SDF scene.
///
/// The SDF tree is serialized to ASDF binary format and embedded
/// in the I-packet's region descriptors as a `Complex` pattern.
/// bincode allocation upper bound (256 MB) fuzz 検出の capacity overflow panic 予防
/// [[karikari-review]] : src/io/asdf.rs::ASDF_BINCODE_LIMIT と同期
const ASP_BINCODE_LIMIT: usize = 256 * 1024 * 1024;

/// Serialise `tree` (ASDF bincode wire format) into an ASP I-packet with the
/// given `sequence` number.
///
/// # Errors
///
/// Returns the `libasp` error when the tree does not fit the packet or the
/// serialisation exceeds the 256 MiB bincode limit.
pub fn create_sdf_i_packet(tree: &SdfTree, sequence: u32) -> AspResult<AspPacket> {
    // Serialize SDF tree to binary via bincode (same format as ASDF files)
    // bincode 2 API: serde compat + legacy config で bincode 1 wire format 互換
    // with_limit で DoS 予防 (fuzz_asdf_decode で発覚した panic の同 root cause)
    let asdf_bytes = bincode::serde::encode_to_vec(
        tree,
        bincode::config::legacy().with_limit::<ASP_BINCODE_LIMIT>(),
    )
    .map_err(|e| libasp::AspError::SerializationError(e.to_string()))?;

    // Build I-packet with ASDF payload embedded as DCT coefficients
    // (reusing the sparse coefficient field for binary data)
    let mut payload = IPacketPayload::new(0, 0, 0.0);
    payload.quality = QualityLevel::High;
    payload.timestamp_ms = 0;

    // Store ASDF as a region descriptor
    let region = RegionDescriptor {
        bounds: Rect::new(0, 0, 0, 0),
        pattern_type: libasp::PatternType::Complex,
        palette: ColorPalette {
            colors: vec![Color::new(
                (asdf_bytes.len() >> 16) as u8,
                (asdf_bytes.len() >> 8) as u8,
                asdf_bytes.len() as u8,
            )],
            weights: None,
        },
        dct_coefficients: Some(
            asdf_bytes
                .iter()
                .enumerate()
                .map(|(i, &b)| (i as u32, 0, b as f32))
                .collect(),
        ),
        texture_id: None,
        params: Some(vec![("asdf_len".to_string(), asdf_bytes.len() as f32)]),
    };
    payload.regions.push(region);

    AspPacket::create_i_packet(sequence, payload)
}

/// Create an ASP D-packet with SDF parameter deltas.
///
/// `delta_asdf` should be a serialized ASDF representing the updated tree.
pub fn create_sdf_d_packet(
    delta_asdf: &[u8],
    ref_sequence: u32,
    sequence: u32,
) -> AspResult<AspPacket> {
    let mut payload = DPacketPayload::new(ref_sequence);
    payload.timestamp_ms = 0;

    // Embed delta as a correction-style region delta
    let delta = libasp::RegionDelta {
        region_index: 0,
        palette_delta: None,
        dct_delta: Some(
            delta_asdf
                .iter()
                .enumerate()
                .map(|(i, &b)| (i as u32, 0, b as f32))
                .collect(),
        ),
        param_delta: Some(vec![("asdf_len".to_string(), delta_asdf.len() as f32)]),
    };
    payload.region_deltas.push(delta);

    AspPacket::create_d_packet(sequence, payload)
}

/// Decode an SDF tree from a received ASP I-packet.
///
/// Extracts the ASDF binary from the packet's region descriptor
/// and deserializes it back into an `SdfTree`.
pub fn decode_sdf_i_packet(packet: &AspPacket) -> Result<SdfTree, String> {
    let i_payload = packet
        .as_i_packet()
        .ok_or_else(|| "Not an I-packet".to_string())?;

    let region = i_payload
        .regions
        .first()
        .ok_or_else(|| "No regions in I-packet".to_string())?;

    // Extract ASDF length from params
    let asdf_len = region
        .params
        .as_ref()
        .and_then(|p| p.iter().find(|(k, _)| k == "asdf_len"))
        .map(|(_, v)| *v as usize)
        .ok_or_else(|| "Missing asdf_len param".to_string())?;

    // Reconstruct ASDF bytes from DCT coefficient field
    let coeffs = region
        .dct_coefficients
        .as_ref()
        .ok_or_else(|| "No DCT data in region".to_string())?;

    let mut asdf_bytes = vec![0u8; asdf_len];
    for &(idx, _, val) in coeffs {
        if (idx as usize) < asdf_len {
            asdf_bytes[idx as usize] = val as u8;
        }
    }

    bincode::serde::decode_from_slice(
        &asdf_bytes,
        bincode::config::legacy().with_limit::<ASP_BINCODE_LIMIT>(),
    )
    .map(|(tree, _)| tree)
    .map_err(|e| format!("ASDF decode failed: {}", e))
}

/// Compute the ASP packet overhead for an SDF scene.
///
/// Returns `(asdf_bytes, total_packet_bytes)`.
pub fn estimate_packet_size(tree: &SdfTree) -> (usize, usize) {
    let asdf_bytes = bincode::serde::encode_to_vec(
        tree,
        bincode::config::legacy().with_limit::<ASP_BINCODE_LIMIT>(),
    )
    .unwrap_or_default();
    let asdf_len = asdf_bytes.len();
    // ASP overhead: 16-byte header + FlatBuffers framing + CRC32
    // Each byte stored as (u32, u32, f32) = 12 bytes in DCT field
    // Total overhead is significant but acceptable for small SDF scenes
    let packet_overhead = 16 + 4 + 64; // header + CRC + FlatBuffers framing
    (asdf_len, asdf_len * 12 + packet_overhead)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::eval::eval;
    use crate::types::SdfNode;
    use glam::Vec3;

    /// I-packet round trip: the tree that comes back evaluates identically.
    #[test]
    fn i_packet_round_trip_preserves_the_tree() {
        let node = SdfNode::sphere(0.7)
            .smooth_union(SdfNode::box3d(0.8, 0.4, 0.6).translate(0.5, 0.2, 0.0), 0.15)
            .twist(0.3);
        let tree = SdfTree::new(node.clone());
        let packet = create_sdf_i_packet(&tree, 7).expect("packet");
        let back = decode_sdf_i_packet(&packet).expect("decode");
        for p in [
            Vec3::ZERO,
            Vec3::new(0.4, -0.3, 0.9),
            Vec3::new(-1.2, 0.8, 0.1),
        ] {
            assert_eq!(eval(&back.root, p).to_bits(), eval(&node, p).to_bits());
        }
        let (asdf, total) = estimate_packet_size(&tree);
        assert!(asdf > 0 && total >= asdf);
    }
}