Define data types for ID-based resource handling (#4168)

* Add resource lib

* rename ResourceSource to ResourceInput

* Rename ResourceInput -> DataSource and improve API

* Review

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Timon
2026-05-25 10:50:57 +00:00
committed by GitHub
parent d0d9a350c7
commit bbbe04903f
28 changed files with 187 additions and 46 deletions

View File

@@ -4,18 +4,13 @@ use wgpu_executor::WgpuExecutor;
pub mod resource;
pub use graphene_application_io::{ApplicationIo, LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
pub use resource::HashMapResourceStorage;
#[cfg(not(target_family = "wasm"))]
pub use resource::mmap::MmapResourceStorage;
#[cfg(target_family = "wasm")]
pub use resource::opfs::OpfsResourceStorage;
pub use graphene_application_io::ApplicationIo;
#[derive(Default)]
pub struct PlatformApplicationIo {
#[cfg(feature = "wgpu")]
pub(crate) gpu_executor: Option<WgpuExecutor>,
resources: Option<Box<dyn LoadResource>>,
resources: Option<Box<dyn resource::LoadResource>>,
}
impl PlatformApplicationIo {
@@ -49,7 +44,7 @@ impl PlatformApplicationIo {
}
}
pub fn inject_resource_proxy(&mut self, resources: Box<dyn LoadResource>) {
pub fn inject_resource_proxy(&mut self, resources: Box<dyn resource::LoadResource>) {
self.resources = Some(resources);
}
}
@@ -65,7 +60,7 @@ impl ApplicationIo for PlatformApplicationIo {
self.gpu_executor.as_ref()
}
fn load_resource(&self, hash: ResourceHash) -> graphene_application_io::ResourceFuture {
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture {
self.resources.as_ref().expect("Resource storage not initialized").load(hash)
}
}

View File

@@ -1,12 +1,16 @@
#[cfg(not(target_family = "wasm"))]
pub mod mmap;
#[cfg(target_family = "wasm")]
pub mod opfs;
use graphene_application_io::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub use graphene_application_io::resource::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
#[cfg(not(target_family = "wasm"))]
pub mod mmap;
#[cfg(not(target_family = "wasm"))]
pub use mmap::MmapResourceStorage;
#[cfg(target_family = "wasm")]
pub mod opfs;
#[cfg(target_family = "wasm")]
pub use opfs::OpfsResourceStorage;
#[derive(Debug, Default)]
pub struct HashMapResourceStorage {
resources: Mutex<HashMap<ResourceHash, Resource>>,

View File

@@ -1,4 +1,4 @@
use graphene_application_io::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use graphene_application_io::resource::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use mmap_io::mmap::{MemoryMappedFile, MmapMode};
use std::collections::HashMap;
use std::fs;

View File

@@ -1,4 +1,4 @@
use graphene_application_io::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use graphene_application_io::resource::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use js_sys::Uint8Array;
use std::collections::{HashMap, HashSet, VecDeque};
use std::pin::Pin;

View File

@@ -396,7 +396,7 @@ tagged_value! {
Footprint(Footprint),
VectorModification(Box<VectorModification>),
ImageData(Image<Color>),
Resource(graphene_application_io::ResourceHash),
Resource(graphene_application_io::resource::ResourceHash),
// ==========
// ENUM TYPES
// ==========

View File

@@ -129,7 +129,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::Resource]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),

View File

@@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "text-nodes/serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "text-nodes/serde", "graphene-resource/serde"]
wasm = ["dep:web-sys"]
wgpu = ["dep:wgpu"]
@@ -18,6 +18,7 @@ dyn-any = { workspace = true }
core-types = { workspace = true }
vector-types = { workspace = true }
text-nodes = { workspace = true }
graphene-resource = { workspace = true }
# Workspace dependencies
blake3 = { workspace = true }

View File

@@ -9,9 +9,10 @@ use std::time::Duration;
use text_nodes::FontCache;
use vector_types::vector::style::RenderMode;
pub mod resource;
pub use core_types::resource::Resource;
pub use resource::{LoadResource, ResourceFuture, ResourceHash, ResourceStorage};
pub mod resource {
pub use core_types::resource::*;
pub use graphene_resource::*;
}
#[cfg(feature = "wgpu")]
#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
@@ -49,7 +50,7 @@ pub trait ApplicationIo {
fn gpu_executor(&self) -> Option<&Self::Executor> {
None
}
fn load_resource(&self, hash: ResourceHash) -> resource::ResourceFuture;
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture;
}
impl<T: ApplicationIo> ApplicationIo for &T {
@@ -59,7 +60,7 @@ impl<T: ApplicationIo> ApplicationIo for &T {
(**self).gpu_executor()
}
fn load_resource(&self, hash: ResourceHash) -> resource::ResourceFuture {
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture {
(**self).load_resource(hash)
}
}

View File

@@ -68,8 +68,8 @@ mod uuid_generation {
}
#[repr(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, DynAny)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeId(pub u64);

View File

@@ -0,0 +1,23 @@
[package]
name = "graphene-resource"
version = "0.1.0"
edition = "2024"
description = "graphene resource interface"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "url/serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
# Workspace dependencies
blake3 = { workspace = true }
url = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }

View File

@@ -1,6 +1,7 @@
use core_types::CacheHash;
use core_types::resource::Resource;
use core_types::{CacheHash, graphene_hash};
use dyn_any::DynAny;
use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::hash::Hash;
@@ -18,6 +19,23 @@ pub trait ResourceStorage: LoadResource {
fn garbage_collect(&mut self, used: &[ResourceHash]);
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResourceId(pub u64);
impl ResourceId {
pub fn new() -> Self {
Self(core_types::uuid::generate_uuid())
}
}
impl std::fmt::Display for ResourceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Blake3 content hash of a resource, represented as 32 bytes
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, DynAny)]
pub struct ResourceHash([u8; 32]);
@@ -166,3 +184,86 @@ impl CacheHash for ResourceHash {
core::hash::Hash::hash(self, state);
}
}
#[derive(Clone, Debug)]
pub struct ResourceInfo {
pub id: ResourceId,
pub hash: Option<ResourceHash>,
pub inputs: DataSources,
}
pub type DataSources = Box<[DataSource]>;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DataSource {
Embedded,
Url(url::Url),
Font { family: String, style: Option<String> },
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResourceRegistry {
hashes: HashMap<ResourceId, ResourceHash>,
sources: HashMap<ResourceId, Vec<DataSource>>,
}
impl ResourceRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.hashes.is_empty() && self.sources.is_empty()
}
pub fn contains(&self, id: &ResourceId) -> bool {
self.hashes.contains_key(id) || self.sources.contains_key(id)
}
pub fn ids(&self) -> impl Iterator<Item = ResourceId> + '_ {
self.hashes.keys().chain(self.sources.keys().filter(|id| !self.hashes.contains_key(id))).copied()
}
pub fn info(&self, id: &ResourceId) -> Option<ResourceInfo> {
self.contains(id).then(|| ResourceInfo {
id: *id,
hash: self.hashes.get(id).copied(),
inputs: self.sources.get(id).cloned().unwrap_or_default().into_boxed_slice(),
})
}
pub fn push_source_back(&mut self, id: &ResourceId, source: DataSource) {
self.sources.entry(*id).or_default().push(source);
}
pub fn push_source_front(&mut self, id: &ResourceId, source: DataSource) {
self.sources.entry(*id).or_default().insert(0, source);
}
pub fn delete(&mut self, id: &ResourceId) -> Option<ResourceInfo> {
let hash = self.hashes.remove(id);
let sources = self.sources.remove(id);
if hash.is_none() && sources.is_none() {
return None;
}
Some(ResourceInfo {
id: *id,
hash,
inputs: sources.unwrap_or_default().into_boxed_slice(),
})
}
pub fn resolve(&mut self, id: &ResourceId, hash: ResourceHash) -> Option<ResourceHash> {
self.hashes.insert(*id, hash)
}
pub fn hash(&self, id: &ResourceId) -> Option<ResourceHash> {
self.hashes.get(id).copied()
}
pub fn unresolved(&self) -> impl Iterator<Item = (ResourceId, &[DataSource])> + '_ {
self.sources.iter().filter(|(id, _)| !self.hashes.contains_key(id)).map(|(id, sources)| (*id, sources.as_slice()))
}
}

View File

@@ -11,6 +11,7 @@ use core_types::transform::Footprint;
#[cfg(target_family = "wasm")]
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
use core_types::{Color, Ctx};
pub use graph_craft::application_io::resource::{Resource, ResourceHash};
pub use graph_craft::application_io::*;
pub use graph_craft::document::value::RenderOutputType;
#[cfg(target_family = "wasm")]

View File

@@ -16,7 +16,7 @@ pub fn expand_network(network: &mut NodeNetwork, substitutions: &HashMap<ProtoNo
/// Replace every `TaggedValue::Resource(hash)` input with a reference to a freshly inserted `resource` proto node.
fn replace_resource_inputs(network: &mut NodeNetwork) {
let mut hash_to_node_id: HashMap<application_io::ResourceHash, NodeId> = HashMap::new();
let mut hash_to_node_id: HashMap<graph_craft::application_io::resource::ResourceHash, NodeId> = HashMap::new();
let mut new_resource_nodes: Vec<(NodeId, DocumentNode)> = Vec::new();
for node in network.nodes.values_mut() {