#import nightshade_renderer::meshlet_data::{Meshlet, meshlet_payload, get_meshlet_triangle_count}
#import nightshade_renderer::meshlet_streams::{raster_clusters, meshlets, meshlet_instances, get_meshlet_vertex_id, get_meshlet_vertex_position}
/// Rasterizes the clusters too large for compute, or crossing the near plane
/// where clipping is needed, on the fixed function rasterizer.
/// `counts.x` is the capacity of the cluster list, which the hardware list is
/// indexed back from.
struct MeshletView {
clip_from_world: mat4x4<f32>,
camera_position: vec4<f32>,
screen_size: vec4<f32>,
counts: vec4<u32>,
}
@group(0) @binding(0) var<uniform> view: MeshletView;
#ifdef MESHLET_ATOMIC_VISIBILITY
@group(0) @binding(1) var visibility_buffer: texture_storage_2d<r64uint, atomic>;
#endif
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) @interpolate(flat) packed_ids: u32,
}
/// One draw covers every cluster this path owns. `vertex_index` walks the
/// cluster's index stream three at a time. A cluster shorter than the maximum
/// emits triangles wholly outside clip space, which the rasterizer discards.
///
/// The two rasters share one cluster list and grow from opposite ends, so this
/// path's clusters are counted from the back. The payload carries where the
/// cluster sits in that list rather than this draw's own instance index, because
/// the resolve knows only the list.
@vertex
fn vertex_main(
@builtin(instance_index) instance_index: u32,
@builtin(vertex_index) vertex_index: u32,
) -> VertexOutput {
let cluster_id = view.counts.x - 1u - instance_index;
let cluster = raster_clusters[cluster_id];
var meshlet = meshlets[cluster.offset];
let triangle_id = vertex_index / 3u;
if triangle_id >= get_meshlet_triangle_count(&meshlet) {
return VertexOutput(vec4<f32>(2.0, 2.0, 2.0, 1.0), 0u);
}
let vertex_id = get_meshlet_vertex_id(meshlet.start_index_id + vertex_index);
let local_position = get_meshlet_vertex_position(&meshlet, vertex_id);
let instance = meshlet_instances[cluster.instance_id];
let world_position = instance.world_from_local * vec4<f32>(local_position, 1.0);
let clip_position = view.clip_from_world * world_position;
return VertexOutput(clip_position, meshlet_payload(cluster_id, triangle_id));
}
#ifdef MESHLET_ATOMIC_VISIBILITY
/// Writes through the same atomic the compute rasterizer uses, so the two paths
/// settle against each other with no depth attachment between them: depth in the
/// high half means the max is the depth test.
///
/// An occluded fragment still writes, because a shader with side effects cannot
/// be depth tested before it runs. The resolve tests depth against the scene
/// again before shading, which is what makes that harmless.
@fragment
fn fragment_main(vertex_output: VertexOutput) {
let depth_bits = bitcast<u32>(vertex_output.clip_position.z);
if depth_bits == 0u {
return;
}
let packed = (u64(depth_bits) << 32u) | u64(vertex_output.packed_ids);
textureAtomicMax(
visibility_buffer,
vec2<i32>(vertex_output.clip_position.xy),
packed,
);
}
#else
@fragment
fn fragment_main(vertex_output: VertexOutput) -> @location(0) u32 {
return vertex_output.packed_ids;
}
#endif