Skip to main content

khora_io/asset/
registry.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Decoder registry — type-erased dispatch for asset decoding.
16
17use super::{AssetDecoder, AssetService};
18use anyhow::{anyhow, Result};
19use khora_core::asset::Asset;
20use khora_telemetry::{
21    metrics::registry::{CounterHandle, HistogramHandle},
22    MetricsRegistry, ScopedMetricTimer,
23};
24use std::{any::Any, collections::HashMap, sync::Arc};
25
26/// Inventory entry for an `AssetDecoder` that wants to be auto-registered
27/// against an [`AssetService`] at startup.
28///
29/// Mirrors the existing pattern used by `MaterialRegistration`
30/// (`crates/khora-data/src/ecs/components/material_registry.rs`),
31/// `DataSystemRegistration`, and `FlowRegistration`.
32///
33/// Use this for decoder slots with **a single canonical implementation**
34/// (e.g. `texture`, `font`). Slots where multiple backends compete (`audio`,
35/// `mesh`) deliberately stay explicit at the call site — see the rationale
36/// in `decoders/audio/mod.rs` and `decoders/mesh/mod.rs`.
37///
38/// ```ignore
39/// inventory::submit! {
40///     DecoderRegistration {
41///         type_name: "texture",
42///         register: |svc| {
43///             svc.register_decoder::<CpuTexture>("texture", TextureDecoder);
44///         },
45///     }
46/// }
47/// ```
48pub struct DecoderRegistration {
49    /// Asset type name the decoder handles. Must match what
50    /// `IndexBuilder::asset_type_for_extension` returns for the relevant
51    /// file extensions (e.g. `"texture"`).
52    pub type_name: &'static str,
53    /// Function that performs the registration on an [`AssetService`].
54    /// Plain function pointer — no captures — so the entry can be embedded
55    /// in `inventory::submit!` (which needs `'static`).
56    pub register: fn(&mut AssetService),
57}
58
59inventory::collect!(DecoderRegistration);
60
61trait AnyDecoder: Send + Sync {
62    fn decode_any(&self, bytes: &[u8], metrics: &DecoderMetrics) -> Result<Box<dyn Any + Send>>;
63}
64
65struct DecoderWrapper<A: Asset, L: AssetDecoder<A>>(L, std::marker::PhantomData<A>);
66
67impl<A: Asset, L: AssetDecoder<A> + Send + Sync> AnyDecoder for DecoderWrapper<A, L> {
68    fn decode_any(&self, bytes: &[u8], metrics: &DecoderMetrics) -> Result<Box<dyn Any + Send>> {
69        let _timer = ScopedMetricTimer::new(&metrics.decode_time_ms);
70        let asset: A = self.0.load(bytes).map_err(|e| anyhow!(e.to_string()))?;
71        metrics.assets_decoded_total.increment()?;
72        Ok(Box::new(asset))
73    }
74}
75
76struct DecoderMetrics {
77    decode_time_ms: HistogramHandle,
78    assets_decoded_total: CounterHandle,
79}
80
81impl DecoderMetrics {
82    fn new(registry: &MetricsRegistry) -> Self {
83        Self {
84            decode_time_ms: registry
85                .register_histogram(
86                    "assets",
87                    "decode_time",
88                    "Asset decoding time",
89                    "ms",
90                    vec![1.0, 5.0, 16.0, 33.0, 100.0, 500.0],
91                )
92                .expect("Failed to register asset decode time metric"),
93            assets_decoded_total: registry
94                .register_counter("assets", "decoded_total", "Total number of assets decoded")
95                .expect("Failed to register asset count metric"),
96        }
97    }
98}
99
100/// Registry of asset decoders, keyed by type name.
101pub struct DecoderRegistry {
102    metrics: DecoderMetrics,
103    decoders: HashMap<String, Box<dyn AnyDecoder>>,
104}
105
106impl DecoderRegistry {
107    /// Creates a new decoder registry.
108    pub fn new(metrics_registry: Arc<MetricsRegistry>) -> Self {
109        Self {
110            decoders: HashMap::new(),
111            metrics: DecoderMetrics::new(&metrics_registry),
112        }
113    }
114
115    /// Registers a decoder for a specific asset type name.
116    pub fn register<A: Asset>(
117        &mut self,
118        type_name: &str,
119        decoder: impl AssetDecoder<A> + Send + Sync + 'static,
120    ) {
121        let wrapped = DecoderWrapper(decoder, std::marker::PhantomData);
122        self.decoders
123            .insert(type_name.to_string(), Box::new(wrapped));
124    }
125
126    /// Decodes an asset of the specified type from raw bytes.
127    pub fn decode<A: Asset>(&self, type_name: &str, bytes: &[u8]) -> Result<A> {
128        let decoder = self
129            .decoders
130            .get(type_name)
131            .ok_or_else(|| anyhow!("No decoder registered for asset type '{}'", type_name))?;
132
133        let asset_any = decoder.decode_any(bytes, &self.metrics)?;
134        let asset_boxed = asset_any.downcast::<A>().map_err(|_| {
135            anyhow!(
136                "Decoder for type '{}' returned a different asset type than requested.",
137                type_name
138            )
139        })?;
140        Ok(*asset_boxed)
141    }
142}