Put Vulkan in your GTK with
VkArea
1const std = @import("std");
2const builtin = @import("builtin");
3
4const gobject = @import("gobject");
5const gio = @import("gio");
6const gtk = @import("gtk");
7const vk = @import("vulkan");
8
9const VkArea = @import("vk_area.zig").VkArea;
10
11const triangle_spv = @embedFile("triangle_spv");
12
13const Application = extern struct {
14 parent: Parent,
15 pub const Parent = gtk.Application;
16
17 pub const getGObjectType = gobject.ext.defineClass(Application, .{
18 .name = "GtkVkTestApplication",
19 .classInit = &Class.init,
20 });
21
22 pub fn new() *Application {
23 return gobject.ext.newInstance(Application, .{
24 .application_id = "me.pluie.GtkVkTest",
25 });
26 }
27
28 pub fn as(app: *Application, comptime T: type) *T {
29 return gobject.ext.as(T, app);
30 }
31
32 fn activateImpl(app: *Application) callconv(.c) void {
33 Window.new(app).as(gtk.Window).present();
34 }
35
36 pub const Class = extern struct {
37 parent_class: Parent.Class,
38
39 pub const Instance = Application;
40
41 pub fn as(class: *Class, comptime T: type) *T {
42 return gobject.ext.as(T, class);
43 }
44
45 fn init(class: *Class) callconv(.c) void {
46 gio.Application.virtual_methods.activate.implement(class, &Application.activateImpl);
47 }
48 };
49};
50
51const Window = extern struct {
52 parent_instance: Parent,
53 pub const Parent = gtk.ApplicationWindow;
54
55 const Private = struct {
56 shaders: vk.ShaderModule,
57 pipeline_layout: vk.PipelineLayout,
58 pipelines: [1]vk.Pipeline,
59
60 var offset: c_int = 0;
61 };
62
63 pub const getGObjectType = gobject.ext.defineClass(Window, .{
64 .name = "GtkVkTestWindow",
65 .instanceInit = &init,
66 // .classInit = &Class.init,
67 .parent_class = &Class.parent,
68 .private = .{ .Type = Private, .offset = &Private.offset },
69 });
70
71 pub fn new(app: *Application) *Window {
72 return gobject.ext.newInstance(Window, .{ .application = app });
73 }
74
75 fn init(self: *Window, _: *Class) callconv(.c) void {
76 const area = VkArea.new();
77
78 // Enable validation layers if we're using debug mode
79 if (comptime builtin.mode == .Debug) {
80 area.addInstanceExtensions(&.{vk.extensions.ext_debug_utils.name}) catch {};
81 area.addValidationLayers(&.{"VK_LAYER_KHRONOS_validation"}) catch {};
82 }
83
84 _ = VkArea.signals.setup.connect(area, *Window, onSetup, self, .{});
85 _ = VkArea.signals.teardown.connect(area, *Window, onTeardown, self, .{});
86 _ = VkArea.signals.render.connect(area, *Window, onRender, self, .{});
87 self.as(gtk.Window).setChild(area.as(gtk.Widget));
88 }
89
90 fn onRender(area: *VkArea, ctx: *const VkArea.RenderContext, self: *Window) callconv(.c) void {
91 _ = area;
92 self.draw(ctx) catch |err| {
93 std.log.err("failed to draw={}", .{err});
94 };
95 }
96 fn onSetup(area: *VkArea, ctx: *const VkArea.RenderContext, self: *Window) callconv(.c) void {
97 _ = area;
98 self.setup(ctx) catch |err| {
99 std.log.err("failed to setup={}", .{err});
100 };
101 }
102 fn onTeardown(area: *VkArea, ctx: *const VkArea.RenderContext, self: *Window) callconv(.c) void {
103 _ = area;
104 self.teardown(ctx) catch |err| {
105 std.log.err("failed to teardown={}", .{err});
106 };
107 }
108
109 fn setup(self: *Window, ctx: *const VkArea.RenderContext) !void {
110 const priv = self.private();
111
112 // Setup shaders
113 priv.shaders = try ctx.device.createShaderModule(&.{
114 .code_size = triangle_spv.len,
115 .p_code = @ptrCast(@alignCast(triangle_spv)),
116 }, null);
117 errdefer ctx.device.destroyShaderModule(
118 priv.shaders,
119 null,
120 );
121
122 const shader_stages = [_]vk.PipelineShaderStageCreateInfo{
123 .{
124 .module = priv.shaders,
125 .stage = .{ .vertex_bit = true },
126 .p_name = "vertMain",
127 },
128 .{
129 .module = priv.shaders,
130 .stage = .{ .fragment_bit = true },
131 .p_name = "fragMain",
132 },
133 };
134
135 // Setup pipeline layout
136 priv.pipeline_layout = try ctx.device.createPipelineLayout(
137 &.{},
138 null,
139 );
140 errdefer ctx.device.destroyPipelineLayout(
141 priv.pipeline_layout,
142 null,
143 );
144
145 // Setup pipeline
146 const pipeline_rendering_info: vk.PipelineRenderingCreateInfo = .{
147 .color_attachment_count = 1,
148 .p_color_attachment_formats = &.{.b8g8r8a8_srgb},
149 .view_mask = 0,
150 .depth_attachment_format = .undefined,
151 .stencil_attachment_format = .undefined,
152 };
153 const dynamic_states = [_]vk.DynamicState{
154 .viewport,
155 .scissor,
156 };
157 const attachment_states = [_]vk.PipelineColorBlendAttachmentState{.{
158 .blend_enable = .true,
159
160 .src_color_blend_factor = .src_alpha,
161 .dst_color_blend_factor = .one_minus_src_alpha,
162 .color_blend_op = .add,
163 .src_alpha_blend_factor = .one,
164 .dst_alpha_blend_factor = .zero,
165 .alpha_blend_op = .add,
166 .color_write_mask = .{
167 .r_bit = true,
168 .g_bit = true,
169 .b_bit = true,
170 .a_bit = true,
171 },
172 }};
173
174 const pipeline_info: vk.GraphicsPipelineCreateInfo = .{
175 .layout = priv.pipeline_layout,
176 .stage_count = shader_stages.len,
177 .p_stages = &shader_stages,
178 .p_dynamic_state = &.{
179 .dynamic_state_count = dynamic_states.len,
180 .p_dynamic_states = &dynamic_states,
181 },
182 .p_color_blend_state = &.{
183 .logic_op_enable = .false,
184 .logic_op = .copy,
185 .blend_constants = @splat(0),
186 .attachment_count = attachment_states.len,
187 .p_attachments = &attachment_states,
188 },
189 .p_input_assembly_state = &.{
190 .topology = .triangle_list,
191 .primitive_restart_enable = .false,
192 },
193 .p_multisample_state = &.{
194 .rasterization_samples = .{
195 .@"1_bit" = true,
196 },
197 .sample_shading_enable = .false,
198 .min_sample_shading = 0,
199 .alpha_to_coverage_enable = .false,
200 .alpha_to_one_enable = .false,
201 },
202 .p_rasterization_state = &.{
203 .depth_clamp_enable = .false,
204 .rasterizer_discard_enable = .false,
205 .polygon_mode = .fill,
206 .cull_mode = .{ .back_bit = true },
207 .front_face = .clockwise,
208 .depth_bias_enable = .false,
209 .depth_bias_constant_factor = 0,
210 .depth_bias_clamp = 0,
211 .depth_bias_slope_factor = 0,
212 .line_width = 1,
213 },
214 .p_viewport_state = &.{
215 .viewport_count = 1,
216 .scissor_count = 1,
217 },
218 .p_vertex_input_state = &.{},
219 .subpass = 0,
220 .render_pass = .null_handle,
221 .base_pipeline_index = -1,
222 .p_next = @ptrCast(&pipeline_rendering_info),
223 };
224
225 switch (try ctx.device.createGraphicsPipelines(
226 .null_handle,
227 priv.pipelines.len,
228 &.{pipeline_info},
229 null,
230 &priv.pipelines,
231 )) {
232 .success => {},
233 else => return error.FailedToGraphicsPipeline,
234 }
235 errdefer for (priv.pipelines) |p| ctx.device.destroyPipeline(
236 p,
237 null,
238 );
239 }
240
241 fn teardown(self: *Window, ctx: *const VkArea.RenderContext) !void {
242 const priv = self.private();
243
244 for (priv.pipelines) |p| ctx.device.destroyPipeline(
245 p,
246 null,
247 );
248 ctx.device.destroyPipelineLayout(
249 priv.pipeline_layout,
250 null,
251 );
252 ctx.device.destroyShaderModule(
253 priv.shaders,
254 null,
255 );
256 }
257
258 fn draw(self: *Window, ctx: *const VkArea.RenderContext) !void {
259 const priv = self.private();
260
261 var color = [4]f32{ 0.824, 0.216, 0.451, 1.0 };
262 for (&color) |*ch| ch.* = srgb2Linear(ch.*);
263
264 const color_attachments = [_]vk.RenderingAttachmentInfo{.{
265 .image_view = ctx.target.view,
266 .image_layout = .color_attachment_optimal,
267 .resolve_mode = .{},
268 .resolve_image_layout = .color_attachment_optimal,
269 .load_op = .clear,
270 .store_op = .store,
271 .clear_value = .{
272 .color = .{ .float_32 = color },
273 },
274 }};
275
276 ctx.device.cmdBeginRendering(ctx.cmds, &.{
277 .render_area = .{
278 .offset = .{ .x = 0, .y = 0 },
279 .extent = ctx.extent,
280 },
281 .layer_count = 1,
282 .view_mask = 0,
283 .color_attachment_count = @intCast(color_attachments.len),
284 .p_color_attachments = &color_attachments,
285 });
286 defer ctx.device.cmdEndRendering(ctx.cmds);
287
288 ctx.device.cmdBindPipeline(
289 ctx.cmds,
290 .graphics,
291 priv.pipelines[0],
292 );
293 ctx.device.cmdSetViewport(
294 ctx.cmds,
295 0,
296 1,
297 &.{.{
298 .x = 0,
299 .y = 0,
300 .width = @floatFromInt(ctx.extent.width / 2),
301 .height = @floatFromInt(ctx.extent.height / 2),
302 .min_depth = 0,
303 .max_depth = 1,
304 }},
305 );
306 ctx.device.cmdSetScissor(
307 ctx.cmds,
308 0,
309 1,
310 &.{.{
311 .offset = .{ .x = 0, .y = 0 },
312 .extent = ctx.extent,
313 }},
314 );
315 ctx.device.cmdDraw(
316 ctx.cmds,
317 3,
318 1,
319 0,
320 0,
321 );
322 }
323
324 pub fn as(self: *Window, comptime T: type) *T {
325 return gobject.ext.as(T, self);
326 }
327
328 fn private(self: *Window) *Private {
329 return gobject.ext.impl_helpers.getPrivate(self, Private, Private.offset);
330 }
331 pub const Class = extern struct {
332 parent_class: Parent.Class,
333 var parent: *Parent.Class = undefined;
334 pub const Instance = Window;
335 };
336};
337
338pub fn main() void {
339 const app = Application.new();
340 const status = app.as(gio.Application).run(
341 @intCast(std.os.argv.len),
342 std.os.argv.ptr,
343 );
344 std.process.exit(@intCast(status));
345}
346
347fn srgb2Linear(v: f32) f32 {
348 return if (v <= 0.04045) v / 12.92 else std.math.pow(f32, (v + 0.055) / 1.055, 2.4);
349}