use ash::vk;
use concinnity_core::gfx::render_types::RtParams;
use concinnity_core::render::error::RenderResult;
use concinnity_core::render::planar_reflection;
use concinnity_core::render::post::rt_reflections::{RtParamsInputs, RtReflectionSettings};
use concinnity_core::render::rt_accel::seed_wanted;
use super::super::allocator::{DeviceAllocator, PooledBuffer};
use super::super::context::{HDR_FORMAT, VkContext};
use super::super::descriptor_layout::{Binding, PoolSizes};
use super::super::pipeline_desc::{Blend, GraphicsPipelineDesc};
use super::super::resources::{alloc_descriptor_sets, create_descriptor_set_layout};
use super::super::set_writes::SetWrites;
use super::super::texture::*;
use crate::vulkan::builtin_shaders::CompileProgram;
use crate::vulkan::owned::{
OwnedDescriptorPool, OwnedFramebuffer, OwnedPipeline, OwnedPipelineLayout, OwnedRenderPass,
OwnedSampler, OwnedSetLayout, VkDevice,
};
use crate::vulkan::wire_cache::WireCache;
pub(in crate::vulkan) struct RtShaders {
pub vs: Vec<u8>,
pub flat_fs: Vec<u8>,
pub textured_fs: Option<Vec<u8>>,
}
pub(in crate::vulkan) fn compile_rt_shaders(
hot_reload: bool,
pool_size: usize,
) -> RenderResult<RtShaders> {
use super::super::builtin_shaders;
let vs = builtin_shaders::FULLSCREEN_VERT.compile(hot_reload)?;
let flat_fs = builtin_shaders::RT_REFLECTIONS_FRAG.compile(hot_reload)?;
let textured_fs = if pool_size > 0 {
Some(builtin_shaders::RT_REFLECTIONS_FRAG_TEXTURED.compile(hot_reload)?)
} else {
None
};
Ok(RtShaders {
vs,
flat_fs,
textured_fs,
})
}
pub(in crate::vulkan) struct RtReflectionsResources {
pub(in crate::vulkan) settings: RtReflectionSettings,
pub(in crate::vulkan) output: GpuImage,
extent: vk::Extent2D,
render_pass: OwnedRenderPass,
framebuffer: OwnedFramebuffer,
_set_layout: OwnedSetLayout,
layout_flat: OwnedPipelineLayout,
layout_textured: Option<OwnedPipelineLayout>,
flat_pso: OwnedPipeline,
textured_pso: Option<OwnedPipeline>,
params_buffers: Vec<PooledBuffer>,
_descriptor_pool: OwnedDescriptorPool,
resolve_sets: Vec<vk::DescriptorSet>,
_sampler: OwnedSampler,
dummy_ssbo: PooledBuffer,
pool_size: usize,
wired_accel: WireCache<RtAccelHandles>,
}
unsafe impl Send for RtReflectionsResources {}
fn resolve_set_bindings() -> [Binding; 13] {
use vk::DescriptorType as T;
let frag = vk::ShaderStageFlags::FRAGMENT;
[
(0, T::UNIFORM_BUFFER, frag),
(1, T::ACCELERATION_STRUCTURE_KHR, frag),
(2, T::STORAGE_BUFFER, frag),
(3, T::STORAGE_BUFFER, frag),
(4, T::STORAGE_BUFFER, frag),
(5, T::SAMPLED_IMAGE, frag),
(6, T::SAMPLED_IMAGE, frag),
(7, T::SAMPLED_IMAGE, frag),
(9, T::STORAGE_BUFFER, frag),
(10, T::STORAGE_BUFFER, frag),
(11, T::SAMPLER, frag),
(12, T::SAMPLER, frag),
(13, T::SAMPLER, frag),
]
}
fn create_rt_render_pass(device: &VkDevice) -> RenderResult<OwnedRenderPass> {
let attachment = vk::AttachmentDescription::default()
.format(HDR_FORMAT)
.samples(vk::SampleCountFlags::TYPE_1)
.load_op(vk::AttachmentLoadOp::DONT_CARE)
.store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.final_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
let color_ref = vk::AttachmentReference::default()
.attachment(0)
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
let subpass = vk::SubpassDescription::default()
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
.color_attachments(std::slice::from_ref(&color_ref));
let dep = vk::SubpassDependency::default()
.src_subpass(vk::SUBPASS_EXTERNAL)
.dst_subpass(0)
.src_stage_mask(
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::FRAGMENT_SHADER,
)
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::SHADER_READ)
.dst_stage_mask(
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::FRAGMENT_SHADER,
)
.dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::SHADER_READ);
let info = vk::RenderPassCreateInfo::default()
.attachments(std::slice::from_ref(&attachment))
.subpasses(std::slice::from_ref(&subpass))
.dependencies(std::slice::from_ref(&dep));
device
.create_render_pass(&info)
.map_err(|e| crate::vulkan::error::map_vk_result(e, "RT reflections render pass"))
}
fn create_rt_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
frag_spv: &[u8],
) -> RenderResult<OwnedPipeline> {
GraphicsPipelineDesc::fullscreen(vert_spv, frag_spv, layout, render_pass, &[Blend::Opaque])
.build(device, "rt reflections")
}
pub(in crate::vulkan) struct RebuiltRtPipelines {
flat: OwnedPipeline,
textured: Option<OwnedPipeline>,
}
pub(in crate::vulkan) fn rebuild_rt_pipelines(
device: &VkDevice,
rt: &RtReflectionsResources,
hot_reload: bool,
) -> RenderResult<RebuiltRtPipelines> {
let shaders = compile_rt_shaders(hot_reload, rt.pool_size)?;
let flat = create_rt_pipeline(
device,
rt.render_pass.handle(),
rt.layout_flat.handle(),
&shaders.vs,
&shaders.flat_fs,
)?;
let textured = match (rt.layout_textured.as_ref(), &shaders.textured_fs) {
(Some(layout), Some(fs)) => Some(create_rt_pipeline(
device,
rt.render_pass.handle(),
layout.handle(),
&shaders.vs,
fs,
)?),
_ => None,
};
Ok(RebuiltRtPipelines { flat, textured })
}
pub(in crate::vulkan) struct RtBuild<'a> {
pub alloc: &'a DeviceAllocator,
pub device: &'a VkDevice,
pub width: u32,
pub height: u32,
pub frames: usize,
}
pub(in crate::vulkan) struct RtStaticInputs<'a> {
pub vertex_buffer: vk::Buffer,
pub index_buffer: vk::Buffer,
pub hdr_resolve_views: &'a [vk::ImageView],
pub gbuffer_views: &'a [vk::ImageView],
pub roughness_views: &'a [vk::ImageView],
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(in crate::vulkan) struct RtAccelHandles {
pub tlas: vk::AccelerationStructureKHR,
pub geom_buffer: vk::Buffer,
pub geom_size: vk::DeviceSize,
pub deformed_verts: vk::Buffer,
pub skinned_indices: vk::Buffer,
}
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct RtLayoutConfig {
pub bindless_set_layout: Option<vk::DescriptorSetLayout>,
pub global_set_layout: vk::DescriptorSetLayout,
pub pool_size: usize,
pub hot_reload: bool,
}
impl RtReflectionsResources {
pub(in crate::vulkan) fn new(
build: RtBuild,
settings: RtReflectionSettings,
static_inputs: RtStaticInputs,
layout: RtLayoutConfig,
) -> RenderResult<Self> {
let RtBuild {
alloc,
device,
width,
height,
frames,
} = build;
let RtStaticInputs {
vertex_buffer,
index_buffer,
hdr_resolve_views,
gbuffer_views,
roughness_views,
} = static_inputs;
let RtLayoutConfig {
bindless_set_layout,
global_set_layout,
pool_size,
hot_reload,
} = layout;
let render_pass = create_rt_render_pass(device)?;
let set_layout = create_descriptor_set_layout(device, &resolve_set_bindings())?;
let flat_layouts = [set_layout.handle(), global_set_layout];
let layout_flat = device
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default().set_layouts(&flat_layouts),
)
.map_err(|e| crate::vulkan::error::map_vk_result(e, "rt flat pipeline layout"))?;
let layout_textured = if let Some(bsl) = bindless_set_layout {
let layouts = [set_layout.handle(), global_set_layout, bsl];
Some(
device
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default().set_layouts(&layouts),
)
.map_err(|e| {
crate::vulkan::error::map_vk_result(e, "rt textured pipeline layout")
})?,
)
} else {
None
};
let shaders = compile_rt_shaders(hot_reload, pool_size)?;
let flat_pso = create_rt_pipeline(
device,
render_pass.handle(),
layout_flat.handle(),
&shaders.vs,
&shaders.flat_fs,
)?;
let textured_pso = match (layout_textured.as_ref(), &shaders.textured_fs) {
(Some(layout), Some(fs)) => Some(create_rt_pipeline(
device,
render_pass.handle(),
layout.handle(),
&shaders.vs,
fs,
)?),
_ => None,
};
let params_size = std::mem::size_of::<RtParams>() as vk::DeviceSize;
let mut params_buffers = Vec::with_capacity(frames);
for _ in 0..frames {
let buf = alloc.create_buffer(
params_size,
vk::BufferUsageFlags::UNIFORM_BUFFER,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
)?;
params_buffers.push(buf);
}
let f = frames as u32;
let pool_sizes = PoolSizes::default()
.sets(&resolve_set_bindings(), f)
.build();
let descriptor_pool = device
.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
.pool_sizes(&pool_sizes)
.max_sets(f),
)
.map_err(|e| crate::vulkan::error::map_vk_result(e, "rt descriptor pool"))?;
let layouts: Vec<_> = (0..frames).map(|_| set_layout.handle()).collect();
let resolve_sets = alloc_descriptor_sets(device, descriptor_pool.handle(), &layouts)?;
let sampler = create_sampler_linear_clamp(device)?;
let screen = sampler.handle();
for &set in &resolve_sets {
SetWrites::new(set)
.sampler(11, screen)
.sampler(12, screen)
.sampler(13, screen)
.apply(device);
}
let dummy_ssbo = alloc.create_buffer(
16,
vk::BufferUsageFlags::STORAGE_BUFFER,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
)?;
let mut me = Self {
settings,
output: GpuImage::null(),
extent: vk::Extent2D::default(),
render_pass,
framebuffer: OwnedFramebuffer::null(),
_set_layout: set_layout,
layout_flat,
layout_textured,
flat_pso,
textured_pso,
params_buffers,
_descriptor_pool: descriptor_pool,
resolve_sets,
_sampler: sampler,
dummy_ssbo,
pool_size,
wired_accel: WireCache::new(frames),
};
me.build_targets(alloc, device, width, height)?;
me.wire_static(
device,
RtStaticInputs {
vertex_buffer,
index_buffer,
hdr_resolve_views,
gbuffer_views,
roughness_views,
},
);
Ok(me)
}
fn build_targets(
&mut self,
alloc: &DeviceAllocator,
device: &VkDevice,
width: u32,
height: u32,
) -> RenderResult<()> {
let (w, h) = self.settings.trace_extent(width, height);
self.extent = vk::Extent2D {
width: w,
height: h,
};
let pooled = create_image(
alloc,
&ImageSpec {
width: w,
height: h,
format: HDR_FORMAT,
tiling: vk::ImageTiling::OPTIMAL,
usage: vk::ImageUsageFlags::COLOR_ATTACHMENT
| vk::ImageUsageFlags::SAMPLED
| vk::ImageUsageFlags::TRANSFER_SRC,
mem_props: vk::MemoryPropertyFlags::DEVICE_LOCAL,
samples: vk::SampleCountFlags::TYPE_1,
},
)?;
let image = pooled.image();
let view = create_image_view(device, image, HDR_FORMAT, vk::ImageAspectFlags::COLOR)?;
self.output = GpuImage::from_pooled(pooled, view);
self.framebuffer = device
.create_framebuffer(
&vk::FramebufferCreateInfo::default()
.render_pass(self.render_pass.handle())
.attachments(std::slice::from_ref(&self.output.view))
.width(w)
.height(h)
.layers(1),
)
.map_err(|e| crate::vulkan::error::map_vk_result(e, "rt framebuffer"))?;
Ok(())
}
pub(in crate::vulkan) fn rewire_geometry(
&self,
device: &VkDevice,
vertex_buffer: vk::Buffer,
index_buffer: vk::Buffer,
) {
for &set in &self.resolve_sets {
SetWrites::new(set)
.storage_buffer(3, vertex_buffer, vk::WHOLE_SIZE)
.storage_buffer(4, index_buffer, vk::WHOLE_SIZE)
.apply(device);
}
}
pub(in crate::vulkan) fn wire_static(&self, device: &VkDevice, inputs: RtStaticInputs) {
let RtStaticInputs {
vertex_buffer,
index_buffer,
hdr_resolve_views,
gbuffer_views,
roughness_views,
} = inputs;
self.rewire_geometry(device, vertex_buffer, index_buffer);
let frames = self.resolve_sets.len();
debug_assert_eq!(hdr_resolve_views.len(), frames);
debug_assert_eq!(gbuffer_views.len(), frames);
debug_assert_eq!(roughness_views.len(), frames);
for (i, &set) in self.resolve_sets.iter().enumerate() {
SetWrites::new(set)
.uniform_buffer(
0,
self.params_buffers[i].buffer(),
size_of::<RtParams>() as vk::DeviceSize,
)
.sampled_image(5, hdr_resolve_views[i])
.sampled_image(6, gbuffer_views[i])
.sampled_image(7, roughness_views[i])
.apply(device);
}
}
pub(in crate::vulkan) fn wire_dynamic(
&mut self,
device: &VkDevice,
frame_idx: usize,
accel: RtAccelHandles,
) {
if !self.wired_accel.changed(frame_idx, accel) {
return;
}
let RtAccelHandles {
tlas,
geom_buffer,
geom_size,
deformed_verts: deformed,
skinned_indices,
} = accel;
let sidx_buffer = if skinned_indices != vk::Buffer::null() {
skinned_indices
} else {
self.dummy_ssbo.buffer()
};
SetWrites::new(self.resolve_sets[frame_idx])
.acceleration_structure(1, tlas)
.storage_buffer(2, geom_buffer, geom_size)
.storage_buffer(9, deformed, vk::WHOLE_SIZE)
.storage_buffer(10, sidx_buffer, vk::WHOLE_SIZE)
.apply(device);
}
fn destroy_targets(&mut self, _device: &VkDevice) {
if !self.framebuffer.is_null() {
self.framebuffer = OwnedFramebuffer::null();
}
if self.output.image != vk::Image::null() {
self.output = GpuImage::null();
}
}
pub(in crate::vulkan) fn rebuild(
&mut self,
alloc: &DeviceAllocator,
device: &VkDevice,
width: u32,
height: u32,
inputs: RtStaticInputs,
) -> RenderResult<()> {
self.destroy_targets(device);
self.build_targets(alloc, device, width, height)?;
self.wire_static(device, inputs);
self.wired_accel.reset();
Ok(())
}
pub(in crate::vulkan) fn forget_accel(&mut self) {
self.wired_accel.reset();
}
pub(in crate::vulkan) fn swap_pipelines(&mut self, rebuilt: RebuiltRtPipelines) {
self.flat_pso = rebuilt.flat;
self.textured_pso = rebuilt.textured;
}
pub(in crate::vulkan) fn destroy(&mut self, device: &VkDevice) {
self.destroy_targets(device);
self.dummy_ssbo = PooledBuffer::null();
self.params_buffers.clear();
}
}
impl VkContext {
pub(in crate::vulkan) fn rt_reflections_active(&self) -> bool {
self.rt_reflections.is_some() && self.rt.accel.is_some()
}
pub(in crate::vulkan) fn planar_pass_needed(&self) -> bool {
planar_reflection::planar_pass_needed(
self.planar_reflection.is_some(),
self.transparent
.as_ref()
.is_some_and(|t| t.water_planar_slot_live()),
self.rt_transparent_active(),
)
}
pub(in crate::vulkan) fn rt_transparent_active(&self) -> bool {
self.rt_reflections_active()
&& self
.transparent
.as_ref()
.is_some_and(|t| t.rt_pipelines_ready())
}
pub(in crate::vulkan) fn rt_dynamic_update(
&mut self,
cmd: vk::CommandBuffer,
frame_idx: usize,
) {
let mut topology_dirty = std::mem::take(&mut self.state.gpu_dirty.rt_topology);
let device = self.hw.device.clone();
self.rt.collect_retired(self.frames_in_flight);
if self.rt_reflections.is_none() {
return;
}
if self.rt.accel.is_none() {
let skinned_present = self.rt_skinned_present();
if !seed_wanted(self.rt.dynamic_mode, topology_dirty, skinned_present) {
return;
}
match self.build_scene_accel(skinned_present) {
Ok(Some(accel)) => self.rt.accel = Some(accel),
Ok(None) => return,
Err(e) => {
crate::rt_report::report_rt_update(&mut self.rt.update_streak, Err(e));
return;
}
}
self.forget_wired_accel();
topology_dirty = false;
}
self.update_live_accel(cmd, frame_idx, topology_dirty);
let Some(accel) = self.rt.accel.as_ref() else {
return;
};
let (geom_buffer, geom_size) = accel.geom_table();
let tlas = accel.tlas();
let deformed = accel.deformed_verts();
let skinned_indices = accel.skinned_indices();
let Some(rt) = self.rt_reflections.as_mut() else {
return;
};
rt.wire_dynamic(
&device,
frame_idx,
RtAccelHandles {
tlas,
geom_buffer,
geom_size,
deformed_verts: deformed,
skinned_indices,
},
);
if let Some(transparent) = self.transparent.as_mut() {
transparent.wire_rt_dynamic(
&device,
frame_idx,
super::super::transparent::TransparentRtDynamic {
tlas,
geom_buffer,
geom_size,
deformed,
skinned_indices,
},
);
}
}
fn update_live_accel(
&mut self,
cmd: vk::CommandBuffer,
frame_idx: usize,
topology_dirty: bool,
) {
let skinned_inputs = self.rt_skinned_buffers();
let exclude_seethrough = self.seethrough_meshes_enabled();
let shared = super::super::raytrace::SharedGeometry::of(&self.geometry);
let Some(mut accel) = self.rt.accel.take() else {
return;
};
let joint_buffers = self
.skinned
.joint_buffers
.get(frame_idx)
.map(|b| b.as_slice())
.unwrap_or(&[]);
let skinned = skinned_inputs
.zip(self.rt.skin.as_mut())
.map(|((vb, ib), skin)| super::super::raytrace::SkinnedRtInputs {
objects: &self.state.skinned.draw_objects,
vertex_buffer: vb,
index_buffer: ib,
joint_buffers,
skin,
});
let updated = accel.dynamic_update(
super::super::raytrace::RtDeviceCtx {
alloc: &self.hw.alloc,
instance: &self.hw.instance,
device: &self.hw.device,
pd: self.hw.physical_device,
},
cmd,
&self.state.draw.objects,
super::super::raytrace::RtDynamicInputs {
policy: super::super::raytrace::RtRebuildPolicy {
mode: self.rt.dynamic_mode,
topology_dirty,
exclude_seethrough,
},
frame_idx,
shared,
skinned,
},
);
crate::rt_report::report_rt_update(&mut self.rt.update_streak, updated);
let spent = accel.is_spent(skinned_inputs.is_some());
self.rt.accel = Some(accel);
if spent {
self.rt.retire_accel();
}
}
pub(in crate::vulkan) fn encode_rt_reflections(
&self,
cmd: vk::CommandBuffer,
frame_idx: usize,
fov_y_radians: f32,
aspect: f32,
cam_pos: [f32; 3],
) {
let rt = match &self.rt_reflections {
Some(r) => r,
None => return,
};
let device = &self.hw.device;
let extent = rt.extent;
let rp_begin = vk::RenderPassBeginInfo::default()
.render_pass(rt.render_pass.handle())
.framebuffer(rt.framebuffer.handle())
.render_area(vk::Rect2D::default().extent(extent));
if self.rt.accel.is_none() {
let clear = vk::ClearAttachment {
aspect_mask: vk::ImageAspectFlags::COLOR,
color_attachment: 0,
clear_value: vk::ClearValue::default(),
};
let rect = vk::ClearRect {
rect: vk::Rect2D::default().extent(extent),
base_array_layer: 0,
layer_count: 1,
};
unsafe {
device.cmd_begin_render_pass(cmd, &rp_begin, vk::SubpassContents::INLINE);
device.cmd_clear_attachments(cmd, &[clear], &[rect]);
device.cmd_end_render_pass(cmd);
}
self.encode_reflection_composite(cmd, rt.output.view, frame_idx);
return;
}
let v = self.state.view.matrix;
let inv_view_rot = [
[v[0][0], v[1][0], v[2][0], 0.0],
[v[0][1], v[1][1], v[2][1], 0.0],
[v[0][2], v[1][2], v[2][2], 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let params = rt.settings.params(RtParamsInputs {
fov_y_radians,
aspect,
inv_view_rot,
cam_pos,
sun_dir: self.fog.sun_dir,
sun_color: self.fog.sun_color,
prefilter_mip_count: self.scene.prefilter_mip_count as f32,
sky_rot: self.state.view.sky_rot,
});
rt.params_buffers[frame_idx].write_val(0, ¶ms);
let textured = self.cull.bindless_pipeline.is_some() && rt.textured_pso.is_some();
let (pso, layout) = match (
textured,
rt.textured_pso.as_ref(),
rt.layout_textured.as_ref(),
) {
(true, Some(pso), Some(layout)) => (pso, layout),
_ => (&rt.flat_pso, &rt.layout_flat),
};
let vp = vk::Viewport {
x: 0.0,
y: 0.0,
width: extent.width as f32,
height: extent.height as f32,
min_depth: 0.0,
max_depth: 1.0,
};
let scissor = vk::Rect2D::default().extent(extent);
unsafe {
device.cmd_begin_render_pass(cmd, &rp_begin, vk::SubpassContents::INLINE);
device.cmd_set_viewport(cmd, 0, std::slice::from_ref(&vp));
device.cmd_set_scissor(cmd, 0, std::slice::from_ref(&scissor));
device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, pso.handle());
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
layout.handle(),
0,
std::slice::from_ref(&rt.resolve_sets[frame_idx]),
&[],
);
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
layout.handle(),
1,
std::slice::from_ref(&self.descriptors.global_sets[frame_idx]),
&[],
);
if textured {
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
layout.handle(),
2,
std::slice::from_ref(&self.cull.bindless_sets[frame_idx]),
&[],
);
}
device.cmd_draw(cmd, 3, 1, 0, 0);
device.cmd_end_render_pass(cmd);
}
self.encode_reflection_composite(cmd, rt.output.view, frame_idx);
}
}
#[cfg(test)]
mod tests {
#[test]
fn rt_reflections_shaders_compile() {
concinnity_shader::require_dxc!();
let shaders = super::compile_rt_shaders(false, 4).expect("rt shaders compile");
assert!(crate::vulkan::pipeline::is_spirv(&shaders.vs));
assert!(crate::vulkan::pipeline::is_spirv(&shaders.flat_fs));
assert!(shaders.textured_fs.is_some(), "pool_size>0 builds textured");
let flat_only = super::compile_rt_shaders(false, 0).expect("rt flat compiles");
assert!(flat_only.textured_fs.is_none());
}
}