1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
use {
bytemuck::cast_slice,
glam::{Mat4, Vec4},
inline_spirv::inline_spirv,
screen_13::prelude::*,
std::sync::Arc,
};
// NOTE: When this example runs, there will be a blank screen - that's OK!
/// This example does no real work, but rather just uses the api in order to call a few
/// shader pipelines in a fun and realistic manner.
///
/// The key principle is that you can lease resources (images and buffers) and compose
/// rendering operations with just a few lines of RenderGraph builder-pattern code.
fn main() -> Result<(), DisplayError> {
pretty_env_logger::init();
// Create a bunch of "pipelines" (shader code setup to run on the GPU) - we keep these
// around and just switch between which one we're using at any one point during a frame
let event_loop = EventLoop::new().build().unwrap();
let fill_quad_linear_gradient = create_fill_quad_linear_gradient_pipeline(&event_loop.device);
let draw_funky_shape_deferred = create_draw_funky_shape_deferred_pipeline(&event_loop.device);
// We also need a cache (this one is backed by a hashmap of resource info, fast but basic)
// There will be more cache types later and traits exposed
let mut cache = HashPool::new(&event_loop.device);
// Static index/vertex data courtesy of the polyhedron-ops library
let (indices, vertices) = funky_shape_triangle_mesh_buffers();
let index_count = indices.len() as u32;
let indices = cast_slice(&indices);
let vertices = cast_slice(&vertices);
// Pre-define some basic information structs we'll repeatedly use to acquire leased resources
// (Usually we would do this at the place of use, but for clarity its outside the loop here)
// (Note the event_loop height and width may change, and are provided in the frame context,
// but we're not using that in this demo so the image won't resize with the window!
let image_info = image_info_2d(event_loop.width(), event_loop.height());
let index_buf_info = index_buffer_info(indices.len() as vk::DeviceSize);
let vertex_buf_info = vertex_buffer_info(vertices.len() as vk::DeviceSize);
// Some colors for readability
let red = [0xffu8, 0x00, 0x00, 0xff];
let green = [0x00u8, 0xff, 0x00, 0xff];
let blue = [0x00u8, 0x00, 0xff, 0xff];
let magenta = [0xffu8, 0x00, 0xff, 0xff];
let white = [0xffu8, 0xff, 0xff, 0xff];
// Event loop runs the frame callback on the current thread
event_loop.run(|frame| {
// We are now rendering a frame for the provided swapchain image node and render graph.
let graph = frame.render_graph;
let swapchain_image = frame.swapchain_image;
// Part 1: Get and prepare some resources - you could have Binding instances that are
// bound to, used on, and then later unbound from a graph and repeated like that each
// frame, or, you could lease things and let the magic of Arc<T> just handle it. Here
// We lease things, and so we have to fill them freshly each time.
// Lease + fill + bind a buffer: the questionably-readable three line way
let mut index_buf = cache.lease(index_buf_info).unwrap();
Buffer::mapped_slice_mut(&mut index_buf)[0..indices.len()].copy_from_slice(indices);
let index_buf = graph.bind_node(index_buf);
// Lease + fill + bind a buffer: maybe a more sane looking way of doing it
let vertex_buf = graph.bind_node({
let mut buf = cache.lease(vertex_buf_info).unwrap();
let data = Buffer::mapped_slice_mut(&mut buf);
data[0..vertices.len()].copy_from_slice(vertices);
buf
});
// Lease a couple images (they may be blank or have pictures of cats in them but they are
// valid/ready)
let _image1 = graph.bind_node(cache.lease(image_info).unwrap());
let _image2 = graph.bind_node(cache.lease(image_info).unwrap());
let _image3 = graph.bind_node(cache.lease(image_info).unwrap());
// You can instead do this:
let image1 = graph.bind_node({
let mut img = cache.lease(image_info).unwrap();
img.name = Some("image1".to_owned());
img
});
let image2 = graph.bind_node({
let mut img = cache.lease(image_info).unwrap();
img.name = Some("image2".to_owned());
img
});
let image3 = graph.bind_node({
let mut img = cache.lease(image_info).unwrap();
img.name = Some("image3".to_owned());
img
});
// Part 2: Do things to the graph! Build passes where each pass contains:
// - Access to nodes: declare either read/write/or specific access
// - Pipeline configuration: tell it what depth settings and push constants to send
// - Read descriptor bindings and load/store color values, have fun, yay!!
// You can record two or more draws in a single pass; they inherit the draw state
// from above calls. In this case we reset the "store" between draws but we do not
// bother resetting the "clear" state as you can see image2 will be cleared with
// white also.
graph
.begin_pass("gradients")
.bind_pipeline(&fill_quad_linear_gradient)
.clear_color_value(0, white)
.store_color(0, image1)
.record_subpass(move |subpass| {
subpass.push_constants_offset(0, &red);
subpass.push_constants_offset(4, &blue);
subpass.draw(6, 1, 0, 0);
})
.store_color(0, image2)
.record_subpass(move |subpass| {
// We updated the constants and which attachment is getting stored, but otherwise
// same pipeline config here
subpass.push_constants_offset(0, &magenta);
subpass.push_constants_offset(4, &green);
subpass.draw(6, 1, 0, 0);
});
// The above is "one pass" which logically happens first but physically may happen later
// once the hardware schedules it - but it can't do that until we hand the graph over
// at the bottom of the closure -> Screen 13 takes the graph and presents it to the
// swapchain so long as we do something (transfer/write/compute) to the swapchain the
// correct operations will be sent to the display. You just need to record some passes to
// the graph.
// Alternatively to the above, you might just record two passes, bind two pipelines, etc. As
// long as they're setup the same they will be trivially merged together or moved apart -
// whatever ends up being best. In the above case because we didn't start a second
// "begin_pass" call, we are not allowing the GPU to break up this unit of work. Maybe in
// general it's a good idea to record lots of short passes so the resolver code has more to
// work with.
// Let's do some more work... This draws the funky shape into image3.
graph
.begin_pass("This text shows up in debuggers like RenderDoc")
.bind_pipeline(&draw_funky_shape_deferred)
.access_node(index_buf, AccessType::IndexBuffer) // We must call access on the buffers
.access_node(vertex_buf, AccessType::VertexBuffer) // because we use them in a subpass
.clear_color(0)
.read_descriptor((0, [0]), image1) // We are declaring the read on image1 here
.read_descriptor((0, [1]), image2) // and the second array item will be image2
.store_color(0, image3) // and we declare we're writing the results to image3
.record_subpass(move |subpass| {
subpass
.push_constants_offset(0, cast_slice(&Mat4::IDENTITY.to_cols_array()))
.push_constants_offset(64, cast_slice(&Vec4::ONE.to_array()))
.bind_index_buffer(index_buf, vk::IndexType::UINT32)
.bind_vertex_buffer(vertex_buf)
.draw(index_count, 1, 0, 0);
});
// This will suffice as a way to get image3 presented - although you might want to check out the
// included presenter types for more advanced display techniques. This issues a copy command at this
// logical point in the graph - nothing is copied "yet" - it copies when the graph resolves later
graph.copy_image(image3, swapchain_image);
// Uncomment the last line if you want to instead draw a magenta screen.
// NOTE: This will not cancel the above render passes; they will still run.
//graph.clear_color_image(swapchain_image, 1.0, 0.0, 1.0, 1.0);
})
}
const fn index_buffer_info(size: vk::DeviceSize) -> BufferInfo {
BufferInfo {
size,
usage: vk::BufferUsageFlags::INDEX_BUFFER,
can_map: true,
}
}
const fn vertex_buffer_info(size: vk::DeviceSize) -> BufferInfo {
BufferInfo {
size,
usage: vk::BufferUsageFlags::VERTEX_BUFFER,
can_map: true,
}
}
fn image_info_2d(width: u32, height: u32) -> ImageInfo {
// Currently this is bad API you MUST specify usage of the image, but it's not part of the ctor
ImageInfo::new_2d(
vk::Format::R8G8B8A8_UNORM,
width,
height,
vk::ImageUsageFlags::SAMPLED
| vk::ImageUsageFlags::STORAGE
| vk::ImageUsageFlags::COLOR_ATTACHMENT
| vk::ImageUsageFlags::INPUT_ATTACHMENT
| vk::ImageUsageFlags::TRANSFER_DST
| vk::ImageUsageFlags::TRANSFER_SRC,
)
.build()
// Additional builder functions that might be of interest:
// .tiling(vk::ImageTiling::OPTIMAL)) <- Thinking about removing - LEAVE AT OPTIMAL ALWAYS
// .mip_level_count(1)
// .array_elements(1)
// .sample_count(SampleCount::X1)
}
fn create_fill_quad_linear_gradient_pipeline(device: &Arc<Device>) -> Arc<GraphicPipeline> {
let vertex_shader = Shader::new_vertex(
inline_spirv!(
r#"
#version 460 core
const vec2 POSITION[6] = vec2[6](
vec2(-1, -1), vec2(-1, 1), vec2( 1, -1),
vec2( 1, 1), vec2( 1, -1), vec2(-1, 1)
);
layout(location = 0) out float vk_Blend;
void main() {
gl_Position = vec4(POSITION[gl_VertexIndex], 0, 1);
vk_Blend = gl_Position.x * -0.5 + 0.5;
}
"#,
vert
)
.as_slice(),
);
let fragment_shader = Shader::new_fragment(
inline_spirv!(
r#"
#version 460 core
layout(push_constant) uniform PushConstants {
layout(offset = 0) vec4 start_color;
layout(offset = 16) vec4 end_color;
} push_constants;
layout(location = 0) in float blend;
layout(location = 0) out vec4 vk_Color;
void main() {
vk_Color = mix(push_constants.start_color, push_constants.end_color, blend);
}
"#,
frag
)
.as_slice(),
);
Arc::new(
GraphicPipeline::create(
device,
GraphicPipelineInfo::new().blend(BlendMode::ALPHA),
[vertex_shader, fragment_shader],
)
.unwrap(),
)
}
// Oh please somebody PR a really nice shader here
fn create_draw_funky_shape_deferred_pipeline(device: &Arc<Device>) -> Arc<GraphicPipeline> {
let vertex_shader = Shader::new_vertex(
inline_spirv!(
r#"
#version 460 core
layout(push_constant) uniform PushConstants {
layout(offset = 0) mat4 transform;
} push_constants;
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 0) out vec3 vk_Normal;
void main() {
gl_Position = push_constants.transform * vec4(position, 1);
vk_Normal = normal;
}
"#,
vert
)
.as_slice(),
);
let fragment_shader = Shader::new_fragment(
inline_spirv!(
r#"
#version 460 core
layout(push_constant) uniform PushConstants {
layout(offset = 0) mat4 transform;
layout(offset = 64) vec4 coolness_factor;
} push_constants;
layout(location = 0) in vec3 normal;
layout(location = 0) out vec4 vk_Color;
void main() {
vk_Color = push_constants.coolness_factor * vec4(normal, 1);
}
"#,
frag
)
.as_slice(),
);
Arc::new(
GraphicPipeline::create(
device,
GraphicPipelineInfo::new()
.cull_mode(vk::CullModeFlags::NONE)
.two_sided(true),
[vertex_shader, fragment_shader],
)
.unwrap(),
)
}
/// Returns index buffer and position/normal buffer (polyhedron_ops you are 🥇🏆🥂💯)
fn funky_shape_triangle_mesh_buffers() -> (Vec<u32>, Vec<[f32; 6]>) {
let (indices, positions, normals) = polyhedron_ops::Polyhedron::dodecahedron()
.chamfer(None, true)
.propeller(None, true)
.ambo(None, true)
.gyro(None, None, true)
.finalize()
.to_triangle_mesh_buffers();
let vertices = positions
.into_iter()
.zip(normals.into_iter())
.map(|(position, normal)| {
[
position.x, position.y, position.z, normal.x, normal.y, normal.z,
]
})
.collect();
(indices, vertices)
}