Repository navigation
Move raster nodes from RustGPU to wgsl shaders and implement 'Rasterize' Node for native targets - #4832
Move raster nodes from RustGPU to wgsl shaders and implement 'Rasterize' Node for native targets#4832timon-schelling wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
11 issues found across 140 files
Confidence score: 3/5
median_filter.wgslperforms over 163,000 texture loads per output pixel at radius 50, which could make large-radius filtering impractically slow. Use a more efficient selection strategy or cap the radius.document_migration.rschangesUpload Texture's default toRaster<GPU>even though the node still requires CPU input. Restrict the rewrite to inputs that accept GPU rasters.context.rsnow makesread_rasterreturn an empty default for CPU raster rows thatmapstill accepts. Preserve CPU support or convert those rows before reading.graphic.rsnow skips CPU raster graphics inflatten_rasterand CPU-valued attributes inwrite_attribute. Preserve those CPU paths or convert the values before lookup.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="node-graph/libraries/wgpu-executor/src/shader_runtime/raster_kernel_runtime.rs">
<violation number="1" location="node-graph/libraries/wgpu-executor/src/shader_runtime/raster_kernel_runtime.rs:183">
P2: The pipeline cache key `(kernel, output_format)` does not include the params length, but the shader's `uniform_params` array size is fixed by the first call. If a later call passes a different number of params for the same kernel, the params buffer and the shader disagree. Either include `param_slots` in the cache key or document that params length must be constant per kernel.</violation>
</file>
<file name="node-graph/nodes/raster/src/color_lookup_table.rs">
<violation number="1" location="node-graph/nodes/raster/src/color_lookup_table.rs:350">
P2: `grid()` has no test. Its blue-fastest reorder (the `red_fastest == false` branch) and its sampled fallback feed the GPU Lut3d texture, and nothing checks that they match `apply`. Add a unit test that compares the grid's tetrahedral lookup with `apply` for a CUBE table (red-fastest) and a 3DL-style table (blue-fastest).</violation>
</file>
<file name="node-graph/libraries/wgpu-executor/src/lib.rs">
<violation number="1" location="node-graph/libraries/wgpu-executor/src/lib.rs:39">
P3: This doc says every raster texture uses `RASTER_FORMAT`, but `request_texture` and the document render path still return `Rgba8Unorm` sRGB textures. Reword it to describe only the linear-light textures this constant covers.</violation>
</file>
<file name="node-graph/nodes/gcore/src/context.rs">
<violation number="1" location="node-graph/nodes/gcore/src/context.rs:26">
P2: `read_raster` now downcasts only GPU raster rows, but `map` still accepts `List<Raster<CPU>>` and passes those rows unchanged; reads then silently return an empty default. Preserve CPU support or convert CPU rows before reading them.</violation>
</file>
<file name="node-graph/libraries/wgpu-executor/src/shader_runtime/raster_kernel_input_sample.wgsl">
<violation number="1" location="node-graph/libraries/wgpu-executor/src/shader_runtime/raster_kernel_input_sample.wgsl:2">
P2: `INPUT_sample` divides by `INPUT_size()` with no guard. An empty input has size zero, so the coordinate becomes inf or NaN, and the result is not guaranteed to be transparent as the runtime docs promise. Return `vec4<f32>(0.)` when `INPUT_size()` is zero, or when any component is zero.</violation>
</file>
<file name="node-graph/nodes/raster/src/adjustments.rs">
<violation number="1" location="node-graph/nodes/raster/src/adjustments.rs:761">
P3: This comment says `curves` stays off the shader build, but this PR moves `curves` to `Raster<GPU>` with a WGSL body. Update or remove the comment so it matches the implementation.</violation>
</file>
<file name="node-graph/libraries/wgpu-executor/src/vello_render.rs">
<violation number="1" location="node-graph/libraries/wgpu-executor/src/vello_render.rs:193">
P3: The flag passed to `premultiply` is named `loads_linear_light` here but lands in the `source_is_srgb` parameter, and it now also selects `srgb_source_parameters` for the linear `RASTER_FORMAT` source. The conversion is correct, but the names and the `srgb_source_parameters` doc now contradict it. Rename the parameter and field to describe the linear-light case, such as `source_is_linear`, and update the doc comments to match.</violation>
</file>
<file name="node-graph/nodes/raster/src/wgsl/median_filter.wgsl">
<violation number="1" location="node-graph/nodes/raster/src/wgsl/median_filter.wgsl:37">
P1: This performs 16 full neighborhood scans per pixel; at the advertised radius of 50, that is over 163,000 texture loads per output pixel. Use a more efficient median-selection strategy or impose a practical radius limit.</violation>
</file>
<file name="node-graph/nodes/graphic/src/graphic.rs">
<violation number="1" location="node-graph/nodes/graphic/src/graphic.rs:809">
P2: CPU raster attributes written by `write_attribute` are now silently skipped because this reader looks up only `Raster<GPU>`. Preserve a read path for CPU-valued attributes or convert them before lookup.</violation>
<violation number="2" location="node-graph/nodes/graphic/src/graphic.rs:999">
P2: `flatten_raster` now discards CPU raster graphics: its `Raster<GPU>` extractor matches only `Graphic::RasterGPU` variants, while CPU variants remain valid. Preserve CPU cases or upload them before flattening.</violation>
</file>
<file name="editor/src/messages/portfolio/document_migration.rs">
<violation number="1" location="editor/src/messages/portfolio/document_migration.rs:1831">
P2: This global rewrite also changes `Upload Texture`'s `Raster<CPU>` input default to `Raster<GPU>`, although that node still requires CPU input. Restrict the rewrite to inputs whose migrated definitions accept GPU rasters.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| return from_total_order_key(median); | ||
| } | ||
|
|
||
| for (var digit_index = 0u; digit_index < 16u; digit_index++) { |
There was a problem hiding this comment.
P1: This performs 16 full neighborhood scans per pixel; at the advertised radius of 50, that is over 163,000 texture loads per output pixel. Use a more efficient median-selection strategy or impose a practical radius limit.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/nodes/raster/src/wgsl/median_filter.wgsl, line 37:
<comment>This performs 16 full neighborhood scans per pixel; at the advertised radius of 50, that is over 163,000 texture loads per output pixel. Use a more efficient median-selection strategy or impose a practical radius limit.</comment>
<file context>
@@ -0,0 +1,54 @@
+ return from_total_order_key(median);
+ }
+
+ for (var digit_index = 0u; digit_index < 16u; digit_index++) {
+ let shift = 30u - 2u * digit_index;
+ var below_one = vec4<u32>(0u);
</file context>
| let param_slots = slots(args.params.len()); | ||
|
|
||
| let mut pipelines = self.pipelines.lock().unwrap(); | ||
| let kernel_pipeline = pipelines.entry((args.kernel, args.output_format)).or_insert_with(|| create_pipeline(device, args, param_slots)); |
There was a problem hiding this comment.
P2: The pipeline cache key (kernel, output_format) does not include the params length, but the shader's uniform_params array size is fixed by the first call. If a later call passes a different number of params for the same kernel, the params buffer and the shader disagree. Either include param_slots in the cache key or document that params length must be constant per kernel.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/libraries/wgpu-executor/src/shader_runtime/raster_kernel_runtime.rs, line 183:
<comment>The pipeline cache key `(kernel, output_format)` does not include the params length, but the shader's `uniform_params` array size is fixed by the first call. If a later call passes a different number of params for the same kernel, the params buffer and the shader disagree. Either include `param_slots` in the cache key or document that params length must be constant per kernel.</comment>
<file context>
@@ -0,0 +1,390 @@
+ let param_slots = slots(args.params.len());
+
+ let mut pipelines = self.pipelines.lock().unwrap();
+ let kernel_pipeline = pipelines.entry((args.kernel, args.output_format)).or_insert_with(|| create_pipeline(device, args, param_slots));
+
+ let mut params = args.params.clone();
</file context>
|
|
||
| /// The mapping as one grid, which [`Lut::apply`] matches by tetrahedral interpolation of the input normalized over the grid's domain: | ||
| /// the 3D table itself when the mapping is nothing more, otherwise the whole mapping sampled at `GRID_SIZE` points per axis across 0 to 1. | ||
| pub fn grid(&self) -> LutGrid { |
There was a problem hiding this comment.
P2: grid() has no test. Its blue-fastest reorder (the red_fastest == false branch) and its sampled fallback feed the GPU Lut3d texture, and nothing checks that they match apply. Add a unit test that compares the grid's tetrahedral lookup with apply for a CUBE table (red-fastest) and a 3DL-style table (blue-fastest).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/nodes/raster/src/color_lookup_table.rs, line 350:
<comment>`grid()` has no test. Its blue-fastest reorder (the `red_fastest == false` branch) and its sampled fallback feed the GPU Lut3d texture, and nothing checks that they match `apply`. Add a unit test that compares the grid's tetrahedral lookup with `apply` for a CUBE table (red-fastest) and a 3DL-style table (blue-fastest).</comment>
<file context>
@@ -344,6 +344,55 @@ impl Lut {
+
+ /// The mapping as one grid, which [`Lut::apply`] matches by tetrahedral interpolation of the input normalized over the grid's domain:
+ /// the 3D table itself when the mapping is nothing more, otherwise the whole mapping sampled at `GRID_SIZE` points per axis across 0 to 1.
+ pub fn grid(&self) -> LutGrid {
+ if self.lab_encoding.is_none()
+ && self.input_curves.is_none()
</file context>
|
|
||
| #[node_macro::node(category("Data: Context"), icon("NodeReadContext"), path(graphene_core::vector))] | ||
| fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> Item<Raster<CPU>> { | ||
| fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> Item<Raster<GPU>> { |
There was a problem hiding this comment.
P2: read_raster now downcasts only GPU raster rows, but map still accepts List<Raster<CPU>> and passes those rows unchanged; reads then silently return an empty default. Preserve CPU support or convert CPU rows before reading them.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/nodes/gcore/src/context.rs, line 26:
<comment>`read_raster` now downcasts only GPU raster rows, but `map` still accepts `List<Raster<CPU>>` and passes those rows unchanged; reads then silently return an empty default. Preserve CPU support or convert CPU rows before reading them.</comment>
<file context>
@@ -23,7 +23,7 @@ fn read_vector(ctx: impl Ctx + ExtractVarArgs) -> Item<Vector> {
#[node_macro::node(category("Data: Context"), icon("NodeReadContext"), path(graphene_core::vector))]
-fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> Item<Raster<CPU>> {
+fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> Item<Raster<GPU>> {
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
let var_arg = var_arg as &dyn std::any::Any;
</file context>
| .filter_map(|(index, input)| { | ||
| let NodeInput::Value { tagged_value, exposed } = input else { return None }; | ||
| let TaggedValue::TypeDefault(stored_type) = &**tagged_value else { return None }; | ||
| let gpu_type = if *stored_type == item!(Raster<CPU>) { |
There was a problem hiding this comment.
P2: This global rewrite also changes Upload Texture's Raster<CPU> input default to Raster<GPU>, although that node still requires CPU input. Restrict the rewrite to inputs whose migrated definitions accept GPU rasters.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At editor/src/messages/portfolio/document_migration.rs, line 1831:
<comment>This global rewrite also changes `Upload Texture`'s `Raster<CPU>` input default to `Raster<GPU>`, although that node still requires CPU input. Restrict the rewrite to inputs whose migrated definitions accept GPU rasters.</comment>
<file context>
@@ -1787,6 +1817,35 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
+ .filter_map(|(index, input)| {
+ let NodeInput::Value { tagged_value, exposed } = input else { return None };
+ let TaggedValue::TypeDefault(stored_type) = &**tagged_value else { return None };
+ let gpu_type = if *stored_type == item!(Raster<CPU>) {
+ item!(Raster<GPU>)
+ } else if *stored_type == list!(Raster<CPU>) {
</file context>
| #[cfg(target_family = "wasm")] | ||
| const TEXTURE_CACHE_SIZE: u64 = 512 * 1024 * 1024; // 512MB | ||
|
|
||
| /// The format of every raster texture, holding linear-light straight-alpha values. |
There was a problem hiding this comment.
P3: This doc says every raster texture uses RASTER_FORMAT, but request_texture and the document render path still return Rgba8Unorm sRGB textures. Reword it to describe only the linear-light textures this constant covers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/libraries/wgpu-executor/src/lib.rs, line 39:
<comment>This doc says every raster texture uses `RASTER_FORMAT`, but `request_texture` and the document render path still return `Rgba8Unorm` sRGB textures. Reword it to describe only the linear-light textures this constant covers.</comment>
<file context>
@@ -36,6 +36,9 @@ const TEXTURE_CACHE_SIZE: u64 = 1024 * 1024 * 1024; // 1GB
#[cfg(target_family = "wasm")]
const TEXTURE_CACHE_SIZE: u64 = 512 * 1024 * 1024; // 512MB
+/// The format of every raster texture, holding linear-light straight-alpha values.
+pub const RASTER_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
+
</file context>
| /// The format of every raster texture, holding linear-light straight-alpha values. | |
| /// The format of linear-light straight-alpha raster textures, such as those from [`Self::render_vello_raster_on_canvas`]. |
| #[cfg(feature = "std")] | ||
| #[node_macro::node(category("Raster: Adjustment"), icon("NodeCurves"), inspector("transfer_curves_inspector"))] | ||
| fn curves<T: Adjust<Color> + Send>( | ||
| fn curves<T: Adjust>( |
There was a problem hiding this comment.
P3: This comment says curves stays off the shader build, but this PR moves curves to Raster<GPU> with a WGSL body. Update or remove the comment so it matches the implementation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/nodes/raster/src/adjustments.rs, line 761:
<comment>This comment says `curves` stays off the shader build, but this PR moves `curves` to `Raster<GPU>` with a WGSL body. Update or remove the comment so it matches the implementation.</comment>
<file context>
@@ -677,41 +757,85 @@ fn levels<T: Adjust<Color>>(
-#[cfg(feature = "std")]
#[node_macro::node(category("Raster: Adjustment"), icon("NodeCurves"), inspector("transfer_curves_inspector"))]
-fn curves<T: Adjust<Color> + Send>(
+fn curves<T: Adjust>(
_: impl Ctx,
- #[implementations(Raster<CPU>, Color, Gradient)] image: Item<T>,
</file context>
| if let SceneImage::Texture(source) = image { | ||
| let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); | ||
| alpha_conversion.premultiply(device, encoder, &source_view, source.format().is_srgb(), &view); | ||
| let loads_linear_light = source.format().is_srgb() || source.format() == crate::RASTER_FORMAT; |
There was a problem hiding this comment.
P3: The flag passed to premultiply is named loads_linear_light here but lands in the source_is_srgb parameter, and it now also selects srgb_source_parameters for the linear RASTER_FORMAT source. The conversion is correct, but the names and the srgb_source_parameters doc now contradict it. Rename the parameter and field to describe the linear-light case, such as source_is_linear, and update the doc comments to match.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/libraries/wgpu-executor/src/vello_render.rs, line 193:
<comment>The flag passed to `premultiply` is named `loads_linear_light` here but lands in the `source_is_srgb` parameter, and it now also selects `srgb_source_parameters` for the linear `RASTER_FORMAT` source. The conversion is correct, but the names and the `srgb_source_parameters` doc now contradict it. Rename the parameter and field to describe the linear-light case, such as `source_is_linear`, and update the doc comments to match.</comment>
<file context>
@@ -180,7 +190,8 @@ impl ImageTextures {
if let SceneImage::Texture(source) = image {
let source_view = source.create_view(&wgpu::TextureViewDescriptor::default());
- alpha_conversion.premultiply(device, encoder, &source_view, source.format().is_srgb(), &view);
+ let loads_linear_light = source.format().is_srgb() || source.format() == crate::RASTER_FORMAT;
+ alpha_conversion.premultiply(device, encoder, &source_view, loads_linear_light, &view);
}
</file context>
b3418f0 to
cb01314
Compare
Performance Benchmark Results
|
There was a problem hiding this comment.
3 issues found across 10 files (changes from recent commits).
Confidence score: 3/5
raster_kernel.rscan route dehaze guided-filter statistics throughRgba16Floaton fallback GPUs, where variance precision is lost. Preserve higher precision for these statistics.wgpu-executor/src/lib.rsdoes not check whether the fallback format supportsRENDER_ATTACHMENT, so pipeline or texture creation can still fail on downlevel adapters. Check the fallback format’s allowed usages before selecting it.texture_conversion.rsduplicates the existingFrom<Color> for RGBA16Fconversion. CollectRGBA16Fvalues and cast them instead so the f16 packing stays in one place.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="node-graph/nodes/raster/src/raster_kernel.rs">
<violation number="1" location="node-graph/nodes/raster/src/raster_kernel.rs:413">
P2: The executor can select `Rgba16Float`, so this routes dehaze’s guided-filter statistics through half-float outputs on fallback GPUs even though their variances lose precision at that format. Preserve higher precision for these intermediates or otherwise adapt the calculation to avoid the loss.</violation>
</file>
<file name="node-graph/libraries/wgpu-executor/src/texture_conversion.rs">
<violation number="1" location="node-graph/libraries/wgpu-executor/src/texture_conversion.rs:20">
P3: This per-channel `f16::from_f32` conversion duplicates the existing `From<Color> for RGBA16F`. Collect `RGBA16F` values instead and cast them, so the f16 packing lives in one place.</violation>
</file>
<file name="node-graph/libraries/wgpu-executor/src/lib.rs">
<violation number="1" location="node-graph/libraries/wgpu-executor/src/lib.rs:174">
P2: The fallback format is not checked for `RENDER_ATTACHMENT` support, so downlevel adapters that also lack renderable `Rgba16Float` still fail during pipeline or texture creation. Check the fallback's allowed usages and choose another supported format or fail initialization cleanly.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| } | ||
|
|
||
| fn raster_format(pipeline: &WgpuPipelineCache) -> WgpuTextureFormat { | ||
| pipeline.executor().map_or(RASTER_FORMAT, WgpuExecutor::raster_format) |
There was a problem hiding this comment.
P2: The executor can select Rgba16Float, so this routes dehaze’s guided-filter statistics through half-float outputs on fallback GPUs even though their variances lose precision at that format. Preserve higher precision for these intermediates or otherwise adapt the calculation to avoid the loss.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/nodes/raster/src/raster_kernel.rs, line 413:
<comment>The executor can select `Rgba16Float`, so this routes dehaze’s guided-filter statistics through half-float outputs on fallback GPUs even though their variances lose precision at that format. Preserve higher precision for these intermediates or otherwise adapt the calculation to avoid the loss.</comment>
<file context>
@@ -409,12 +409,16 @@ fn run<K: ToGpuParams + 'static>(
}
+fn raster_format(pipeline: &WgpuPipelineCache) -> WgpuTextureFormat {
+ pipeline.executor().map_or(RASTER_FORMAT, WgpuExecutor::raster_format)
+}
+
</file context>
| RASTER_FORMAT | ||
| } else { | ||
| log::warn!("The GPU can't render to {RASTER_FORMAT:?}, so rasters fall back to {FALLBACK_RASTER_FORMAT:?}"); | ||
| FALLBACK_RASTER_FORMAT |
There was a problem hiding this comment.
P2: The fallback format is not checked for RENDER_ATTACHMENT support, so downlevel adapters that also lack renderable Rgba16Float still fail during pipeline or texture creation. Check the fallback's allowed usages and choose another supported format or fail initialization cleanly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/libraries/wgpu-executor/src/lib.rs, line 174:
<comment>The fallback format is not checked for `RENDER_ATTACHMENT` support, so downlevel adapters that also lack renderable `Rgba16Float` still fail during pipeline or texture creation. Check the fallback's allowed usages and choose another supported format or fail initialization cleanly.</comment>
<file context>
@@ -137,17 +145,32 @@ impl WgpuExecutor {
+ RASTER_FORMAT
+ } else {
+ log::warn!("The GPU can't render to {RASTER_FORMAT:?}, so rasters fall back to {FALLBACK_RASTER_FORMAT:?}");
+ FALLBACK_RASTER_FORMAT
+ }
+}
</file context>
| let texture = executor.request_texture_with_format(glam::UVec2::new(image.width, image.height), TextureFormat::Rgba8UnormSrgb); | ||
| let format = executor.raster_format(); | ||
| let texture = executor.request_texture_with_format(glam::UVec2::new(image.width, image.height), format); | ||
| let half_floats: Vec<[f16; 4]>; |
There was a problem hiding this comment.
P3: This per-channel f16::from_f32 conversion duplicates the existing From<Color> for RGBA16F. Collect RGBA16F values instead and cast them, so the f16 packing lives in one place.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At node-graph/libraries/wgpu-executor/src/texture_conversion.rs, line 20:
<comment>This per-channel `f16::from_f32` conversion duplicates the existing `From<Color> for RGBA16F`. Collect `RGBA16F` values instead and cast them, so the f16 packing lives in one place.</comment>
<file context>
@@ -14,13 +15,21 @@ fn upload_to_texture(executor: &WgpuExecutor, queue: &wgpu::Queue, image: &Raste
- let texture = executor.request_texture_with_format(glam::UVec2::new(image.width, image.height), crate::RASTER_FORMAT);
+ let format = executor.raster_format();
+ let texture = executor.request_texture_with_format(glam::UVec2::new(image.width, image.height), format);
+ let half_floats: Vec<[f16; 4]>;
+ let (bytes, bytes_per_pixel) = if format == crate::FALLBACK_RASTER_FORMAT {
+ half_floats = image.data.iter().map(|color| color.to_vec4().to_array().map(f16::from_f32)).collect();
</file context>
Performance Benchmark Results
|
No description provided.