Post processing shader, extended .prog format to include attachment format and depth info

This commit is contained in:
sergeypdev 2024-12-15 20:09:37 +04:00
parent baf3e2fee8
commit 1181e56236
9 changed files with 229 additions and 82 deletions

View File

@ -1,20 +1,40 @@
// Input, output blocks #extension GL_EXT_scalar_block_layout : require
VERTEX_EXPORT VertexData { #extension GL_EXT_nonuniform_qualifier : require
vec2 uv;
} VertexOut; #include "global.glsl"
#if VERTEX_SHADER #if VERTEX_SHADER
layout(location = 0) in vec3 aPos; // Input, output blocks
layout(location = 0) out VertexData {
vec2 uv;
} VertexOut;
// QUAD
vec2 positions[6] = vec2[](
vec2(-1, -1),
vec2(-1, 1),
vec2(1, 1),
vec2(1, 1),
vec2(1, -1),
vec2(-1, -1)
);
void main() { void main() {
gl_Position = vec4(aPos, 1); gl_Position = vec4(positions[gl_VertexIndex], 0, 1);
VertexOut.uv = aPos.xy * 0.5 + 0.5; VertexOut.uv = positions[gl_VertexIndex].xy * 0.5 + 0.5;
} }
#endif // VERTEX_SHADER #endif // VERTEX_SHADER
#if FRAGMENT_SHADER #if FRAGMENT_SHADER
// Input, output blocks
layout(location = 0) in VertexData {
vec2 uv;
} VertexOut;
// Translated from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl // Translated from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
const mat3 ACESInputMat = mat3( const mat3 ACESInputMat = mat3(
@ -51,9 +71,10 @@ vec3 ACESFitted(vec3 color)
return color; return color;
} }
layout(binding = 0) uniform sampler2D screen_sampler; layout(push_constant, std430) uniform constants {
uint scene_color_texture;
out vec4 FragColor; uint scene_color_sampler;
} PushConstants;
vec3 linearToSRGB(vec3 color) { vec3 linearToSRGB(vec3 color) {
vec3 x = color * 12.92f; vec3 x = color * 12.92f;
@ -67,8 +88,10 @@ vec3 linearToSRGB(vec3 color) {
return clr; return clr;
} }
layout(location = 0) out vec4 FragColor;
void main() { void main() {
vec3 hdr_color = texture(screen_sampler, VertexOut.uv).rgb; vec3 hdr_color = texture(sampler2D(global_textures2d[PushConstants.scene_color_texture], global_samplers[PushConstants.scene_color_sampler]), VertexOut.uv).rgb;
hdr_color = ACESFitted(hdr_color); hdr_color = ACESFitted(hdr_color);
FragColor.rgb = hdr_color; FragColor.rgb = hdr_color;

View File

@ -0,0 +1,7 @@
{
"vertex": "post_process.glsl",
"fragment": "post_process.glsl",
"compute": null,
"color_attachment_type": "swapchain",
"depth_stencil_attachment": false
}

View File

@ -1,7 +0,0 @@
{
"shader": "post_process.glsl",
"vertex": true,
"fragment": true,
"compute": false
}

View File

@ -19,15 +19,6 @@ vec3 colors[3] = vec3[](
layout(location = 0) out vec3 VertexColor; layout(location = 0) out vec3 VertexColor;
layout(push_constant) uniform constants {
vec3 my_vec;
float my_float;
mat4x4 my_mat;
uint tex_index1;
uint tex_index2;
uint tex_index3;
} PushConstants;
void main() { void main() {
VertexColor = colors[gl_VertexIndex]; VertexColor = colors[gl_VertexIndex];
@ -43,7 +34,7 @@ layout(location = 0) in vec3 VertexColor;
layout(location = 0) out vec4 FragColor; layout(location = 0) out vec4 FragColor;
void main() { void main() {
FragColor = vec4(VertexColor, 1.0); FragColor = vec4(VertexColor * 10, 1.0);
} }
#endif #endif

View File

@ -1,5 +1,7 @@
{ {
"vertex": "triangle.glsl", "vertex": "triangle.glsl",
"fragment": "triangle.glsl", "fragment": "triangle.glsl",
"compute": null "compute": null,
"color_attachment_type": "main",
"depth_stencil_attachment": true
} }

View File

@ -369,6 +369,7 @@ fn loadShaderProgramErr(self: *AssetManager, id: AssetId) !LoadedShaderProgram {
var push_constant_ranges_buf: [2]vk.PushConstantRange = undefined; var push_constant_ranges_buf: [2]vk.PushConstantRange = undefined;
const push_constant_ranges = getPushConstantRanges(program, &push_constant_ranges_buf); const push_constant_ranges = getPushConstantRanges(program, &push_constant_ranges_buf);
std.debug.print("push constant ranges: {any}\n", .{push_constant_ranges});
// TODO: parse from shaders or something // TODO: parse from shaders or something
const pipeline_layout = try self.gc.device.createPipelineLayout(&.{ const pipeline_layout = try self.gc.device.createPipelineLayout(&.{
@ -405,9 +406,12 @@ fn loadShaderProgramErr(self: *AssetManager, id: AssetId) !LoadedShaderProgram {
vk.GraphicsPipelineCreateInfo{ vk.GraphicsPipelineCreateInfo{
.p_next = &vk.PipelineRenderingCreateInfo{ .p_next = &vk.PipelineRenderingCreateInfo{
.color_attachment_count = 1, .color_attachment_count = 1,
.p_color_attachment_formats = &[_]vk.Format{.r16g16b16a16_sfloat}, .p_color_attachment_formats = &[_]vk.Format{switch (graphics_pipeline.color_attachment_type) {
.depth_attachment_format = .d24_unorm_s8_uint, .main => .r16g16b16a16_sfloat,
.stencil_attachment_format = .d24_unorm_s8_uint, .swapchain => .r8g8b8a8_unorm,
}},
.depth_attachment_format = if (graphics_pipeline.depth_stencil_attachment) .d24_unorm_s8_uint else .undefined,
.stencil_attachment_format = if (graphics_pipeline.depth_stencil_attachment) .d24_unorm_s8_uint else .undefined,
.view_mask = 0, .view_mask = 0,
}, },
.base_pipeline_index = 0, .base_pipeline_index = 0,

View File

@ -400,7 +400,7 @@ fn allocateRenderTarget(self: *Render2) !MainRenderTarget {
.mip_count = 1, .mip_count = 1,
.sync_state = .{}, .sync_state = .{},
}, },
.color_descriptor = self.createPerFrameImageDescriptor(color_image_view, .read_only_optimal), .color_descriptor = self.createPerFrameImageDescriptor(color_image_view, .shader_read_only_optimal),
}; };
} }
@ -453,6 +453,10 @@ fn createPerFrameImageDescriptor(self: *Render2, view: vk.ImageView, layout: vk.
return result; return result;
} }
fn pushConstants(cmds: GraphicsContext.CommandBuffer, layout: vk.PipelineLayout, stage_flags: vk.ShaderStageFlags, value: anytype) void {
cmds.pushConstants(layout, stage_flags, 0, @sizeOf(@TypeOf(value)), &value);
}
pub fn draw(self: *Render2) !void { pub fn draw(self: *Render2) !void {
const gc = self.gc; const gc = self.gc;
const device = gc.device; const device = gc.device;
@ -504,7 +508,7 @@ pub fn draw(self: *Render2) !void {
const global_descriptor_set = self.global_descriptor_set; const global_descriptor_set = self.global_descriptor_set;
device.updateDescriptorSets(1, &.{ device.updateDescriptorSets(2, &.{
vk.WriteDescriptorSet{ vk.WriteDescriptorSet{
.dst_set = global_descriptor_set, .dst_set = global_descriptor_set,
.dst_binding = 0, .dst_binding = 0,
@ -521,6 +525,22 @@ pub fn draw(self: *Render2) !void {
.p_image_info = &[_]vk.DescriptorImageInfo{}, .p_image_info = &[_]vk.DescriptorImageInfo{},
.p_texel_buffer_view = &[_]vk.BufferView{}, .p_texel_buffer_view = &[_]vk.BufferView{},
}, },
vk.WriteDescriptorSet{
.dst_set = global_descriptor_set,
.dst_binding = 1,
.dst_array_element = 0,
.descriptor_type = .sampler,
.descriptor_count = 1,
.p_image_info = &.{
vk.DescriptorImageInfo{
.sampler = self.screen_color_sampler,
.image_view = .null_handle,
.image_layout = .undefined,
},
},
.p_buffer_info = &[_]vk.DescriptorBufferInfo{},
.p_texel_buffer_view = &[_]vk.BufferView{},
},
}, 0, null); }, 0, null);
// TODO: move this into descriptorman? // TODO: move this into descriptorman?
@ -642,38 +662,112 @@ pub fn draw(self: *Render2) !void {
cmds.draw(3, 2, 0, 0); cmds.draw(3, 2, 0, 0);
} }
try color_image.sync(cmds, .{ .stage_mask = .{ .blit_bit = true }, .access_mask = .{ .transfer_read_bit = true } }, .transfer_src_optimal, .{ .color_bit = true }); // Post process and convert from f16 to rgba8_unorm
try swapchain_image.sync(cmds, .{ .stage_mask = .{ .blit_bit = true }, .access_mask = .{ .transfer_write_bit = true } }, .transfer_dst_optimal, .{ .color_bit = true }); {
cmds.blitImage( try swapchain_image.sync(
color_image.handle, cmds,
color_image.sync_state.layout, .{
swapchain_image.handle, .stage_mask = .{ .color_attachment_output_bit = true },
swapchain_image.sync_state.layout, .access_mask = .{ .color_attachment_write_bit = true },
1,
&.{vk.ImageBlit{
.src_subresource = vk.ImageSubresourceLayers{
.aspect_mask = .{ .color_bit = true },
.mip_level = 0,
.base_array_layer = 0,
.layer_count = 1,
}, },
.src_offsets = .{ .color_attachment_optimal,
vk.Offset3D{ .x = 0, .y = 0, .z = 0 }, .{ .color_bit = true },
vk.Offset3D{ .x = @intCast(gc.swapchain_extent.width), .y = @intCast(gc.swapchain_extent.height), .z = 1 },
},
.dst_subresource = vk.ImageSubresourceLayers{
.aspect_mask = .{ .color_bit = true },
.mip_level = 0,
.base_array_layer = 0,
.layer_count = 1,
},
.dst_offsets = .{
vk.Offset3D{ .x = 0, .y = 0, .z = 0 },
vk.Offset3D{ .x = @intCast(gc.swapchain_extent.width), .y = @intCast(gc.swapchain_extent.height), .z = 1 },
},
}},
.nearest,
); );
try color_image.sync(
cmds,
.{
.stage_mask = .{ .fragment_shader_bit = true },
.access_mask = .{ .shader_sampled_read_bit = true },
},
.shader_read_only_optimal,
.{ .color_bit = true },
);
cmds.beginRendering(&.{
.render_area = vk.Rect2D{ .offset = .{ .x = 0, .y = 0 }, .extent = gc.swapchain_extent },
.layer_count = 1,
.view_mask = 0,
.color_attachment_count = 1,
.p_color_attachments = &.{
vk.RenderingAttachmentInfo{
.clear_value = .{ .color = .{ .float_32 = .{ 0.8, 0.7, 0.6, 1.0 } } },
.load_op = .dont_care,
.store_op = .store,
.image_layout = .color_attachment_optimal,
.image_view = swapchain_image_view,
.resolve_image_layout = .color_attachment_optimal,
.resolve_mode = .{},
},
},
.p_depth_attachment = null,
.p_stencil_attachment = null,
});
defer cmds.endRendering();
cmds.setDepthTestEnable(vk.FALSE);
cmds.setDepthWriteEnable(vk.FALSE);
const post_process = self.assetman.resolveShaderProgram(a.ShaderPrograms.shaders.post_process);
cmds.bindPipeline(.graphics, post_process.pipeline);
cmds.bindDescriptorSets(.graphics, post_process.layout, 0, 1, &.{global_descriptor_set}, 0, null);
pushConstants(cmds, post_process.layout, .{ .fragment_bit = true }, PostProcessPushConstants{
.scene_color_texture = main_render_target.color_descriptor.index(),
.scene_color_sampler = 0,
});
cmds.setViewportWithCount(1, &.{vk.Viewport{
.x = 0,
.y = 0,
.width = @floatFromInt(gc.swapchain_extent.width),
.height = @floatFromInt(gc.swapchain_extent.height),
.min_depth = 0,
.max_depth = 1,
}});
cmds.setScissorWithCount(1, &.{vk.Rect2D{
.offset = .{ .x = 0, .y = 0 },
.extent = gc.swapchain_extent,
}});
cmds.draw(6, 1, 0, 0);
}
// Direct blit without PP
// if (false) {
// try color_image.sync(cmds, .{ .stage_mask = .{ .blit_bit = true }, .access_mask = .{ .transfer_read_bit = true } }, .transfer_src_optimal, .{ .color_bit = true });
// try swapchain_image.sync(cmds, .{ .stage_mask = .{ .blit_bit = true }, .access_mask = .{ .transfer_write_bit = true } }, .transfer_dst_optimal, .{ .color_bit = true });
// cmds.blitImage(
// color_image.handle,
// color_image.sync_state.layout,
// swapchain_image.handle,
// swapchain_image.sync_state.layout,
// 1,
// &.{vk.ImageBlit{
// .src_subresource = vk.ImageSubresourceLayers{
// .aspect_mask = .{ .color_bit = true },
// .mip_level = 0,
// .base_array_layer = 0,
// .layer_count = 1,
// },
// .src_offsets = .{
// vk.Offset3D{ .x = 0, .y = 0, .z = 0 },
// vk.Offset3D{ .x = @intCast(gc.swapchain_extent.width), .y = @intCast(gc.swapchain_extent.height), .z = 1 },
// },
// .dst_subresource = vk.ImageSubresourceLayers{
// .aspect_mask = .{ .color_bit = true },
// .mip_level = 0,
// .base_array_layer = 0,
// .layer_count = 1,
// },
// .dst_offsets = .{
// vk.Offset3D{ .x = 0, .y = 0, .z = 0 },
// vk.Offset3D{ .x = @intCast(gc.swapchain_extent.width), .y = @intCast(gc.swapchain_extent.height), .z = 1 },
// },
// }},
// .nearest,
// );
// }
try swapchain_image.sync(cmds, .{ .stage_mask = .{}, .access_mask = .{} }, .present_src_khr, .{ .color_bit = true }); try swapchain_image.sync(cmds, .{ .stage_mask = .{}, .access_mask = .{} }, .present_src_khr, .{ .color_bit = true });
} }
try cmds.endCommandBuffer(); try cmds.endCommandBuffer();
@ -837,3 +931,8 @@ const GlobalUniform = extern struct {
view: View, view: View,
}; };
const PostProcessPushConstants = extern struct {
scene_color_texture: u32,
scene_color_sampler: u32,
};

View File

@ -129,7 +129,7 @@ pub const ShaderProgram = union(ShaderProgramPipelineType) {
}; };
pub const ShaderStage = struct { pub const ShaderStage = struct {
source: []u8, source: []align(4) u8,
push_constant_range: PushConstantRange, push_constant_range: PushConstantRange,
pub fn serialize(self: *ShaderStage, serializer: *Serializer) !void { pub fn serialize(self: *ShaderStage, serializer: *Serializer) !void {
@ -140,10 +140,17 @@ pub const ShaderProgram = union(ShaderProgramPipelineType) {
} }
}; };
pub const ColorAttachmentType = enum(u8) {
main = 0,
swapchain = 1,
};
pub const GraphicsPipeline = struct { pub const GraphicsPipeline = struct {
// TODO: extend to support tesselation, geometry, mesh shaders and etc // TODO: extend to support tesselation, geometry, mesh shaders and etc
vertex: ShaderStage, vertex: ShaderStage,
fragment: ShaderStage, fragment: ShaderStage,
color_attachment_type: ColorAttachmentType,
depth_stencil_attachment: bool,
}; };
pub const ComputePipeline = struct { pub const ComputePipeline = struct {
@ -169,6 +176,8 @@ pub const ShaderProgram = union(ShaderProgramPipelineType) {
try self.graphics.vertex.serialize(serializer); try self.graphics.vertex.serialize(serializer);
try self.graphics.fragment.serialize(serializer); try self.graphics.fragment.serialize(serializer);
try serializer.serializeEnum(ColorAttachmentType, &self.graphics.color_attachment_type);
try serializer.serializeBool(&self.graphics.depth_stencil_attachment);
}, },
.compute => { .compute => {
if (!serializer.write) { if (!serializer.write) {
@ -198,6 +207,15 @@ pub const Serializer = struct {
} }
} }
pub fn serializeEnum(self: *Serializer, comptime T: type, data: *T) !void {
if (@typeInfo(T) != .Enum) {
@compileError("serializeEnum expects enum");
}
const IntType = @typeInfo(T).Enum.tag_type;
try self.serializeInt(IntType, @ptrCast(data));
}
pub fn serializeBool(self: *Serializer, data: *bool) !void { pub fn serializeBool(self: *Serializer, data: *bool) !void {
var buf: [1]u8 = undefined; var buf: [1]u8 = undefined;
if (self.write) { if (self.write) {

View File

@ -626,6 +626,7 @@ fn processShader(allocator: std.mem.Allocator, flags: []const []const u8, input:
}, },
} }
// TODO: align pointer to 4 bytes
var result = ProcessedShader{ .spirv = compile_result.stdout }; var result = ProcessedShader{ .spirv = compile_result.stdout };
{ {
@ -633,25 +634,20 @@ fn processShader(allocator: std.mem.Allocator, flags: []const []const u8, input:
try spvReflectTry(c.spvReflectCreateShaderModule(compile_result.stdout.len, compile_result.stdout.ptr, &shader_module)); try spvReflectTry(c.spvReflectCreateShaderModule(compile_result.stdout.len, compile_result.stdout.ptr, &shader_module));
defer c.spvReflectDestroyShaderModule(&shader_module); defer c.spvReflectDestroyShaderModule(&shader_module);
if (shader_module.push_constant_blocks != null) { var spv_result: c.SpvReflectResult = c.SPV_REFLECT_RESULT_SUCCESS;
const push_constant_block: ?*const c.SpvReflectBlockVariable = @ptrCast(c.spvReflectGetEntryPointPushConstantBlock(&shader_module, "main", &spv_result));
spvReflectTry(spv_result) catch |err| switch (err) {
error.SPV_REFLECT_RESULT_ERROR_ELEMENT_NOT_FOUND => {},
else => return err,
};
if (push_constant_block) |block| {
// Assuming single push constant block per stage, this is what glslc enforces // Assuming single push constant block per stage, this is what glslc enforces
std.debug.assert(shader_module.push_constant_block_count == 1); std.debug.assert(shader_module.push_constant_block_count == 1);
const block = shader_module.push_constant_blocks[0];
result.push_constant_range = .{ .offset = block.offset, .size = block.size }; result.push_constant_range = .{ .offset = block.offset, .size = block.size };
} }
} }
// NOTE: Dep file is technically incorrect, but zig build system doesn't care, it will collect all dependencies after colon
// even if they are not for the same file it's processing
// TODO: figure out a better way to handle depfile
// if (maybe_dep_file) |dep_file| {
// const file = try std.fs.cwd().openFile(dep_file, .{ .mode = .read_write });
// defer file.close();
// try file.seekFromEnd(0);
// try file.writeAll(old_depfile_contents);
// }
return result; return result;
} }
@ -709,6 +705,8 @@ fn processShaderProgram(allocator: std.mem.Allocator, input: []const u8, output_
vertex: ?[]const u8, vertex: ?[]const u8,
fragment: ?[]const u8, fragment: ?[]const u8,
compute: ?[]const u8, compute: ?[]const u8,
color_attachment_type: []const u8,
depth_stencil_attachment: bool,
}; };
const program = try std.json.parseFromSlice(InputShaderProgram, allocator, file_contents, .{}); const program = try std.json.parseFromSlice(InputShaderProgram, allocator, file_contents, .{});
defer program.deinit(); defer program.deinit();
@ -717,14 +715,26 @@ fn processShaderProgram(allocator: std.mem.Allocator, input: []const u8, output_
if (program.value.vertex != null and program.value.fragment != null) { if (program.value.vertex != null and program.value.fragment != null) {
result = .{ .graphics = undefined }; result = .{ .graphics = undefined };
result.graphics.color_attachment_type = blk: {
if (std.mem.eql(u8, program.value.color_attachment_type, "main")) {
break :blk .main;
}
if (std.mem.eql(u8, program.value.color_attachment_type, "swapchain")) {
break :blk .swapchain;
}
return error.UnsupportedColorAttachmentType;
};
result.graphics.depth_stencil_attachment = program.value.depth_stencil_attachment;
// TODO: remove duplication // TODO: remove duplication
{ {
const stage = program.value.vertex.?; const stage = program.value.vertex.?;
const shader_source_path = try std.fs.path.resolve(allocator, &.{ input_dir, stage }); const shader_source_path = try std.fs.path.resolve(allocator, &.{ input_dir, stage });
const relative_path = try std.fs.path.relative(allocator, try std.fs.cwd().realpathAlloc(allocator, "."), shader_source_path); const relative_path = try std.fs.path.relative(allocator, try std.fs.cwd().realpathAlloc(allocator, "."), shader_source_path);
const shader = try processShader(allocator, &.{ "-DVERTEX_SHADER=1", "-fshader-stage=vert" }, relative_path, dep_file); const shader = try processShader(allocator, &.{ "-DVERTEX_SHADER=1", "-fshader-stage=vert", "-fpreserve-bindings" }, relative_path, dep_file);
result.graphics.vertex.source = shader.spirv; result.graphics.vertex.source = @alignCast(shader.spirv);
result.graphics.vertex.push_constant_range = .{ .offset = shader.push_constant_range.offset, .size = shader.push_constant_range.size }; result.graphics.vertex.push_constant_range = .{ .offset = shader.push_constant_range.offset, .size = shader.push_constant_range.size };
} }
{ {
@ -732,8 +742,8 @@ fn processShaderProgram(allocator: std.mem.Allocator, input: []const u8, output_
const shader_source_path = try std.fs.path.resolve(allocator, &.{ input_dir, stage }); const shader_source_path = try std.fs.path.resolve(allocator, &.{ input_dir, stage });
const relative_path = try std.fs.path.relative(allocator, try std.fs.cwd().realpathAlloc(allocator, "."), shader_source_path); const relative_path = try std.fs.path.relative(allocator, try std.fs.cwd().realpathAlloc(allocator, "."), shader_source_path);
const shader = try processShader(allocator, &.{ "-DFRAGMENT_SHADER=1", "-fshader-stage=frag" }, relative_path, dep_file); const shader = try processShader(allocator, &.{ "-DFRAGMENT_SHADER=1", "-fshader-stage=frag", "-fpreserve-bindings" }, relative_path, dep_file);
result.graphics.fragment.source = shader.spirv; result.graphics.fragment.source = @alignCast(shader.spirv);
result.graphics.fragment.push_constant_range = .{ .offset = shader.push_constant_range.offset, .size = shader.push_constant_range.size }; result.graphics.fragment.push_constant_range = .{ .offset = shader.push_constant_range.offset, .size = shader.push_constant_range.size };
} }
} else if (program.value.compute != null) { } else if (program.value.compute != null) {
@ -744,7 +754,7 @@ fn processShaderProgram(allocator: std.mem.Allocator, input: []const u8, output_
const relative_path = try std.fs.path.relative(allocator, try std.fs.cwd().realpathAlloc(allocator, "."), shader_source_path); const relative_path = try std.fs.path.relative(allocator, try std.fs.cwd().realpathAlloc(allocator, "."), shader_source_path);
const shader = try processShader(allocator, &.{ "-DCOMPUTE_SHADER=1", "-fshader-stage=compute" }, relative_path, dep_file); const shader = try processShader(allocator, &.{ "-DCOMPUTE_SHADER=1", "-fshader-stage=compute" }, relative_path, dep_file);
result.compute.compute.source = shader.spirv; result.compute.compute.source = @alignCast(shader.spirv);
result.compute.compute.push_constant_range = .{ .offset = shader.push_constant_range.offset, .size = shader.push_constant_range.size }; result.compute.compute.push_constant_range = .{ .offset = shader.push_constant_range.offset, .size = shader.push_constant_range.size };
} else { } else {
std.log.err("Provide vertex and fragment shaders for a graphics pipeline or a compute shader for a compute pipeline\n", .{}); std.log.err("Provide vertex and fragment shaders for a graphics pipeline or a compute shader for a compute pipeline\n", .{});