diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 472e61c..aa631c1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -36,6 +36,18 @@ jobs: run: zig build examples - name: Build Examples Windows run: zig build examples -Dtarget=x86_64-windows + gpu_examples: + name: Build GPU Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + - name: Build GPU Examples Linux + run: cd gpu_examples && zig build examples + - name: Build GPU Examples Windows + run: cd gpu_examples && zig build examples -Dtarget=x86_64-windows template: name: Build Template runs-on: ubuntu-latest diff --git a/build.zig b/build.zig index 573b250..d60d0a9 100644 --- a/build.zig +++ b/build.zig @@ -119,6 +119,8 @@ pub fn build(b: *std.Build) !void { extension_options.addOption(bool, "image", ext_image); const ext_net = b.option(bool, "ext_net", "Enable SDL_net extension") orelse false; extension_options.addOption(bool, "net", ext_net); + const ext_shadercross = b.option(bool, "ext_shadercross", "Enable SDL_shadercross extension") orelse false; + extension_options.addOption(bool, "shadercross", ext_shadercross); const ext_ttf = b.option(bool, "ext_ttf", "Enable SDL_ttf extension") orelse false; extension_options.addOption(bool, "ttf", ext_ttf); @@ -131,10 +133,12 @@ pub fn build(b: *std.Build) !void { \\{s} \\{s} \\{s} + \\{s} , .{ if (!sdl3_main) "#define SDL_MAIN_NOIMPL\n" else "", if (ext_image) "#include \n" else "", if (ext_net) "#include \n" else "", + if (ext_shadercross) "#include \n" else "", if (ext_ttf) "#include \n#include \n" else "", }); @@ -192,6 +196,9 @@ pub fn build(b: *std.Build) !void { .target = target, }, sdl_system_include_path); } + if (ext_shadercross) { + setupSdlShadercross(b, sdl3, translate_c, sdl_dep_lib, c_sdl_preferred_linkage, cfg); + } _ = setupDocs(b, sdl3); _ = setupTest(b, cfg, extension_options, options, c_module); @@ -200,6 +207,56 @@ pub fn build(b: *std.Build) !void { _ = try runExample(b, sdl3, cfg, example_options); } +// Most of this is copied from https://github.com/Beyley/SDL_shadercross_zig/blob/master/build.zig. +pub fn setupSdlShadercross( + b: *std.Build, + sdl3: *std.Build.Module, + translate_c: *std.Build.Step.TranslateC, + sdl_dep_lib: *std.Build.Step.Compile, + linkage: std.builtin.LinkMode, + cfg: Config, +) void { + const upstream = b.lazyDependency("sdl_shadercross", .{}) orelse return; + + const target = cfg.target; + const lib = b.addLibrary(.{ + .name = "SDL3_shadercross", + .version = .{ .major = 3, .minor = 0, .patch = 0 }, + .linkage = linkage, + .root_module = b.createModule(.{ + .target = target, + .optimize = cfg.optimize, + .link_libc = true, + }), + }); + lib.root_module.linkLibrary(sdl_dep_lib); + + translate_c.addIncludePath(upstream.path("include")); + lib.root_module.addIncludePath(upstream.path("include")); + lib.root_module.addIncludePath(upstream.path("src")); + + lib.root_module.addCSourceFiles(.{ + .root = upstream.path("src"), + .files = &.{ + "SDL_shadercross.c", + }, + }); + + lib.installHeadersDirectory(upstream.path("include"), "", .{}); + + const spirv_headers = b.dependency("spirv_headers", .{}); + const spirv_cross = b.dependency("spirv_cross", .{ + .target = target, + .optimize = .ReleaseFast, // There is a C bug in spirv-cross upstream! Ignore undefined behavior for now. + .spv_cross_reflect = true, + .spv_cross_cpp = false, + }); + lib.linkLibrary(spirv_cross.artifact("spirv-cross-c")); + lib.addIncludePath(spirv_headers.path("include/spirv/1.2/")); + + sdl3.linkLibrary(lib); +} + pub fn setupDocs(b: *std.Build, sdl3: *std.Build.Module) *std.Build.Step { const sdl3_lib = b.addLibrary(.{ .root_module = sdl3, diff --git a/build.zig.zon b/build.zig.zon index 427026b..c7d7db1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,6 +3,14 @@ .version = "0.1.5", .minimum_zig_version = "0.15.1", .dependencies = .{ + .freetype = .{ + .url = "git+https://github.com/allyourcodebase/freetype.git#9d847affbd224cf6a7303ad6d98715e3675ef78c", + .hash = "freetype-2.13.3-C3-WdeyJAAD9z8BeYYQ8N3gNlPI9NvHRxg5039S5vhz7", + }, + .harfbuzz = .{ + .url = "git+https://github.com/allyourcodebase/harfbuzz#6bb522d22cee0ce1d1446bd59fa5b0417f25303e", + .hash = "harfbuzz-8.5.0-Ip1VAqCZAADRh-K2AVujG0N3VAal-AKwkZrvlYOaloyB", + }, .sdl = .{ .url = "git+https://github.com/castholm/SDL.git?ref=v0.3.1%2B3.2.24#1d4b8dcc03bd216712b55318b4ad2402b5511490", .hash = "sdl-0.3.1+3.2.24-7uIn9L9AfwFQf90JcTh3Slw__ZA8SEomk6fQI9F8ItY0", @@ -17,18 +25,22 @@ .hash = "N-V-__8AAK74BwCyuisw6wdXWDDg8sXuHJd_tPk1VKxLTUBK", .lazy = true, }, + .sdl_shadercross = .{ + .url = "git+https://github.com/libsdl-org/SDL_shadercross#392d12afc1ef084c5cd656307180027399b7a54e", + .hash = "N-V-__8AAH9GBgCySY9rt5VJj3A-OGl0Z05BPZ9PgS5Pt1zm", + }, .sdl_ttf = .{ .url = "git+https://github.com/libsdl-org/SDL_ttf#release-3.2.2", .hash = "N-V-__8AAF2JgAA_Y5Qzm3b226N8y7ivM-qFVpCo9NkbfO8Y", .lazy = true, }, - .harfbuzz = .{ - .url = "git+https://github.com/allyourcodebase/harfbuzz#6bb522d22cee0ce1d1446bd59fa5b0417f25303e", - .hash = "harfbuzz-8.5.0-Ip1VAqCZAADRh-K2AVujG0N3VAal-AKwkZrvlYOaloyB", + .spirv_cross = .{ + .url = "git+https://github.com/Gota7/spirv_cross-zig#75cfd40f81f63a678d70d7e3de021edd944eb553", + .hash = "spirv_cross-0.0.0-bzNVZC4YAADQdsxyfpxC9Iw8xxietH7xoEQxbHY43bmo", }, - .freetype = .{ - .url = "git+https://github.com/allyourcodebase/freetype.git#9d847affbd224cf6a7303ad6d98715e3675ef78c", - .hash = "freetype-2.13.3-C3-WdeyJAAD9z8BeYYQ8N3gNlPI9NvHRxg5039S5vhz7", + .spirv_headers = .{ + .url = "git+https://github.com/KhronosGroup/SPIRV-Headers#1bfd27101e4578d0284061bdf8f09fb4755c7c2d", + .hash = "N-V-__8AADq1MQCCoUh-qOkYXGvqKFClPzdiedYrsGFYzTV9", }, }, .paths = .{ diff --git a/gpu_examples/README.md b/gpu_examples/README.md new file mode 100644 index 0000000..412c5a1 --- /dev/null +++ b/gpu_examples/README.md @@ -0,0 +1,40 @@ +# SDL3 GPU Examples +Zig examples of using SDL3's GPU subsystem along with shadercross to run all provided shader formats on the target. SDL shadercross bridges the platform shader format gap such that the shaders "just work" everywhere. + +## Build System +You can use `zig build examples` to build all of the example executables, and `zig build run -Dexample=example-zig-file-here` (Ex: `-Dexample=basic-triangle`) to run a particular example. You may also specify the shader format to use with `-Dshader_format`. Note that the default format is `zig`. You are free to use the build system as a reference for building your own project's, taking the shader format you like most. + +Zig shaders use `spirv-opt` on the system to optimize the result of SPIR-V as zig does not optimize SPIR-V iirc? Also, you may not compile zig shaders on a Windows system at the moment [due to this issue](https://github.com/ziglang/zig/issues/23883). There is nothing stopping you from cross-compiling for a Windows system, but doing dev from a Windows environment would be unfun. + +GLSL shaders use `glslang` on the system in order to compile GLSL into SPIR-V binaries. + +HLSL shaders are compiled at runtime using SDL shadercross. Note that nothing is stopping you from pre-compiling HLSL into SPIR-V using `glslang` if runtime flexibility is not needed. + +## Shader Formats +Each shader format has its own ups and downs. + +### Zig +Pros: +* It's zig +* Tooling already installed with the compiler + +Cons: +* Inline assembly required for more complex tasks +* External tool required at compile time to maximize performance +* No runtime recompilation support + +### Compiled GLSL/HLSL +Pros: +* Normal shading language + +Cons: +* Requires `glslang` or other shader compiler at build time +* No runtime recompilation support + +### Runtime HLSL +Pros: +* Runtime recompilation support +* Normal shading language + +Cons: +* Requires additional less-flexible dependencies for SDL shadercross diff --git a/gpu_examples/build.zig b/gpu_examples/build.zig new file mode 100644 index 0000000..6778feb --- /dev/null +++ b/gpu_examples/build.zig @@ -0,0 +1,236 @@ +const std = @import("std"); + +const depth_overrides = [_][]const u8{ + "solidColorDepth.frag", +}; + +const compute_sizes = std.StaticStringMap([3][]const u8).initComptime(.{ + .{ "fillTexture.comp", .{ "8", "8", "1" } }, + .{ "gradientTexture.comp", .{ "8", "8", "1" } }, + .{ "spriteBatch.comp", .{ "64", "1", "1" } }, + .{ "texturedQuad.comp", .{ "8", "8", "1" } }, +}); + +const ShaderFormat = enum { + glsl, + hlsl, + zig, + hlsl_runtime, +}; + +fn setupShader( + b: *std.Build, + module: *std.Build.Module, + name: []const u8, + format: ShaderFormat, +) !void { + // Compute shaders not possible for zig atm. See `shaders/basic-compute.zig` for more info. + const suffix = name[std.mem.lastIndexOf(u8, name, ".").? + 1 ..]; + const b_dis_opt: ?[]const u8 = b.findProgram(&.{"spirv-dis"}, &.{}) catch null; + const b_as_opt: ?[]const u8 = b.findProgram(&.{"spirv-as"}, &.{}) catch null; + const supports_zig_comp = b_dis_opt != null and b_as_opt != null; + const want_zig_compute = format == .zig and std.mem.eql(u8, suffix, "comp"); + if (want_zig_compute and !supports_zig_comp) + std.debug.print("spirv-dis and spirv-as not found, zig compute kernel support disabled, using HLSL instead for compute kernels", .{}); + + const actual_format = if (!supports_zig_comp and want_zig_compute) .hlsl else format; + switch (actual_format) { + .glsl, .hlsl => { + const glslang = b.findProgram(&.{"glslang"}, &.{}) catch @panic("glslang not found, can not compile GLSL shaders"); + const glslang_cmd = b.addSystemCommand(&.{ glslang, "-V100", "-e", "main", "-S" }); + glslang_cmd.addArg(suffix); + if (actual_format == .hlsl) + glslang_cmd.addArg("-D"); + glslang_cmd.addFileArg(b.path(b.fmt("shaders/{s}.{s}", .{ name, if (actual_format == .glsl) "glsl" else "hlsl" }))); + glslang_cmd.addArg("-o"); + const glslang_cmd_out = glslang_cmd.addOutputFileArg(b.fmt("{s}.spv", .{name})); + + module.addAnonymousImport(name, .{ .root_source_file = glslang_cmd_out }); + }, + .hlsl_runtime => module.addAnonymousImport(name, .{ .root_source_file = b.path(b.fmt("shaders/{s}.hlsl", .{name})) }), + .zig => { + const obj = b.addObject(.{ + .name = name, + .root_module = b.addModule(name, .{ + .root_source_file = b.path(b.fmt("shaders/{s}.zig", .{name})), + .target = b.resolveTargetQuery(.{ + .cpu_arch = .spirv64, + .cpu_model = .{ .explicit = &std.Target.spirv.cpu.vulkan_v1_2 }, + .cpu_features_add = std.Target.spirv.featureSet(&.{}), + .os_tag = .vulkan, + .ofmt = .spirv, + }), + }), + .use_llvm = false, + .use_lld = false, + }); + var shader_out = obj.getEmittedBin(); + + if (b.findProgram(&.{"spirv-opt"}, &.{})) |spirv_opt| { + + // Remove duplicate type definitions that might be done by inline assembly. + const spirv_fix = b.addSystemCommand(&.{ spirv_opt, "--remove-duplicates", "--skip-validation" }); + spirv_fix.addFileArg(obj.getEmittedBin()); + spirv_fix.addArg("-o"); + var fixed_spirv = spirv_fix.addOutputFileArg(b.fmt("{s}-fixed.spv", .{name})); + + // Handle depth overrides. + var depth_override = false; + for (depth_overrides) |val| { + if (std.mem.eql(u8, val, name)) { + depth_override = true; + break; + } + } + if (supports_zig_comp or depth_override) { + + // Disassemble into SPIRV assembly. + const spirv_dis = try b.findProgram(&.{"spirv-dis"}, &.{}); + const spirv_dis_cmd = b.addSystemCommand(&.{spirv_dis}); + spirv_dis_cmd.addFileArg(fixed_spirv); + spirv_dis_cmd.addArg("-o"); + const spirv_dis_out = spirv_dis_cmd.addOutputFileArg(b.fmt("{s}.spvasm", .{name})); + + // Modify the execution mode using a custom build tool. + const spirv_execution_mode = b.addExecutable(.{ + .name = "spirv-execution-mode", + .root_module = b.createModule(.{ + .root_source_file = b.path("build_tools/spirv_execution_mode.zig"), + .target = b.graph.host, + }), + }); + const spirv_execution_mode_cmd = b.addRunArtifact(spirv_execution_mode); + spirv_execution_mode_cmd.addFileArg(spirv_dis_out); + const execution_mode_changed_spirv = spirv_execution_mode_cmd.addOutputFileArg(b.fmt("{s}-execution-mode-fixed.spvasm", .{name})); + if (depth_override) + spirv_execution_mode_cmd.addArg("DepthReplacing"); + if (want_zig_compute) { + spirv_execution_mode_cmd.addArg("LocalSize"); + spirv_execution_mode_cmd.addArgs(&compute_sizes.get(name).?); + } + + // Reassemble updated assembly. + const spirv_as = try b.findProgram(&.{"spirv-as"}, &.{}); + const spirv_as_cmd = b.addSystemCommand(&.{spirv_as}); + spirv_as_cmd.addFileArg(execution_mode_changed_spirv); + spirv_as_cmd.addArg("-o"); + fixed_spirv = spirv_as_cmd.addOutputFileArg(b.fmt("{s}-execution-mode-fixed.spv", .{name})); + } + + // Optimize the SPIRV. + const spirv_opt_cmd = b.addSystemCommand(&.{ spirv_opt, "-O" }); + spirv_opt_cmd.addFileArg(fixed_spirv); + spirv_opt_cmd.addArg("-o"); + shader_out = spirv_opt_cmd.addOutputFileArg(b.fmt("{s}-opt.spv", .{name})); + } else |err| switch (err) { + error.FileNotFound => std.debug.print("spirv-opt not found, shader output will be unoptimized!\n", .{}), + } + + module.addAnonymousImport(name, .{ .root_source_file = shader_out }); + }, + } +} + +fn buildExample( + b: *std.Build, + sdl3: *std.Build.Module, + format: ShaderFormat, + options: *std.Build.Step.Options, + name: []const u8, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) !*std.Build.Step.Compile { + const exe_mod = b.createModule(.{ + .root_source_file = b.path(b.fmt("src/{s}.zig", .{name})), + .target = target, + .optimize = optimize, + }); + const exe = b.addExecutable(.{ + .name = name, + .root_module = exe_mod, + }); + + var dir = (try std.fs.openDirAbsolute(b.path("shaders").getPath(b), .{ .iterate = true })); + defer dir.close(); + var dir_iterator = try dir.walk(b.allocator); + defer dir_iterator.deinit(); + while (try dir_iterator.next()) |file| { + if (file.kind == .file) { + const extension = switch (format) { + .glsl => ".glsl", + .hlsl => ".hlsl", + .zig => ".zig", + .hlsl_runtime => ".hlsl", + }; + if (!std.mem.endsWith(u8, file.basename, extension)) + continue; + if (std.mem.eql(u8, file.basename, "common.zig")) // Special exception. + continue; + try setupShader(b, exe.root_module, file.basename[0..(file.basename.len - extension.len)], format); + } + } + + exe.root_module.addImport("sdl3", sdl3); + exe.root_module.addOptions("options", options); + b.installArtifact(exe); + return exe; +} + +pub fn runExample( + b: *std.Build, + sdl3: *std.Build.Module, + format: ShaderFormat, + options: *std.Build.Step.Options, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) !void { + const run_example: ?[]const u8 = b.option([]const u8, "example", "The example name for running an example") orelse null; + const run = b.step("run", "Run an example with -Dexample= option"); + if (run_example) |example| { + const run_art = b.addRunArtifact(try buildExample(b, sdl3, format, options, example, target, optimize)); + run_art.step.dependOn(b.getInstallStep()); + run.dependOn(&run_art.step); + } +} + +pub fn setupExamples( + b: *std.Build, + sdl3: *std.Build.Module, + format: ShaderFormat, + options: *std.Build.Step.Options, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) !void { + const exp = b.step("examples", "Build all examples"); + const examples_dir = b.path("src"); + var dir = (try std.fs.openDirAbsolute(examples_dir.getPath(b), .{ .iterate = true })); + defer dir.close(); + var dir_iterator = try dir.walk(b.allocator); + defer dir_iterator.deinit(); + while (try dir_iterator.next()) |file| { + if (file.kind == .file and std.mem.endsWith(u8, file.basename, ".zig")) { + _ = try buildExample(b, sdl3, format, options, file.basename[0 .. file.basename.len - 4], target, optimize); + } + } + exp.dependOn(b.getInstallStep()); +} + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const sdl3 = b.dependency("sdl3", .{ + .target = target, + .optimize = optimize, + .callbacks = true, + .ext_shadercross = true, + }); + const sdl3_mod = sdl3.module("sdl3"); + const options = b.addOptions(); + + const format = b.option(ShaderFormat, "shader_format", "Shader format to use") orelse .zig; + options.addOption(bool, "spirv", format != .hlsl_runtime); + options.addOption(bool, "gpu_debug", b.option(bool, "gpu_debug", "Enable GPU debugging functionality") orelse false); + try setupExamples(b, sdl3_mod, format, options, target, optimize); + try runExample(b, sdl3_mod, format, options, target, optimize); +} diff --git a/gpu_examples/build.zig.zon b/gpu_examples/build.zig.zon new file mode 100644 index 0000000..2de806d --- /dev/null +++ b/gpu_examples/build.zig.zon @@ -0,0 +1,16 @@ +.{ + .name = .gpu_examples, + .version = "0.0.1", + .minimum_zig_version = "0.15.1", + .dependencies = .{ + .sdl3 = .{ + .path = "../", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, + .fingerprint = 0x7a44a34cd5e71f29, +} diff --git a/gpu_examples/build_tools/spirv_execution_mode.zig b/gpu_examples/build_tools/spirv_execution_mode.zig new file mode 100644 index 0000000..d5dd5cf --- /dev/null +++ b/gpu_examples/build_tools/spirv_execution_mode.zig @@ -0,0 +1,95 @@ +const std = @import("std"); + +const buf_size = 1024; + +const Error = error{ + BadArgCount, + UnrecognizedArg, +}; + +const DepthReplacing = void; +const LocalSize = struct { + x: u32, + y: u32, + z: u32, +}; + +fn usage(err: Error) !void { + var buf1: [buf_size]u8 = undefined; + var buf2: [buf_size]u8 = undefined; + var err_writer = std.fs.File.stderr().writer(&buf1).interface; + var out_writer = std.fs.File.stdout().writer(&buf2).interface; + switch (err) { + error.BadArgCount => try err_writer.writeAll("Invalid arguments specified"), + error.UnrecognizedArg => try err_writer.writeAll("Unrecognized argument"), + } + try out_writer.writeAll("Usage: spirv-execution-mode [DepthReplacing] [LocalSize x y z]"); + try out_writer.flush(); + return err; +} + +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.smp_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Gather parameters. + var depth_replacing: ?DepthReplacing = null; + var local_size: ?LocalSize = null; + var args = try std.process.argsWithAllocator(allocator); + _ = args.next(); + const input_file = args.next() orelse return usage(error.BadArgCount); + const output_file = args.next() orelse return usage(error.BadArgCount); + while (args.next()) |execution_mode| { + if (std.mem.eql(u8, execution_mode, "DepthReplacing")) { + depth_replacing = {}; + } else if (std.mem.eql(u8, execution_mode, "LocalSize")) { + const x = args.next() orelse return usage(error.BadArgCount); + const y = args.next() orelse return usage(error.BadArgCount); + const z = args.next() orelse return usage(error.BadArgCount); + local_size = .{ + .x = try std.fmt.parseInt(u32, x, 10), + .y = try std.fmt.parseInt(u32, y, 10), + .z = try std.fmt.parseInt(u32, z, 10), + }; + } else return usage(error.UnrecognizedArg); + } + + // Output modified source. + var read_buf: [buf_size]u8 = undefined; + var write_buf: [buf_size]u8 = undefined; + var in_file = try std.fs.cwd().openFile(input_file, .{}); + defer in_file.close(); + var out_file = try std.fs.cwd().createFile(output_file, .{}); + defer out_file.close(); + var f_reader = in_file.reader(&read_buf); + const reader = &f_reader.interface; + var f_writer = out_file.writer(&write_buf); + const writer = &f_writer.interface; + while (true) { + const line = reader.takeDelimiterInclusive('\n') catch |err| { + switch (err) { + error.EndOfStream => break, + else => return err, + } + }; + try writer.writeAll(line); + var toks = std.mem.tokenizeScalar(u8, line, ' '); + const op = toks.next() orelse continue; + if (!std.mem.eql(u8, op, "OpEntryPoint")) + continue; + _ = toks.next(); + const entry_point_var = toks.next() orelse continue; + if (depth_replacing != null) { + try writer.writeAll(" OpExecutionMode "); + try writer.writeAll(entry_point_var); + try writer.writeAll(" DepthReplacing\n"); + } + if (local_size) |sz| { + try writer.writeAll(" OpExecutionMode "); + try writer.writeAll(entry_point_var); + try writer.print(" LocalSize {d} {d} {d}\n", .{ sz.x, sz.y, sz.z }); + } + } + try writer.flush(); +} diff --git a/gpu_examples/shaders/common.zig b/gpu_examples/shaders/common.zig new file mode 100644 index 0000000..1b770c7 --- /dev/null +++ b/gpu_examples/shaders/common.zig @@ -0,0 +1,368 @@ +/// SPIR-V on zig currently can not use cosine, use some inline assembly to steal from GLSL's extended instruction set. +/// +/// ## Function Parameters +/// * `val`: The scalar or vector to get the cosine of. +/// +/// ## Return Value +/// Returns a scalar or vector with the cosines of each element. +pub fn cos(val: anytype) @TypeOf(val) { + // https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html + return asm volatile ( + \\%glsl_ext = OpExtInstImport "GLSL.std.450" + \\%ret = OpExtInst %val_type %glsl_ext 14 %val + : [ret] "" (-> @TypeOf(val)), + : [val] "" (val), + [val_type] "t" (@TypeOf(val)), + ); +} + +/// SPIR-V on zig currently can not use sine, use some inline assembly to steal from GLSL's extended instruction set. +/// +/// ## Function Parameters +/// * `val`: The scalar or vector to get the sine of. +/// +/// ## Return Value +/// Returns a scalar or vector with the sines of each element. +pub fn sin(val: anytype) @TypeOf(val) { + // https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html + return asm volatile ( + \\%glsl_ext = OpExtInstImport "GLSL.std.450" + \\%ret = OpExtInst %val_type %glsl_ext 13 %val + : [ret] "" (-> @TypeOf(val)), + : [val] "" (val), + [val_type] "t" (@TypeOf(val)), + ); +} + +/// Create a runtime array for a type. +/// +/// ## Function Parameters +/// * `set`: The binding set of the array. +/// * `bind`: The binding slot of the array. +/// * `Type`: Type of element stored in the array. +/// +/// ## Return Value +/// Returns the runtime array. +pub fn RuntimeArray( + comptime set: u32, + comptime bind: u32, + comptime Type: type, +) type { + return struct { + /// Read from a runtime array storage buffer. + /// + /// ## Function Parameters + /// * `index`: Index to access the element in the runtime array. + /// + /// ## Return Value + /// Returns the value at the given index in the array. + pub fn read( + index: u32, + ) Type { + return asm volatile ( + \\%int = OpTypeInt 32 1 + \\%zero = OpConstant %int 0 + \\%uniform_ptr_type = OpTypePointer StorageBuffer %entry_type + \\%arr = OpTypeRuntimeArray %entry_type + \\%compute_buffer = OpTypeStruct %arr + \\%uniform_type = OpTypePointer StorageBuffer %compute_buffer + \\%uniform = OpVariable %uniform_type StorageBuffer + \\ OpDecorate %uniform DescriptorSet $set + \\ OpDecorate %uniform Binding $bind + \\%access = OpAccessChain %uniform_ptr_type %uniform %zero %index + \\%ret = OpLoad %entry_type %access + : [ret] "" (-> Type), + : [entry_type] "t" (Type), + [index] "" (index), + [set] "c" (set), + [bind] "c" (bind), + ); + } + + /// Write to a runtime array storage buffer. + /// + /// ## Function Parameters + /// * `index`: Index to access the element in the runtime array. + /// * `val`: Value to write to the given index in the runtime array. + pub fn write( + index: u32, + val: Type, + ) void { + asm volatile ( + \\%int = OpTypeInt 32 1 + \\%zero = OpConstant %int 0 + \\%uniform_ptr_type = OpTypePointer StorageBuffer %entry_type + \\%arr = OpTypeRuntimeArray %entry_type + \\%compute_buffer = OpTypeStruct %arr + \\%uniform_type = OpTypePointer StorageBuffer %compute_buffer + \\%uniform = OpVariable %uniform_type StorageBuffer + \\ OpDecorate %uniform DescriptorSet $set + \\ OpDecorate %uniform Binding $bind + \\%access = OpAccessChain %uniform_ptr_type %uniform %zero %index + \\ OpStore %access %val + : + : [entry_type] "t" (Type), + [index] "" (index), + [val] "" (val), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a 2d sampler. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d sampler object. +pub fn Sampler2d( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Get the texture size of a 2d sampler. + /// + /// ## Function Parameters + /// * `lod`: The LOD to sample at. + /// + /// ## Return Value + /// Returns the sampler texture size. + pub fn size( + lod: i32, + ) @Vector(2, i32) { + return asm volatile ( + \\ OpCapability ImageQuery + \\%float = OpTypeFloat 32 + \\%int = OpTypeInt 32 1 + \\%v2int = OpTypeVector %int 2 + \\%img_type = OpTypeImage %float 2D 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%loaded_image = OpImage %img_type %loaded_sampler + \\%ret = OpImageQuerySizeLod %v2int %loaded_image %lod + : [ret] "" (-> @Vector(2, i32)), + : [set] "c" (set), + [bind] "c" (bind), + [lod] "" (lod), + ); + } + + /// Sample the 2d sampler at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn texture( + uv: @Vector(2, f32), + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleImplicitLod %v4float %loaded_sampler %uv + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [set] "c" (set), + [bind] "c" (bind), + ); + } + + /// Sample a 2d sampler at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// * `lod`: The LOD to sample with. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn textureLod( + uv: @Vector(2, f32), + lod: f32, + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleExplicitLod %v4float %loaded_sampler %uv Lod %lod + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [lod] "" (lod), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a 2d sampler array. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d sampler array object. +pub fn Sampler2dArray( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Sample the 2d sampler array at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn texture( + uv: @Vector(3, f32), + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 1 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleImplicitLod %v4float %loaded_sampler %uv + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a cube sampler. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d sampler object. +pub fn SamplerCube( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Sample a cube sampler at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn texture( + uv: @Vector(3, f32), + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float Cube 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleImplicitLod %v4float %loaded_sampler %uv + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a 2d texture in RGBA8 format. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d RGBA8 texture object. +pub fn Texture2dRgba8( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Get the texture size of a 2d RGBA8 texture. + /// + /// ## Return Value + /// Returns the texture size. + pub fn size() @Vector(2, i32) { + return asm volatile ( + \\ OpCapability ImageQuery + \\%float = OpTypeFloat 32 + \\%int = OpTypeInt 32 1 + \\%v2int = OpTypeVector %int 2 + \\%img_type = OpTypeImage %float 2D 0 0 0 2 Rgba8 + \\%img_ptr = OpTypePointer UniformConstant %img_type + \\%img = OpVariable %img_ptr UniformConstant + \\ OpDecorate %img DescriptorSet $set + \\ OpDecorate %img Binding $bind + \\%loaded_image = OpLoad %img_type %img + \\%ret = OpImageQuerySize %v2int %loaded_image + : [ret] "" (-> @Vector(2, i32)), + : [set] "c" (set), + [bind] "c" (bind), + ); + } + + /// Store to a 2d RGBA8 texture. + /// + /// ## Function Parameters + /// * `uv`: The UV to store to. + /// * `pixel`: The pixel data to store. + pub fn store( + uv: @Vector(2, u32), + pixel: @Vector(4, f32), + ) void { + asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 0 0 2 Rgba8 + \\%img_ptr = OpTypePointer UniformConstant %img_type + \\%img = OpVariable %img_ptr UniformConstant + \\ OpDecorate %img DescriptorSet $set + \\ OpDecorate %img Binding $bind + \\%loaded_image = OpLoad %img_type %img + \\ OpImageWrite %loaded_image %uv %pixel + : + : [uv] "" (uv), + [pixel] "" (pixel), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} diff --git a/gpu_examples/shaders/customSampling.frag.glsl b/gpu_examples/shaders/customSampling.frag.glsl new file mode 100644 index 0000000..52c4150 --- /dev/null +++ b/gpu_examples/shaders/customSampling.frag.glsl @@ -0,0 +1,17 @@ +#version 450 + +layout(set = 2, binding = 0, rgba8) uniform image2D tex0; +layout(set = 3, binding = 0, std140) uniform uniforms { + int custom_sampler; +}; + +layout(location = 0) in vec2 tex_coord_in; + +layout(location = 0) out vec4 color_out; + +void main() { + ivec2 size = imageSize(tex0); + ivec2 texel_pos = ivec2(vec2(size) * tex_coord_in); + vec4 texel = imageLoad(tex0, texel_pos); + color_out = texel; +} diff --git a/gpu_examples/shaders/customSampling.frag.hlsl b/gpu_examples/shaders/customSampling.frag.hlsl new file mode 100644 index 0000000..dc37b31 --- /dev/null +++ b/gpu_examples/shaders/customSampling.frag.hlsl @@ -0,0 +1,26 @@ +cbuffer UBO : register(b0, space3) +{ + int mode : packoffset(c0); +}; + +Texture2D Texture : register(t0, space2); + +float4 main(float2 TexCoord : TEXCOORD0) : SV_Target0 +{ + float w, h; + Texture.GetDimensions(w, h); + int2 texelPos = int2(float2(w, h) * TexCoord); + float4 mainTexel = Texture[texelPos]; + if (mode == 0) + { + return mainTexel; + } + else + { + float4 bottomTexel = Texture[texelPos + int2(0, 1)]; + float4 leftTexel = Texture[texelPos + int2(-1, 0)]; + float4 topTexel = Texture[texelPos + int2(0, -1)]; + float4 rightTexel = Texture[texelPos + int2(1, 0)]; + return ((((mainTexel * 0.2f) + (bottomTexel * 0.2f)) + (leftTexel * 0.20000000298023223876953125f)) + (topTexel * 0.20000000298023223876953125f)) + (rightTexel * 0.2f); + } +} diff --git a/gpu_examples/shaders/depthOutline.frag.glsl b/gpu_examples/shaders/depthOutline.frag.glsl new file mode 100644 index 0000000..c539f7b --- /dev/null +++ b/gpu_examples/shaders/depthOutline.frag.glsl @@ -0,0 +1,38 @@ +#version 450 + +layout(set = 2, binding = 0) uniform sampler2D color_tex; +layout(set = 2, binding = 1) uniform sampler2D depth_tex; + +layout(location = 0) in vec2 tex_coord_in; + +layout(location = 0) out vec4 color_out; + +// Get the difference between depth value and adjacent depth pixels. +// This is used to detect "edges" where the depth falls off. +float getDifference(float depth, vec2 tex_coord, float distance) { + vec2 dim = vec2(textureSize(depth_tex, 0)); + return max(texture(depth_tex, tex_coord + vec2(1.0 / dim.x, 0) * distance).r - depth, + max(texture(depth_tex, tex_coord + vec2(-1.0 / dim.x, 0) * distance).r - depth, + max(texture(depth_tex, tex_coord + vec2(0, 1.0 / dim.y) * distance).r - depth, + texture(depth_tex, tex_coord + vec2(0, -1.0 / dim.y) * distance).r - depth))); +} + +void main() { + + // Get color and depth. + vec4 color = texture(color_tex, tex_coord_in); + float depth = texture(depth_tex, tex_coord_in).r; + + // Get the difference between the edges at 1 and 2 pixels away. + float edge1 = step(0.2, getDifference(depth, tex_coord_in, 1)); + float edge2 = step(0.2, getDifference(depth, tex_coord_in, 2)); + + // Turn inner edges black. + vec3 res = mix(color.rgb, vec3(0), edge2); + + // Turn outer edges white. + res = mix(res, vec3(1), edge1); + + // Combine results. + color_out = vec4(res, color.a); +} diff --git a/gpu_examples/shaders/depthOutline.frag.hlsl b/gpu_examples/shaders/depthOutline.frag.hlsl new file mode 100644 index 0000000..7bef5e7 --- /dev/null +++ b/gpu_examples/shaders/depthOutline.frag.hlsl @@ -0,0 +1,39 @@ +Texture2D ColorTexture : register(t0, space2); +SamplerState ColorSampler : register(s0, space2); + +Texture2D DepthTexture : register(t1, space2); +SamplerState DepthSampler : register(s1, space2); + +// Gets the difference between a depth value and adjacent depth pixels +// This is used to detect "edges", where the depth falls off. +float GetDifference(float depth, float2 TexCoord, float distance) +{ + float w, h; + DepthTexture.GetDimensions(w, h); + + return + max(DepthTexture.Sample(DepthSampler, TexCoord + float2(1.0 / w, 0) * distance).r - depth, + max(DepthTexture.Sample(DepthSampler, TexCoord + float2(-1.0 / w, 0) * distance).r - depth, + max(DepthTexture.Sample(DepthSampler, TexCoord + float2(0, 1.0 / h) * distance).r - depth, + DepthTexture.Sample(DepthSampler, TexCoord + float2(0, -1.0 / h) * distance).r - depth))); +} + +float4 main(float2 TexCoord : TEXCOORD0) : SV_Target0 +{ + // get our color & depth value + float4 color = ColorTexture.Sample(ColorSampler, TexCoord); + float depth = DepthTexture.Sample(DepthSampler, TexCoord).r; + + // get the difference between the edges at 1px and 2px away + float edge = step(0.2, GetDifference(depth, TexCoord, 1.0f)); + float edge2 = step(0.2, GetDifference(depth, TexCoord, 2.0f)); + + // turn inner edges black + float3 res = lerp(color.rgb, 0, edge2); + + // turn the outer edges white + res = lerp(res, 1, edge); + + // combine results + return float4(res, color.a); +} diff --git a/gpu_examples/shaders/depthOutline.frag.zig b/gpu_examples/shaders/depthOutline.frag.zig new file mode 100644 index 0000000..051a803 --- /dev/null +++ b/gpu_examples/shaders/depthOutline.frag.zig @@ -0,0 +1,48 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const color_tex = common.Sampler2d(2, 0); +const depth_tex = common.Sampler2d(2, 1); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +inline fn getDifference(depth: f32, tex_coord: @Vector(2, f32), distance: f32) f32 { + const dimi = depth_tex.size(0); + const dim: @Vector(2, f32) = .{ @floatFromInt(dimi[0]), @floatFromInt(dimi[1]) }; + return @max( + depth_tex.texture(tex_coord + @Vector(2, f32){ 1.0 / dim[0], 0 } * @as(@Vector(2, f32), @splat(distance)))[0] - depth, + depth_tex.texture(tex_coord + @Vector(2, f32){ -1.0 / dim[0], 0 } * @as(@Vector(2, f32), @splat(distance)))[0] - depth, + depth_tex.texture(tex_coord + @Vector(2, f32){ 0, 1.0 / dim[1] } * @as(@Vector(2, f32), @splat(distance)))[0] - depth, + depth_tex.texture(tex_coord + @Vector(2, f32){ 0, -1.0 / dim[1] } * @as(@Vector(2, f32), @splat(distance)))[0] - depth, + ); +} + +fn lerpVec(a: @Vector(3, f32), b: @Vector(3, f32), t: f32) @Vector(3, f32) { + const t_vec: @Vector(3, f32) = @splat(t); + return a * (@as(@Vector(3, f32), @splat(1)) - t_vec) + b * t_vec; +} + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + // Get color and depth. + const color = color_tex.texture(tex_coord_in); + const depth = depth_tex.texture(tex_coord_in)[0]; + + // Get the difference between the edges at 1 and 2 pixels away. + const edge1: f32 = if (getDifference(depth, tex_coord_in, 1) < 0.2) 0 else 1; + const edge2: f32 = if (getDifference(depth, tex_coord_in, 2) < 0.2) 0 else 1; + + // Turn inner edges black. + var res = lerpVec(.{ color[0], color[1], color[2] }, @splat(0), edge2); + + // Turn outer edges white. + res = lerpVec(res, @splat(1), edge1); + + // Combine results. + color_out = .{ res[0], res[1], res[2], color[3] }; +} diff --git a/gpu_examples/shaders/fillTexture.comp.glsl b/gpu_examples/shaders/fillTexture.comp.glsl new file mode 100644 index 0000000..cf58cb4 --- /dev/null +++ b/gpu_examples/shaders/fillTexture.comp.glsl @@ -0,0 +1,8 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 1, binding = 0, rgba8) uniform image2D image_out; + +void main() { + imageStore(image_out, ivec2(gl_GlobalInvocationID.xy), vec4(1, 1, 0, 1)); +} diff --git a/gpu_examples/shaders/fillTexture.comp.hlsl b/gpu_examples/shaders/fillTexture.comp.hlsl new file mode 100644 index 0000000..f7bcc2d --- /dev/null +++ b/gpu_examples/shaders/fillTexture.comp.hlsl @@ -0,0 +1,9 @@ +[[vk::image_format("rgba8")]] +RWTexture2D outImage : register(u0, space1); + +[numthreads(8, 8, 1)] +void main(uint3 GlobalInvocationID : SV_DispatchThreadID) +{ + int2 coord = int2(GlobalInvocationID.xy); + outImage[coord] = float4(1.0f, 1.0f, 0.0f, 1.0f); +} diff --git a/gpu_examples/shaders/fillTexture.comp.zig b/gpu_examples/shaders/fillTexture.comp.zig new file mode 100644 index 0000000..57b8699 --- /dev/null +++ b/gpu_examples/shaders/fillTexture.comp.zig @@ -0,0 +1,10 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const image_out = common.Texture2dRgba8(1, 0); + +export fn main() callconv(.spirv_kernel) void { + // std.gpu.executionMode(main, .{ .local_size = .{ .x = 8, .y = 8, .z = 1 } }); // Set in build system by `spriv_execution_mode`. + + image_out.store(.{ std.gpu.global_invocation_id[0], std.gpu.global_invocation_id[1] }, .{ 1, 1, 0, 1 }); +} diff --git a/gpu_examples/shaders/gradientTexture.comp.glsl b/gpu_examples/shaders/gradientTexture.comp.glsl new file mode 100644 index 0000000..f2d7f7f --- /dev/null +++ b/gpu_examples/shaders/gradientTexture.comp.glsl @@ -0,0 +1,15 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 1, binding = 0, rgba8) uniform image2D image_out; +layout(set = 2, binding = 0) uniform uniforms { + float time; +}; + +void main() { + ivec2 image_size = imageSize(image_out); + vec2 uv = gl_GlobalInvocationID.xy / vec2(image_size); + + vec3 color = vec3(0.5) + (cos((vec3(time) + uv.xyx) + vec3(0, 2, 4)) * 0.5); + imageStore(image_out, ivec2(gl_GlobalInvocationID.xy), vec4(color, 1)); +} diff --git a/gpu_examples/shaders/gradientTexture.comp.hlsl b/gpu_examples/shaders/gradientTexture.comp.hlsl new file mode 100644 index 0000000..d5dc279 --- /dev/null +++ b/gpu_examples/shaders/gradientTexture.comp.hlsl @@ -0,0 +1,21 @@ + +[[vk::image_format("rgba8")]] +RWTexture2D OutImage : register(u0, space1); + +cbuffer UBO : register(b0, space2) +{ + float ubo_time : packoffset(c0); +}; + +[numthreads(8, 8, 1)] +void main(uint3 GlobalInvocationID : SV_DispatchThreadID) +{ + float w, h; + OutImage.GetDimensions(w, h); + float2 size = float2(w, h); + float2 coord = GlobalInvocationID.xy; + float2 uv = coord / size; + + float3 col = 0.5f.xxx + (cos((ubo_time.xxx + uv.xyx) + float3(0.0f, 2.0f, 4.0f)) * 0.5f); + OutImage[int2(coord)] = float4(col, 1.0f); +} diff --git a/gpu_examples/shaders/gradientTexture.comp.zig b/gpu_examples/shaders/gradientTexture.comp.zig new file mode 100644 index 0000000..6afd1b8 --- /dev/null +++ b/gpu_examples/shaders/gradientTexture.comp.zig @@ -0,0 +1,26 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const image_out = common.Texture2dRgba8(1, 0); + +extern var uniforms: extern struct { + time: f32, +} addrspace(.uniform); + +export fn main() callconv(.spirv_kernel) void { + // std.gpu.executionMode(main, .{ .local_size = .{ .x = 8, .y = 8, .z = 1 } }); // Set in build system by `spriv_execution_mode`. + + std.gpu.binding(&uniforms, 2, 0); + + const image_sizei = image_out.size(); + const image_size = @Vector(2, f32){ @floatFromInt(image_sizei[0]), @floatFromInt(image_sizei[1]) }; + const uvi = @Vector(2, u32){ std.gpu.global_invocation_id[0], std.gpu.global_invocation_id[1] }; + const uv = @Vector(2, f32){ + @as(f32, @floatFromInt(uvi[0])) / image_size[0], + @as(f32, @floatFromInt(uvi[1])) / image_size[1], + }; + + const half_vec = @as(@Vector(3, f32), @splat(0.5)); + const color = half_vec + (common.cos((@as(@Vector(3, f32), @splat(uniforms.time)) + @Vector(3, f32){ uv[0], uv[1], uv[0] }) + @Vector(3, f32){ 0, 2, 4 }) * half_vec); + image_out.store(uvi, .{ color[0], color[1], color[2], 1 }); +} diff --git a/gpu_examples/shaders/linearToSrgb.comp.glsl b/gpu_examples/shaders/linearToSrgb.comp.glsl new file mode 100644 index 0000000..73c6f05 --- /dev/null +++ b/gpu_examples/shaders/linearToSrgb.comp.glsl @@ -0,0 +1,15 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 0, binding = 0, rgba8) uniform image2D image_in; // Is this correct? +layout(set = 1, binding = 0, rgba8) uniform image2D image_out; + +vec3 linearToSrgb(vec3 color) { + return pow(abs(color), vec3(1 / 2.2)); +} + +void main() { + ivec2 coords = ivec2(gl_GlobalInvocationID.xy); + vec4 in_pixel = imageLoad(image_in, coords); + imageStore(image_out, coords, vec4(linearToSrgb(in_pixel.xyz), 1)); +} diff --git a/gpu_examples/shaders/linearToSrgb.comp.hlsl b/gpu_examples/shaders/linearToSrgb.comp.hlsl new file mode 100644 index 0000000..57d0f2b --- /dev/null +++ b/gpu_examples/shaders/linearToSrgb.comp.hlsl @@ -0,0 +1,18 @@ +Texture2D InImage : register(t0, space0); +[[vk::image_format("rgba8")]] +RWTexture2D OutImage : register(u0, space1); + +float3 LinearToSRGB(float3 color) +{ + return pow(abs(color), float(1.0f/2.2f).xxx); +} + +[numthreads(8, 8, 1)] +void main(uint3 GlobalInvocationID : SV_DispatchThreadID) +{ + int2 coord = int2(GlobalInvocationID.xy); + float4 inPixel = InImage[coord]; + float3 param = inPixel.xyz; + float3 outColor = LinearToSRGB(param); + OutImage[coord] = float4(outColor, 1.0f); +} diff --git a/gpu_examples/shaders/linearToSt2084.comp.glsl.wip b/gpu_examples/shaders/linearToSt2084.comp.glsl.wip new file mode 100644 index 0000000..3245f3d --- /dev/null +++ b/gpu_examples/shaders/linearToSt2084.comp.glsl.wip @@ -0,0 +1,27 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 0, binding = 0) uniform image2D image_in; +layout(set = 1, binding = 0, rgba8) uniform image2D image_out; + +vec3 normalizeHdrSceneValue(vec3 hdrSceneValue, vec3 paperWhiteNits) { + return (hdrSceneValue * paperWhiteNits) / vec3(10000); +} + +vec3 LinearToST2084(vec3 normalizedLinearValue) +{ + // return pow((0.8359375f.xxx + (pow(abs(normalizedLinearValue), 0.1593017578125f.xxx) * 18.8515625f)) / (1.0f.xxx + (pow(abs(normalizedLinearValue), 0.1593017578125f.xxx) * 18.6875f)), 78.84375f.xxx); +} + +vec4 convertToHdr10(float4 hdrSceneValue, float paperWhiteNits) { + // float3 rec2020 = mul(float3x3(float3(0.6274039745330810546875f, 0.329281985759735107421875f, 0.043313600122928619384765625f), float3(0.06909699738025665283203125f, 0.919539988040924072265625f, 0.0113612003624439239501953125f), float3(0.01639159955084323883056640625f, 0.0880132019519805908203125f, 0.895595014095306396484375f)), hdrSceneValue.xyz); + // float3 normalizedLinearValue = NormalizeHDRSceneValue(rec2020, paperWhiteNits); + // float3 HDR10 = LinearToST2084(normalizedLinearValue); + // return float4(HDR10, hdrSceneValue.w); +} + +void main() { + ivec2 coords = ivec2(gl_GlobalInvocationID.xy); + vec4 in_pixel = imageLoad(image_in, coords); + imageStore(image_out, coords, convertToHdr10(in_pixel, 200)); +} diff --git a/gpu_examples/shaders/linearToSt2084.comp.hlsl b/gpu_examples/shaders/linearToSt2084.comp.hlsl new file mode 100644 index 0000000..70b9ad7 --- /dev/null +++ b/gpu_examples/shaders/linearToSt2084.comp.hlsl @@ -0,0 +1,29 @@ +Texture2D InImage : register(t0, space0); +[[vk::image_format("rgba8")]] +RWTexture2D OutImage : register(u0, space1); + +float3 NormalizeHDRSceneValue(float3 hdrSceneValue, float paperWhiteNits) +{ + return (hdrSceneValue * paperWhiteNits) / 10000.0f.xxx; +} + +float3 LinearToST2084(float3 normalizedLinearValue) +{ + return pow((0.8359375f.xxx + (pow(abs(normalizedLinearValue), 0.1593017578125f.xxx) * 18.8515625f)) / (1.0f.xxx + (pow(abs(normalizedLinearValue), 0.1593017578125f.xxx) * 18.6875f)), 78.84375f.xxx); +} + +float4 ConvertToHDR10(float4 hdrSceneValue, float paperWhiteNits) +{ + float3 rec2020 = mul(float3x3(float3(0.6274039745330810546875f, 0.329281985759735107421875f, 0.043313600122928619384765625f), float3(0.06909699738025665283203125f, 0.919539988040924072265625f, 0.0113612003624439239501953125f), float3(0.01639159955084323883056640625f, 0.0880132019519805908203125f, 0.895595014095306396484375f)), hdrSceneValue.xyz); + float3 normalizedLinearValue = NormalizeHDRSceneValue(rec2020, paperWhiteNits); + float3 HDR10 = LinearToST2084(normalizedLinearValue); + return float4(HDR10, hdrSceneValue.w); +} + +[numthreads(8, 8, 1)] +void main(uint3 GlobalInvocationID : SV_DispatchThreadID) +{ + int2 coord = int2(GlobalInvocationID.xy); + float4 inPixel = InImage[coord]; + OutImage[coord] = ConvertToHDR10(inPixel, 200.0f); +} diff --git a/gpu_examples/shaders/positionColor.vert.glsl b/gpu_examples/shaders/positionColor.vert.glsl new file mode 100644 index 0000000..181ca35 --- /dev/null +++ b/gpu_examples/shaders/positionColor.vert.glsl @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) in vec3 position_in; +layout(location = 1) in vec4 color_in; + +layout(location = 0) out vec4 color_out; + +void main() { + gl_Position = vec4(position_in, 1); + color_out = color_in; +} diff --git a/gpu_examples/shaders/positionColor.vert.hlsl b/gpu_examples/shaders/positionColor.vert.hlsl new file mode 100644 index 0000000..8b106f1 --- /dev/null +++ b/gpu_examples/shaders/positionColor.vert.hlsl @@ -0,0 +1,19 @@ +struct Input +{ + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; +}; + +struct Output +{ + float4 Color : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.Color = input.Color; + output.Position = float4(input.Position, 1.0f); + return output; +} diff --git a/gpu_examples/shaders/positionColor.vert.zig b/gpu_examples/shaders/positionColor.vert.zig new file mode 100644 index 0000000..c4ce485 --- /dev/null +++ b/gpu_examples/shaders/positionColor.vert.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +extern var position_in: @Vector(3, f32) addrspace(.input); +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_vertex) void { + std.gpu.location(&position_in, 0); + std.gpu.location(&color_in, 1); + + std.gpu.location(&color_out, 0); + + std.gpu.position_out.* = .{ position_in[0], position_in[1], position_in[2], 1 }; + color_out = color_in; +} diff --git a/gpu_examples/shaders/positionColorInstanced.vert.glsl b/gpu_examples/shaders/positionColorInstanced.vert.glsl new file mode 100644 index 0000000..075c353 --- /dev/null +++ b/gpu_examples/shaders/positionColorInstanced.vert.glsl @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) in vec3 position_in; +layout(location = 1) in vec4 color_in; + +layout(location = 0) out vec4 color_out; + +void main() { + color_out = color_in; + vec3 pos = (position_in * 0.25) - vec3(0.75, 0.75, 0.0); + pos.x += float(gl_InstanceIndex % 4) * 0.5f; + pos.y += floor(float(gl_InstanceIndex / 4)) * 0.5f; + gl_Position = vec4(pos, 1.0f); +} diff --git a/gpu_examples/shaders/positionColorInstanced.vert.hlsl b/gpu_examples/shaders/positionColorInstanced.vert.hlsl new file mode 100644 index 0000000..3b0ba50 --- /dev/null +++ b/gpu_examples/shaders/positionColorInstanced.vert.hlsl @@ -0,0 +1,23 @@ +struct Input +{ + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; + uint InstanceIndex : SV_InstanceID; +}; + +struct Output +{ + float4 Color : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.Color = input.Color; + float3 pos = (input.Position * 0.25f) - float3(0.75f, 0.75f, 0.0f); + pos.x += (float(input.InstanceIndex % 4) * 0.5f); + pos.y += (floor(float(input.InstanceIndex / 4)) * 0.5f); + output.Position = float4(pos, 1.0f); + return output; +} diff --git a/gpu_examples/shaders/positionColorInstanced.vert.zig b/gpu_examples/shaders/positionColorInstanced.vert.zig new file mode 100644 index 0000000..6223e53 --- /dev/null +++ b/gpu_examples/shaders/positionColorInstanced.vert.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +extern var position_in: @Vector(3, f32) addrspace(.input); +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_vertex) void { + std.gpu.location(&position_in, 0); + std.gpu.location(&color_in, 1); + + std.gpu.location(&color_out, 0); + + color_out = color_in; + const instance_index = std.gpu.instance_index; + var pos = position_in * @as(@Vector(3, f32), @splat(0.25)) - @Vector(3, f32){ 0.75, 0.75, 0.0 }; + pos[0] += @as(f32, @floatFromInt(instance_index % 4)) * 0.5; + pos[1] += @floor(@as(f32, @floatFromInt(instance_index / 4))) * 0.5; + std.gpu.position_out.* = .{ pos[0], pos[1], pos[2], 1 }; +} diff --git a/gpu_examples/shaders/positionColorTransform.vert.glsl b/gpu_examples/shaders/positionColorTransform.vert.glsl new file mode 100644 index 0000000..de87c31 --- /dev/null +++ b/gpu_examples/shaders/positionColorTransform.vert.glsl @@ -0,0 +1,15 @@ +#version 450 + +layout(set = 1, binding = 0, std140) uniform uniforms { + mat4 transform; +}; + +layout(location = 0) in vec3 position_in; +layout(location = 1) in vec4 color_in; + +layout(location = 0) out vec4 color_out; + +void main() { + gl_Position = transform * vec4(position_in, 1); + color_out = color_in; +} diff --git a/gpu_examples/shaders/positionColorTransform.vert.hlsl b/gpu_examples/shaders/positionColorTransform.vert.hlsl new file mode 100644 index 0000000..05c8826 --- /dev/null +++ b/gpu_examples/shaders/positionColorTransform.vert.hlsl @@ -0,0 +1,24 @@ +cbuffer UBO : register(b0, space1) +{ + float4x4 transform : packoffset(c0); +}; + +struct Input +{ + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; +}; + +struct Output +{ + float4 Color : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.Color = input.Color; + output.Position = mul(transform, float4(input.Position, 1.0f)); + return output; +} diff --git a/gpu_examples/shaders/positionColorTransform.vert.zig b/gpu_examples/shaders/positionColorTransform.vert.zig new file mode 100644 index 0000000..612aff2 --- /dev/null +++ b/gpu_examples/shaders/positionColorTransform.vert.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +extern var uniforms: extern struct { + transform: Mat4, +} addrspace(.uniform); + +extern var position_in: @Vector(3, f32) addrspace(.input); +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +const Mat4 = extern struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ @reduce(.Add, ar0 * b), @reduce(.Add, ar1 * b), @reduce(.Add, ar2 * b), @reduce(.Add, ar3 * b) }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +export fn main() callconv(.spirv_vertex) void { + std.gpu.binding(&uniforms, 1, 0); + + std.gpu.location(&position_in, 0); + std.gpu.location(&color_in, 1); + + std.gpu.location(&color_out, 0); + + std.gpu.position_out.* = uniforms.transform.mulVec(.{ position_in[0], position_in[1], position_in[2], 1 }); + color_out = color_in; +} diff --git a/gpu_examples/shaders/rawTriangle.vert.glsl b/gpu_examples/shaders/rawTriangle.vert.glsl new file mode 100644 index 0000000..9ea937d --- /dev/null +++ b/gpu_examples/shaders/rawTriangle.vert.glsl @@ -0,0 +1,20 @@ +#version 450 + +layout(location = 0) out vec4 color_out; + +void main() { + switch (gl_VertexIndex) { + case 0: + gl_Position = vec4(-1, -1, 0, 1); + color_out = vec4(1, 0, 0, 1); + break; + case 1: + gl_Position = vec4(1, -1, 0, 1); + color_out = vec4(0, 1, 0, 1); + break; + default: + gl_Position = vec4(0, 1, 0, 1); + color_out = vec4(0, 0, 1, 1); + break; + } +} diff --git a/gpu_examples/shaders/rawTriangle.vert.hlsl b/gpu_examples/shaders/rawTriangle.vert.hlsl new file mode 100644 index 0000000..56c2885 --- /dev/null +++ b/gpu_examples/shaders/rawTriangle.vert.hlsl @@ -0,0 +1,39 @@ +struct Input +{ + uint VertexIndex : SV_VertexID; +}; + +struct Output +{ + float4 Color : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + float2 pos; + if (input.VertexIndex == 0) + { + pos = (-1.0f).xx; + output.Color = float4(1.0f, 0.0f, 0.0f, 1.0f); + } + else + { + if (input.VertexIndex == 1) + { + pos = float2(1.0f, -1.0f); + output.Color = float4(0.0f, 1.0f, 0.0f, 1.0f); + } + else + { + if (input.VertexIndex == 2) + { + pos = float2(0.0f, 1.0f); + output.Color = float4(0.0f, 0.0f, 1.0f, 1.0f); + } + } + } + output.Position = float4(pos, 0.0f, 1.0f); + return output; +} diff --git a/gpu_examples/shaders/rawTriangle.vert.zig b/gpu_examples/shaders/rawTriangle.vert.zig new file mode 100644 index 0000000..5885abd --- /dev/null +++ b/gpu_examples/shaders/rawTriangle.vert.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_vertex) void { + std.gpu.location(&color_out, 0); + + switch (std.gpu.vertex_index) { + 0 => { + std.gpu.position_out.* = .{ -1, -1, 0, 1 }; + color_out = .{ 1, 0, 0, 1 }; + }, + 1 => { + std.gpu.position_out.* = .{ 1, -1, 0, 1 }; + color_out = .{ 0, 1, 0, 1 }; + }, + else => { + std.gpu.position_out.* = .{ 0, 1, 0, 1 }; + color_out = .{ 0, 0, 1, 1 }; + }, + } +} diff --git a/gpu_examples/shaders/skybox.frag.glsl b/gpu_examples/shaders/skybox.frag.glsl new file mode 100644 index 0000000..f12982c --- /dev/null +++ b/gpu_examples/shaders/skybox.frag.glsl @@ -0,0 +1,11 @@ +#version 450 + +layout(set = 2, binding = 0) uniform samplerCube tex0; + +layout(location = 0) in vec3 tex_coord_in; + +layout(location = 0) out vec4 color_out; + +void main() { + color_out = texture(tex0, tex_coord_in); +} diff --git a/gpu_examples/shaders/skybox.frag.hlsl b/gpu_examples/shaders/skybox.frag.hlsl new file mode 100644 index 0000000..4fff46e --- /dev/null +++ b/gpu_examples/shaders/skybox.frag.hlsl @@ -0,0 +1,7 @@ +TextureCube SkyboxTexture : register(t0, space2); +SamplerState SkyboxSampler : register(s0, space2); + +float4 main(float3 TexCoord : TEXCOORD0) : SV_Target0 +{ + return SkyboxTexture.Sample(SkyboxSampler, TexCoord); +} diff --git a/gpu_examples/shaders/skybox.frag.zig b/gpu_examples/shaders/skybox.frag.zig new file mode 100644 index 0000000..09f785f --- /dev/null +++ b/gpu_examples/shaders/skybox.frag.zig @@ -0,0 +1,16 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const tex0 = common.SamplerCube(2, 0); + +extern var tex_coord_in: @Vector(3, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = tex0.texture(tex_coord_in); +} diff --git a/gpu_examples/shaders/skybox.vert.glsl b/gpu_examples/shaders/skybox.vert.glsl new file mode 100644 index 0000000..d57ab53 --- /dev/null +++ b/gpu_examples/shaders/skybox.vert.glsl @@ -0,0 +1,14 @@ +#version 450 + +layout(set = 1, binding = 0, std140) uniform uniforms { + mat4 transform; +}; + +layout(location = 0) in vec3 position_in; + +layout(location = 0) out vec3 tex_coord_out; + +void main() { + gl_Position = transform * vec4(position_in, 1); + tex_coord_out = position_in; +} diff --git a/gpu_examples/shaders/skybox.vert.hlsl b/gpu_examples/shaders/skybox.vert.hlsl new file mode 100644 index 0000000..14c72f6 --- /dev/null +++ b/gpu_examples/shaders/skybox.vert.hlsl @@ -0,0 +1,23 @@ +cbuffer UniformBlock : register(b0, space1) +{ + float4x4 MatrixTransform : packoffset(c0); +}; + +struct SPIRV_Cross_Input +{ + float3 inTexCoord : TEXCOORD0; +}; + +struct Output +{ + float3 TexCoord : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(float3 inTexCoord : TEXCOORD0) +{ + Output output; + output.TexCoord = inTexCoord; + output.Position = mul(MatrixTransform, float4(inTexCoord, 1.0)); + return output; +} diff --git a/gpu_examples/shaders/skybox.vert.zig b/gpu_examples/shaders/skybox.vert.zig new file mode 100644 index 0000000..37c51c7 --- /dev/null +++ b/gpu_examples/shaders/skybox.vert.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +extern var uniforms: extern struct { + transform: Mat4, +} addrspace(.uniform); + +extern var position_in: @Vector(3, f32) addrspace(.input); + +extern var tex_coord_out: @Vector(3, f32) addrspace(.output); + +const Mat4 = extern struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ @reduce(.Add, ar0 * b), @reduce(.Add, ar1 * b), @reduce(.Add, ar2 * b), @reduce(.Add, ar3 * b) }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +export fn main() callconv(.spirv_vertex) void { + std.gpu.binding(&uniforms, 1, 0); + + std.gpu.location(&position_in, 0); + + std.gpu.location(&tex_coord_out, 0); + + std.gpu.position_out.* = uniforms.transform.mulVec(.{ position_in[0], position_in[1], position_in[2], 1 }); + tex_coord_out = position_in; +} diff --git a/gpu_examples/shaders/solidColor.frag.glsl b/gpu_examples/shaders/solidColor.frag.glsl new file mode 100644 index 0000000..e49cf9d --- /dev/null +++ b/gpu_examples/shaders/solidColor.frag.glsl @@ -0,0 +1,9 @@ +#version 450 + +layout(location = 0) in vec4 color_in; + +layout(location = 0) out vec4 color_out; + +void main() { + color_out = color_in; +} diff --git a/gpu_examples/shaders/solidColor.frag.hlsl b/gpu_examples/shaders/solidColor.frag.hlsl new file mode 100644 index 0000000..a8e6d9f --- /dev/null +++ b/gpu_examples/shaders/solidColor.frag.hlsl @@ -0,0 +1,4 @@ +float4 main(float4 Color : TEXCOORD0) : SV_Target0 +{ + return Color; +} diff --git a/gpu_examples/shaders/solidColor.frag.zig b/gpu_examples/shaders/solidColor.frag.zig new file mode 100644 index 0000000..158420a --- /dev/null +++ b/gpu_examples/shaders/solidColor.frag.zig @@ -0,0 +1,12 @@ +const std = @import("std"); + +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&color_in, 0); + std.gpu.location(&color_out, 0); + + color_out = color_in; +} diff --git a/gpu_examples/shaders/solidColorDepth.frag.glsl b/gpu_examples/shaders/solidColorDepth.frag.glsl new file mode 100644 index 0000000..b5c1727 --- /dev/null +++ b/gpu_examples/shaders/solidColorDepth.frag.glsl @@ -0,0 +1,20 @@ +#version 450 + +layout(set = 3, binding = 0, std140) uniform uniforms { + float near_plane; + float far_plane; +}; + +layout(location = 0) in vec4 color_in; + +layout(location = 0) out vec4 color_out; + +float linearizeDepth(float depth, float near, float far) { + float z = depth * 2 - 1; + return ((2.0 * near * far) / (far + near - z * (far - near))) / far; +} + +void main() { + color_out = color_in; + gl_FragDepth = linearizeDepth(gl_FragCoord.z, near_plane, far_plane); +} diff --git a/gpu_examples/shaders/solidColorDepth.frag.hlsl b/gpu_examples/shaders/solidColorDepth.frag.hlsl new file mode 100644 index 0000000..ea61ec1 --- /dev/null +++ b/gpu_examples/shaders/solidColorDepth.frag.hlsl @@ -0,0 +1,25 @@ +cbuffer UBO : register(b0, space3) +{ + float NearPlane; + float FarPlane; +}; + +struct Output +{ + float4 Color : SV_Target0; + float Depth : SV_Depth; +}; + +float LinearizeDepth(float depth, float near, float far) +{ + float z = depth * 2.0 - 1.0; + return ((2.0 * near * far) / (far + near - z * (far - near))) / far; +} + +Output main(float4 Color : TEXCOORD0, float4 Position : SV_Position) +{ + Output result; + result.Color = Color; + result.Depth = LinearizeDepth(Position.z, NearPlane, FarPlane); + return result; +} diff --git a/gpu_examples/shaders/solidColorDepth.frag.zig b/gpu_examples/shaders/solidColorDepth.frag.zig new file mode 100644 index 0000000..f640b59 --- /dev/null +++ b/gpu_examples/shaders/solidColorDepth.frag.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +extern var uniforms: extern struct { + near_plane: f32, + far_plane: f32, +} addrspace(.uniform); + +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +fn linearizeDepth(depth: f32, near: f32, far: f32) f32 { + const z = depth * 2 - 1; + return ((2 * near * far) / (far + near - z * (far - near))) / far; +} + +export fn main() callconv(.spirv_fragment) void { + // Unfortunately this is broken currently. However, I hack this functionality in using SPIRV-tools and a custom build tool... + // std.gpu.executionMode(main, .depth_replacing); + std.gpu.binding(&uniforms, 3, 0); + std.gpu.location(&color_in, 0); + std.gpu.location(&color_out, 0); + + color_out = color_in; + std.gpu.frag_depth = linearizeDepth(std.gpu.frag_coord[2], uniforms.near_plane, uniforms.far_plane); +} diff --git a/gpu_examples/shaders/spriteBatch.comp.glsl b/gpu_examples/shaders/spriteBatch.comp.glsl new file mode 100644 index 0000000..e963e70 --- /dev/null +++ b/gpu_examples/shaders/spriteBatch.comp.glsl @@ -0,0 +1,77 @@ +#version 450 + +struct SpriteComputeData +{ + vec3 position; + float rotation; + vec2 scale; + vec4 texture; + vec4 color; +}; + +struct SpriteVertex +{ + vec4 position; + vec2 tex_coord; + vec4 color; +}; + +layout(local_size_x = 64) in; +layout(set = 0, binding = 0, std140) buffer computeBuffer { + SpriteComputeData sprites[]; +}; + +layout(set = 1, binding = 0, std140) buffer vertexBuffer { + SpriteVertex vertices[]; +}; + +void main() { + uint sprite_ind = gl_GlobalInvocationID.x; + SpriteComputeData sprite = sprites[sprite_ind]; + + mat4 scale = mat4( + vec4(sprite.scale.x, 0.0f, 0.0f, 0.0f), + vec4(0.0f, sprite.scale.y, 0.0f, 0.0f), + vec4(0.0f, 0.0f, 1.0f, 0.0f), + vec4(0.0f, 0.0f, 0.0f, 1.0f) + ); + + float c = cos(sprite.rotation); + float s = sin(sprite.rotation); + + mat4 rotation = mat4( + vec4(c, s, 0.0f, 0.0f), + vec4(-s, c, 0.0f, 0.0f), + vec4(0.0f, 0.0f, 1.0f, 0.0f), + vec4(0.0f, 0.0f, 0.0f, 1.0f) + ); + + mat4 translation = mat4( + vec4(1.0f, 0.0f, 0.0f, 0.0f), + vec4(0.0f, 1.0f, 0.0f, 0.0f), + vec4(0.0f, 0.0f, 1.0f, 0.0f), + vec4(sprite.position.x, sprite.position.y, sprite.position.z, 1.0f) + ); + + mat4 model = translation * rotation * scale; + + vec4 top_left = vec4(0.0f, 0.0f, 0.0f, 1.0f); + vec4 top_right = vec4(1.0f, 0.0f, 0.0f, 1.0f); + vec4 bottom_left = vec4(0.0f, 1.0f, 0.0f, 1.0f); + vec4 bottom_right = vec4(1.0f, 1.0f, 0.0f, 1.0f); + + vertices[sprite_ind * 4u].position = model * top_left; + vertices[sprite_ind * 4u + 1].position = model * top_right; + vertices[sprite_ind * 4u + 2].position = model * bottom_left; + vertices[sprite_ind * 4u + 3].position = model * bottom_right; + + vertices[sprite_ind * 4u].tex_coord = vec2(sprite.texture.x, sprite.texture.y); + vertices[sprite_ind * 4u + 1].tex_coord = vec2(sprite.texture.x + sprite.texture.z, sprite.texture.y); + vertices[sprite_ind * 4u + 2].tex_coord = vec2(sprite.texture.x, sprite.texture.y + sprite.texture.w); + vertices[sprite_ind * 4u + 3].tex_coord = vec2(sprite.texture.x + sprite.texture.z, sprite.texture.y + sprite.texture.w); + + vertices[sprite_ind * 4u].color = sprite.color; + vertices[sprite_ind * 4u + 1].color = sprite.color; + vertices[sprite_ind * 4u + 2].color = sprite.color; + vertices[sprite_ind * 4u + 3].color = sprite.color; +} diff --git a/gpu_examples/shaders/spriteBatch.comp.hlsl b/gpu_examples/shaders/spriteBatch.comp.hlsl new file mode 100644 index 0000000..c732bca --- /dev/null +++ b/gpu_examples/shaders/spriteBatch.comp.hlsl @@ -0,0 +1,73 @@ +struct SpriteComputeData +{ + float3 Position; + float Rotation; + float2 Scale; + float2 Padding; + float TexU, TexV, TexW, TexH; + float4 Color; +}; + +struct SpriteVertex +{ + float4 Position; + float2 Texcoord; + float4 Color; +}; + +StructuredBuffer ComputeBuffer : register(t0, space0); +RWStructuredBuffer VertexBuffer : register(u0, space1); + +[numthreads(64, 1, 1)] +void main(uint3 GlobalInvocationID : SV_DispatchThreadID) +{ + uint n = GlobalInvocationID.x; + + SpriteComputeData sprite = ComputeBuffer[n]; + + float4x4 Scale = float4x4( + float4(sprite.Scale.x, 0.0f, 0.0f, 0.0f), + float4(0.0f, sprite.Scale.y, 0.0f, 0.0f), + float4(0.0f, 0.0f, 1.0f, 0.0f), + float4(0.0f, 0.0f, 0.0f, 1.0f) + ); + + float c = cos(sprite.Rotation); + float s = sin(sprite.Rotation); + + float4x4 Rotation = float4x4( + float4( c, s, 0.0f, 0.0f), + float4( -s, c, 0.0f, 0.0f), + float4(0.0f, 0.0f, 1.0f, 0.0f), + float4(0.0f, 0.0f, 0.0f, 1.0f) + ); + + float4x4 Translation = float4x4( + float4(1.0f, 0.0f, 0.0f, 0.0f), + float4(0.0f, 1.0f, 0.0f, 0.0f), + float4(0.0f, 0.0f, 1.0f, 0.0f), + float4(sprite.Position.x, sprite.Position.y, sprite.Position.z, 1.0f) + ); + + float4x4 Model = mul(Scale, mul(Rotation, Translation)); + + float4 topLeft = float4(0.0f, 0.0f, 0.0f, 1.0f); + float4 topRight = float4(1.0f, 0.0f, 0.0f, 1.0f); + float4 bottomLeft = float4(0.0f, 1.0f, 0.0f, 1.0f); + float4 bottomRight = float4(1.0f, 1.0f, 0.0f, 1.0f); + + VertexBuffer[n * 4u] .Position = mul(topLeft, Model); + VertexBuffer[n * 4u + 1].Position = mul(topRight, Model); + VertexBuffer[n * 4u + 2].Position = mul(bottomLeft, Model); + VertexBuffer[n * 4u + 3].Position = mul(bottomRight, Model); + + VertexBuffer[n * 4u] .Texcoord = float2(sprite.TexU, sprite.TexV); + VertexBuffer[n * 4u + 1].Texcoord = float2(sprite.TexU + sprite.TexW, sprite.TexV); + VertexBuffer[n * 4u + 2].Texcoord = float2(sprite.TexU, sprite.TexV + sprite.TexH); + VertexBuffer[n * 4u + 3].Texcoord = float2(sprite.TexU + sprite.TexW, sprite.TexV + sprite.TexH); + + VertexBuffer[n * 4u] .Color = sprite.Color; + VertexBuffer[n * 4u + 1].Color = sprite.Color; + VertexBuffer[n * 4u + 2].Color = sprite.Color; + VertexBuffer[n * 4u + 3].Color = sprite.Color; +} diff --git a/gpu_examples/shaders/spriteBatch.comp.zig b/gpu_examples/shaders/spriteBatch.comp.zig new file mode 100644 index 0000000..fc348cf --- /dev/null +++ b/gpu_examples/shaders/spriteBatch.comp.zig @@ -0,0 +1,139 @@ +// TODO: FIGURE OUT WHY THIS DOESN'T WORK! + +const common = @import("common.zig"); +const std = @import("std"); + +const sprites = common.RuntimeArray(0, 0, SpriteComputeData); + +const vertices = common.RuntimeArray(1, 0, SpriteVertex); + +const SpriteComputeData = extern struct { + position_rot: @Vector(4, f32), // Rotation should be after this, but having it after messes up the alignment! + scale: @Vector(2, f32), + texture: @Vector(4, f32), + color: @Vector(4, f32), + + comptime { + std.debug.assert(@offsetOf(SpriteComputeData, "position_rot") == 0); + // std.debug.assert(@offsetOf(SpriteComputeData, "rotation") == 12); + std.debug.assert(@offsetOf(SpriteComputeData, "scale") == 16); + std.debug.assert(@offsetOf(SpriteComputeData, "texture") == 32); + std.debug.assert(@offsetOf(SpriteComputeData, "color") == 48); + } +}; + +const SpriteVertex = extern struct { + position: @Vector(4, f32), + tex_coord: @Vector(2, f32), + color: @Vector(4, f32), + + comptime { + std.debug.assert(@offsetOf(SpriteVertex, "position") == 0); + std.debug.assert(@offsetOf(SpriteVertex, "tex_coord") == 16); + std.debug.assert(@offsetOf(SpriteVertex, "color") == 32); + } +}; + +const Mat4 = extern struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn mul(a: Mat4, b: Mat4) Mat4 { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ + .c0 = .{ @reduce(.Add, ar0 * b.c0), @reduce(.Add, ar1 * b.c0), @reduce(.Add, ar2 * b.c0), @reduce(.Add, ar3 * b.c0) }, + .c1 = .{ @reduce(.Add, ar0 * b.c1), @reduce(.Add, ar1 * b.c1), @reduce(.Add, ar2 * b.c1), @reduce(.Add, ar3 * b.c1) }, + .c2 = .{ @reduce(.Add, ar0 * b.c2), @reduce(.Add, ar1 * b.c2), @reduce(.Add, ar2 * b.c2), @reduce(.Add, ar3 * b.c2) }, + .c3 = .{ @reduce(.Add, ar0 * b.c3), @reduce(.Add, ar1 * b.c3), @reduce(.Add, ar2 * b.c3), @reduce(.Add, ar3 * b.c3) }, + }; + } + + pub fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ @reduce(.Add, ar0 * b), @reduce(.Add, ar1 * b), @reduce(.Add, ar2 * b), @reduce(.Add, ar3 * b) }; + } + + pub fn rotation2(rot: f32) Mat4 { + const c = common.cos(rot); + const s = common.sin(rot); + return .{ + .c0 = .{ c, s, 0, 0 }, + .c1 = .{ -s, c, 0, 0 }, + .c2 = .{ 0, 0, 1, 0 }, + .c3 = .{ 0, 0, 0, 1 }, + }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } + + pub fn scale(val: @Vector(4, f32)) Mat4 { + return .{ + .c0 = .{ val[0], 0, 0, 0 }, + .c1 = .{ 0, val[1], 0, 0 }, + .c2 = .{ 0, 0, val[2], 0 }, + .c3 = .{ 0, 0, 0, val[3] }, + }; + } + + pub fn translation(amount: @Vector(3, f32)) Mat4 { + return .{ + .c0 = .{ 1, 0, 0, 0 }, + .c1 = .{ 0, 1, 0, 0 }, + .c2 = .{ 0, 0, 1, 0 }, + .c3 = .{ amount[0], amount[1], amount[2], 1 }, + }; + } +}; + +export fn main() callconv(.spirv_kernel) void { + // std.gpu.executionMode(main, .{ .local_size = .{ .x = 64, .y = 1, .z = 1 } }); // Set in build system by `spriv_execution_mode`. + + const sprite_ind = std.gpu.global_invocation_id[0]; + const sprite = sprites.read(sprite_ind); + + const model = Mat4.translation(.{ + sprite.position_rot[0], + sprite.position_rot[1], + sprite.position_rot[2], + }).mul(Mat4.rotation2(sprite.position_rot[3])).mul(Mat4.scale(.{ sprite.scale[0], sprite.scale[1], 1, 1 })); + const top_left = @Vector(4, f32){ 0, 0, 0, 1 }; + const top_right = @Vector(4, f32){ 1, 0, 0, 1 }; + const bottom_left = @Vector(4, f32){ 0, 1, 0, 1 }; + const bottom_right = @Vector(4, f32){ 1, 1, 0, 1 }; + vertices.write(sprite_ind * 4, .{ + .position = model.mulVec(top_left), + .tex_coord = .{ sprite.texture[0], sprite.texture[1] }, + .color = sprite.color, + }); + vertices.write(sprite_ind * 4 + 1, .{ + .position = model.mulVec(top_right), + .tex_coord = .{ sprite.texture[0] + sprite.texture[2], sprite.texture[1] }, + .color = sprite.color, + }); + vertices.write(sprite_ind * 4 + 2, .{ + .position = model.mulVec(bottom_left), + .tex_coord = .{ sprite.texture[0], sprite.texture[1] + sprite.texture[3] }, + .color = sprite.color, + }); + vertices.write(sprite_ind * 4 + 3, .{ + .position = model.mulVec(bottom_right), + .tex_coord = .{ sprite.texture[0] + sprite.texture[2], sprite.texture[1] + sprite.texture[3] }, + .color = sprite.color, + }); +} diff --git a/gpu_examples/shaders/texturedQuad.comp.glsl b/gpu_examples/shaders/texturedQuad.comp.glsl new file mode 100644 index 0000000..a51c692 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.comp.glsl @@ -0,0 +1,18 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 0, binding = 0) uniform sampler2D texture_in; + +layout(set = 1, binding = 0, rgba8) uniform image2D image_out; + +layout(set = 2, binding = 0, std140) uniform uniforms { + float texcoord_multiplier; +}; + +void main() { + ivec2 size = textureSize(texture_in, 0); + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + vec2 texcoord = (vec2(coord) * vec2(texcoord_multiplier)) / vec2(size); + vec4 in_pixel = texture(texture_in, texcoord, 0); + imageStore(image_out, coord, in_pixel); +} diff --git a/gpu_examples/shaders/texturedQuad.comp.hlsl b/gpu_examples/shaders/texturedQuad.comp.hlsl new file mode 100644 index 0000000..f3dc7b7 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.comp.hlsl @@ -0,0 +1,21 @@ +cbuffer UBO : register(b0, space2) +{ + float ubo_texcoord_multiplier : packoffset(c0); +}; + +Texture2D inImage : register(t0, space0); +SamplerState inImageSampler : register(s0, space0); + +[[vk::image_format("rgba8")]] +RWTexture2D outImage : register(u0, space1); + +[numthreads(8, 8, 1)] +void main(uint3 GlobalInvocationID : SV_DispatchThreadID) +{ + float w, h; + inImage.GetDimensions(w, h); + int2 coord = int2(GlobalInvocationID.xy); + float2 texcoord = (float2(coord) * ubo_texcoord_multiplier) / float2(w, h); + float4 inPixel = inImage.SampleLevel(inImageSampler, texcoord, 0.0f); + outImage[coord] = inPixel; +} diff --git a/gpu_examples/shaders/texturedQuad.comp.zig b/gpu_examples/shaders/texturedQuad.comp.zig new file mode 100644 index 0000000..99d38d0 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.comp.zig @@ -0,0 +1,25 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const texture_in = common.Sampler2d(0, 0); + +const image_out = common.Texture2dRgba8(1, 0); + +extern var uniforms: extern struct { + texcoord_multiplier: f32, +} addrspace(.uniform); + +export fn main() callconv(.spirv_kernel) void { + // std.gpu.executionMode(main, .{ .local_size = .{ .x = 8, .y = 8, .z = 1 } }); // Set in build system by `spriv_execution_mode`. + + std.gpu.binding(&uniforms, 2, 0); + + const image_sizei = texture_in.size(0); + const image_size = @Vector(2, f32){ @floatFromInt(image_sizei[0]), @floatFromInt(image_sizei[1]) }; + const uvi = @Vector(2, u32){ std.gpu.global_invocation_id[0], std.gpu.global_invocation_id[1] }; + const uv = @Vector(2, f32){ + @as(f32, @floatFromInt(uvi[0])) * uniforms.texcoord_multiplier / image_size[0], + @as(f32, @floatFromInt(uvi[1])) * uniforms.texcoord_multiplier / image_size[1], + }; + image_out.store(uvi, texture_in.textureLod(uv, 0)); +} diff --git a/gpu_examples/shaders/texturedQuad.frag.glsl b/gpu_examples/shaders/texturedQuad.frag.glsl new file mode 100644 index 0000000..f1a5694 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.frag.glsl @@ -0,0 +1,12 @@ +#version 450 + +// https://wiki.libsdl.org/SDL3/SDL_CreateGPUShader +layout(set = 2, binding = 0) uniform sampler2D tex0; + +layout(location = 0) in vec2 tex_coord_in; + +layout(location = 0) out vec4 color_out; + +void main() { + color_out = texture(tex0, tex_coord_in); +} diff --git a/gpu_examples/shaders/texturedQuad.frag.hlsl b/gpu_examples/shaders/texturedQuad.frag.hlsl new file mode 100644 index 0000000..fe329e1 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.frag.hlsl @@ -0,0 +1,7 @@ +Texture2D Texture : register(t0, space2); +SamplerState Sampler : register(s0, space2); + +float4 main(float2 TexCoord : TEXCOORD0) : SV_Target0 +{ + return Texture.Sample(Sampler, TexCoord); +} diff --git a/gpu_examples/shaders/texturedQuad.frag.zig b/gpu_examples/shaders/texturedQuad.frag.zig new file mode 100644 index 0000000..2c41027 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.frag.zig @@ -0,0 +1,16 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const tex0 = common.Sampler2d(2, 0); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = tex0.texture(tex_coord_in); +} diff --git a/gpu_examples/shaders/texturedQuad.vert.glsl b/gpu_examples/shaders/texturedQuad.vert.glsl new file mode 100644 index 0000000..ba9f3c1 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.vert.glsl @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) in vec3 position_in; +layout(location = 1) in vec2 tex_coord_in; + +layout(location = 0) out vec2 tex_coord_out; + +void main() { + gl_Position = vec4(position_in, 1); + tex_coord_out = tex_coord_in; +} diff --git a/gpu_examples/shaders/texturedQuad.vert.hlsl b/gpu_examples/shaders/texturedQuad.vert.hlsl new file mode 100644 index 0000000..54a62de --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.vert.hlsl @@ -0,0 +1,19 @@ +struct Input +{ + float3 Position : TEXCOORD0; + float2 TexCoord : TEXCOORD1; +}; + +struct Output +{ + float2 TexCoord : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.TexCoord = input.TexCoord; + output.Position = float4(input.Position, 1.0f); + return output; +} diff --git a/gpu_examples/shaders/texturedQuad.vert.zig b/gpu_examples/shaders/texturedQuad.vert.zig new file mode 100644 index 0000000..eb7ca40 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.vert.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +extern var position_in: @Vector(3, f32) addrspace(.input); +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var tex_coord_out: @Vector(2, f32) addrspace(.output); + +export fn main() callconv(.spirv_vertex) void { + std.gpu.location(&position_in, 0); + std.gpu.location(&tex_coord_in, 1); + + std.gpu.location(&tex_coord_out, 0); + + std.gpu.position_out.* = .{ position_in[0], position_in[1], position_in[2], 1 }; + tex_coord_out = tex_coord_in; +} diff --git a/gpu_examples/shaders/texturedQuadArray.frag.glsl b/gpu_examples/shaders/texturedQuadArray.frag.glsl new file mode 100644 index 0000000..b608607 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadArray.frag.glsl @@ -0,0 +1,16 @@ +#version 450 + +// https://wiki.libsdl.org/SDL3/SDL_CreateGPUShader +layout(set = 2, binding = 0) uniform sampler2DArray tex0; + +layout(location = 0) in vec2 tex_coord_in; + +layout(location = 0) out vec4 color_out; + +void main() { + float array_index = 0; + if (tex_coord_in.y > 0.5) { + array_index = 1; + } + color_out = texture(tex0, vec3(tex_coord_in, array_index)); +} diff --git a/gpu_examples/shaders/texturedQuadArray.frag.hlsl b/gpu_examples/shaders/texturedQuadArray.frag.hlsl new file mode 100644 index 0000000..6588a57 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadArray.frag.hlsl @@ -0,0 +1,8 @@ +Texture2DArray Texture : register(t0, space2); +SamplerState Sampler : register(s0, space2); + +float4 main(float2 TexCoord : TEXCOORD0) : SV_Target0 +{ + uint arrayIndex = uint(int(TexCoord.y > 0.5f)); + return Texture.Sample(Sampler, float3(TexCoord, float(arrayIndex))); +} diff --git a/gpu_examples/shaders/texturedQuadArray.frag.zig b/gpu_examples/shaders/texturedQuadArray.frag.zig new file mode 100644 index 0000000..b2d0302 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadArray.frag.zig @@ -0,0 +1,20 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const tex0 = common.Sampler2dArray(2, 0); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = tex0.texture(.{ + tex_coord_in[0], + tex_coord_in[1], + if (tex_coord_in[1] > 0.5) 1 else 0, + }); +} diff --git a/gpu_examples/shaders/texturedQuadColor.frag.glsl b/gpu_examples/shaders/texturedQuadColor.frag.glsl new file mode 100644 index 0000000..11074b8 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadColor.frag.glsl @@ -0,0 +1,12 @@ +#version 450 + +layout(set = 2, binding = 0) uniform sampler2D tex0; + +layout(location = 0) in vec2 tex_coord_in; +layout(location = 1) in vec4 color_in; + +layout(location = 0) out vec4 color_out; + +void main() { + color_out = texture(tex0, tex_coord_in) * color_in; +} diff --git a/gpu_examples/shaders/texturedQuadColor.frag.hlsl b/gpu_examples/shaders/texturedQuadColor.frag.hlsl new file mode 100644 index 0000000..3878dbe --- /dev/null +++ b/gpu_examples/shaders/texturedQuadColor.frag.hlsl @@ -0,0 +1,13 @@ +Texture2D Texture : register(t0, space2); +SamplerState Sampler : register(s0, space2); + +struct Input +{ + float2 TexCoord : TEXCOORD0; + float4 Color : TEXCOORD1; +}; + +float4 main(Input input) : SV_Target0 +{ + return input.Color * Texture.Sample(Sampler, input.TexCoord); +} diff --git a/gpu_examples/shaders/texturedQuadColor.frag.zig b/gpu_examples/shaders/texturedQuadColor.frag.zig new file mode 100644 index 0000000..2bedc29 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadColor.frag.zig @@ -0,0 +1,18 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const tex0 = common.Sampler2d(2, 0); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + std.gpu.location(&color_in, 1); + + std.gpu.location(&color_out, 0); + + color_out = tex0.texture(tex_coord_in) * color_in; +} diff --git a/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.glsl b/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.glsl new file mode 100644 index 0000000..f4306ae --- /dev/null +++ b/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.glsl @@ -0,0 +1,18 @@ +#version 450 + +layout(set = 1, binding = 0, std140) uniform uniforms { + mat4 transform; +}; + +layout(location = 0) in vec4 position_in; +layout(location = 1) in vec2 tex_coord_in; +layout(location = 2) in vec4 color_in; + +layout(location = 0) out vec2 tex_coord_out; +layout(location = 1) out vec4 color_out; + +void main() { + gl_Position = transform * position_in; + tex_coord_out = tex_coord_in; + color_out = color_in; +} diff --git a/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.hlsl b/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.hlsl new file mode 100644 index 0000000..103ae99 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.hlsl @@ -0,0 +1,27 @@ +cbuffer UniformBlock : register(b0, space1) +{ + float4x4 MatrixTransform : packoffset(c0); +}; + +struct Input +{ + float4 Position : TEXCOORD0; + float2 TexCoord : TEXCOORD1; + float4 Color : TEXCOORD2; +}; + +struct Output +{ + float2 TexCoord : TEXCOORD0; + float4 Color : TEXCOORD1; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.TexCoord = input.TexCoord; + output.Color = input.Color; + output.Position = mul(MatrixTransform, input.Position); + return output; +} diff --git a/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.zig b/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.zig new file mode 100644 index 0000000..6b14760 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadColorWithMatrix.vert.zig @@ -0,0 +1,52 @@ +const std = @import("std"); + +extern var uniforms: extern struct { + transform: Mat4, +} addrspace(.uniform); + +extern var position_in: @Vector(4, f32) addrspace(.input); +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); +extern var color_in: @Vector(4, f32) addrspace(.input); + +extern var tex_coord_out: @Vector(2, f32) addrspace(.output); +extern var color_out: @Vector(4, f32) addrspace(.output); + +const Mat4 = extern struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ @reduce(.Add, ar0 * b), @reduce(.Add, ar1 * b), @reduce(.Add, ar2 * b), @reduce(.Add, ar3 * b) }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +export fn main() callconv(.spirv_vertex) void { + std.gpu.binding(&uniforms, 1, 0); + + std.gpu.location(&position_in, 0); + std.gpu.location(&tex_coord_in, 1); + std.gpu.location(&color_in, 2); + + std.gpu.location(&tex_coord_out, 0); + std.gpu.location(&color_out, 1); + + std.gpu.position_out.* = uniforms.transform.mulVec(position_in); + tex_coord_out = tex_coord_in; + color_out = color_in; +} diff --git a/gpu_examples/shaders/texturedQuadWithMatrix.vert.glsl b/gpu_examples/shaders/texturedQuadWithMatrix.vert.glsl new file mode 100644 index 0000000..8408b23 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMatrix.vert.glsl @@ -0,0 +1,15 @@ +#version 450 + +layout(set = 1, binding = 0, std140) uniform uniforms { + mat4 transform; +}; + +layout(location = 0) in vec3 position_in; +layout(location = 1) in vec2 tex_coord_in; + +layout(location = 0) out vec2 tex_coord_out; + +void main() { + gl_Position = transform * vec4(position_in, 1); + tex_coord_out = tex_coord_in; +} diff --git a/gpu_examples/shaders/texturedQuadWithMatrix.vert.hlsl b/gpu_examples/shaders/texturedQuadWithMatrix.vert.hlsl new file mode 100644 index 0000000..ce24788 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMatrix.vert.hlsl @@ -0,0 +1,24 @@ +cbuffer UniformBlock : register(b0, space1) +{ + float4x4 MatrixTransform : packoffset(c0); +}; + +struct Input +{ + float4 Position : TEXCOORD0; + float2 TexCoord : TEXCOORD1; +}; + +struct Output +{ + float2 TexCoord : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.TexCoord = input.TexCoord; + output.Position = mul(MatrixTransform, input.Position); + return output; +} diff --git a/gpu_examples/shaders/texturedQuadWithMatrix.vert.zig b/gpu_examples/shaders/texturedQuadWithMatrix.vert.zig new file mode 100644 index 0000000..cb48643 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMatrix.vert.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +extern var uniforms: extern struct { + transform: Mat4, +} addrspace(.uniform); + +extern var position_in: @Vector(3, f32) addrspace(.input); +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var tex_coord_out: @Vector(2, f32) addrspace(.output); + +const Mat4 = extern struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ @reduce(.Add, ar0 * b), @reduce(.Add, ar1 * b), @reduce(.Add, ar2 * b), @reduce(.Add, ar3 * b) }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +export fn main() callconv(.spirv_vertex) void { + std.gpu.binding(&uniforms, 1, 0); + + std.gpu.location(&position_in, 0); + std.gpu.location(&tex_coord_in, 1); + + std.gpu.location(&tex_coord_out, 0); + + std.gpu.position_out.* = uniforms.transform.mulVec(.{ position_in[0], position_in[1], position_in[2], 1 }); + tex_coord_out = tex_coord_in; +} diff --git a/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.glsl b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.glsl new file mode 100644 index 0000000..28d621f --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.glsl @@ -0,0 +1,14 @@ +#version 450 + +layout(set = 2, binding = 0) uniform sampler2D tex0; +layout(set = 3, binding = 0, std140) uniform uniforms { + vec4 multiply_color; +}; + +layout(location = 0) in vec2 tex_coord_in; + +layout(location = 0) out vec4 color_out; + +void main() { + color_out = multiply_color * texture(tex0, tex_coord_in); +} diff --git a/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.hlsl b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.hlsl new file mode 100644 index 0000000..b0e23da --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.hlsl @@ -0,0 +1,12 @@ +cbuffer UniformBlock : register(b0, space3) +{ + float4 MultiplyColor : packoffset(c0); +}; + +Texture2D Texture : register(t0, space2); +SamplerState Sampler : register(s0, space2); + +float4 main(float2 TexCoord : TEXCOORD0) : SV_Target0 +{ + return MultiplyColor * Texture.Sample(Sampler, TexCoord); +} diff --git a/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.zig b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.zig new file mode 100644 index 0000000..51ff603 --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.zig @@ -0,0 +1,22 @@ +const common = @import("common.zig"); +const std = @import("std"); + +const tex0 = common.Sampler2d(2, 0); + +extern var uniforms: extern struct { + multiply_color: @Vector(4, f32), +} addrspace(.uniform); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.binding(&uniforms, 3, 0); + + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = uniforms.multiply_color * tex0.texture(tex_coord_in); +} diff --git a/gpu_examples/src/basic-compute.zig b/gpu_examples/src/basic-compute.zig new file mode 100644 index 0000000..0c879c5 --- /dev/null +++ b/gpu_examples/src/basic-compute.zig @@ -0,0 +1,316 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const comp_shader_source = @embedFile("fillTexture.comp"); +const comp_shader_name = "Fill Texture"; +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("texturedQuad.frag"); +const frag_shader_name = "Textured Quad"; + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + draw_pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, +}; + +fn loadComputeShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, +) !struct { pipeline: sdl3.gpu.ComputePipeline, metadata: sdl3.shadercross.ComputePipelineMetadata } { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = .compute, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectComputeSpirv(spirv_code); + return .{ .pipeline = try sdl3.shadercross.compileComputePipelineFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = .compute, + }, spirv_metadata), .metadata = spirv_metadata }; +} + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Basic Compute", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const draw_pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(draw_pipeline); + + // Prepare texture and sampler. + const texture = try device.createTexture(.{ + .format = .r8g8b8a8_unorm, + .width = window_width, + .height = window_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .compute_storage_write = true, .sampler = true }, + }); + errdefer device.releaseTexture(texture); + const sampler = try device.createSampler(.{ + .address_mode_u = .repeat, + .address_mode_v = .repeat, + }); + errdefer device.releaseSampler(sampler); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = vertices_bytes.len, + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped, vertices_bytes); + } + + // Create compute pipeline. + const fill_texture_pipeline_result = try loadComputeShader(device, comp_shader_name, comp_shader_source); + defer device.releaseComputePipeline(fill_texture_pipeline_result.pipeline); + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + } + + // Create fill texture. + { + const compute_pass = cmd_buf.beginComputePass(&.{ + .{ .texture = texture }, + }, &.{}); + defer compute_pass.end(); + compute_pass.bindPipeline(fill_texture_pipeline_result.pipeline); + compute_pass.dispatch(window_width / fill_texture_pipeline_result.metadata.threadcount_x, window_height / fill_texture_pipeline_result.metadata.threadcount_y, fill_texture_pipeline_result.metadata.threadcount_z); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .draw_pipeline = draw_pipeline, + .vertex_buffer = vertex_buffer, + .texture = texture, + .sampler = sampler, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.draw_pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }, + ); + render_pass.drawPrimitives(6, 1, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.draw_pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/basic-stencil.zig b/gpu_examples/src/basic-stencil.zig new file mode 100644 index 0000000..ca2ece0 --- /dev/null +++ b/gpu_examples/src/basic-stencil.zig @@ -0,0 +1,341 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("positionColor.vert"); +const vert_shader_name = "Position Color"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const vertices = [_]PositionColorVertex{ + .{ .position = .{ -0.5, -0.5, 0 }, .color = .{ 255, 255, 0, 255 } }, + .{ .position = .{ 0.5, -0.5, 0 }, .color = .{ 255, 255, 0, 255 } }, + .{ .position = .{ 0, 0.5, 0 }, .color = .{ 255, 255, 0, 255 } }, + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 0, 0, 255, 255 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + masker_pipeline: sdl3.gpu.GraphicsPipeline, + maskee_pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + depth_stencil_texture: sdl3.gpu.Texture, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Basic Stencil", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Get format for the depth stencil. + const depth_stencil_format: sdl3.gpu.TextureFormat = if (device.textureSupportsFormat(.depth24_unorm_s8_uint, .two_dimensional, .{ .depth_stencil_target = true })) + .depth24_unorm_s8_uint + else if (device.textureSupportsFormat(.depth32_float_s8_uint, .two_dimensional, .{ .depth_stencil_target = true })) + .depth32_float_s8_uint + else { + try sdl3.errors.set("Stencil formats not supported"); + unreachable; + }; + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + var pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + .depth_stencil_format = depth_stencil_format, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + .depth_stencil_state = .{ + .enable_stencil_test = true, + .front_stencil_state = .{ + .compare = .never, + .fail = .replace, + .pass = .keep, + .depth_fail = .keep, + }, + .back_stencil_state = .{ + .compare = .never, + .fail = .replace, + .pass = .keep, + .depth_fail = .keep, + }, + .write_mask = 0xff, + }, + .rasterizer_state = .{ + .cull_mode = .none, + .fill_mode = .fill, + .front_face = .counter_clockwise, + }, + }; + const masker_pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(masker_pipeline); + + // Setup maskee state. + pipeline_create_info.depth_stencil_state = .{ + .enable_stencil_test = true, + .front_stencil_state = .{ + .compare = .equal, + .fail = .keep, + .pass = .keep, + .depth_fail = .keep, + }, + .back_stencil_state = .{ + .compare = .never, + .fail = .keep, + .pass = .keep, + .depth_fail = .keep, + }, + .compare_mask = 0xff, + .write_mask = 0, + }; + const maskee_pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(maskee_pipeline); + + // Create depth stencil texture. + const window_size = try window.getSizeInPixels(); + const depth_stencil_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .width = @intCast(window_size.width), + .height = @intCast(window_size.height), + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = .no_multisampling, + .format = depth_stencil_format, + .usage = .{ .depth_stencil_target = true }, + }); + errdefer device.releaseTexture(depth_stencil_texture); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = vertices_bytes.len, + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped, vertices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .masker_pipeline = masker_pipeline, + .maskee_pipeline = maskee_pipeline, + .vertex_buffer = vertex_buffer, + .depth_stencil_texture = depth_stencil_texture, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass( + &.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, + .{ + .texture = app_state.depth_stencil_texture, + .clear_depth = 0, + .clear_stencil = 0, + .load = .clear, + .store = .do_not_care, + .stencil_load = .clear, + .stencil_store = .store, + .cycle = false, + }, + ); + defer render_pass.end(); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + + // Draw masker primitives. + render_pass.setStencilReference(1); + render_pass.bindGraphicsPipeline(app_state.masker_pipeline); + render_pass.drawPrimitives(3, 1, 0, 0); + + // Draw maskee primitives. + render_pass.setStencilReference(0); + render_pass.bindGraphicsPipeline(app_state.maskee_pipeline); + render_pass.drawPrimitives(3, 1, 3, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseTexture(val.depth_stencil_texture); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.masker_pipeline); + val.device.releaseGraphicsPipeline(val.maskee_pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/basic-triangle.zig b/gpu_examples/src/basic-triangle.zig new file mode 100644 index 0000000..d39c72f --- /dev/null +++ b/gpu_examples/src/basic-triangle.zig @@ -0,0 +1,213 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("rawTriangle.vert"); +const vert_shader_name = "Raw Triangle"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; +const small_viewport = sdl3.gpu.Viewport{ + .region = .{ .x = 100, .y = 120, .w = 320, .h = 240 }, + .min_depth = 0.1, + .max_depth = 1.0, +}; +const scissor_rect = sdl3.rect.IRect{ .x = 320, .y = 240, .w = 320, .h = 240 }; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + fill_pipeline: sdl3.gpu.GraphicsPipeline, + line_pipeline: sdl3.gpu.GraphicsPipeline, + use_wireframe_mode: bool = false, + use_small_viewport: bool = false, + use_scissor_rect: bool = false, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Basic Triangle", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + var pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + }; + const fill_pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(fill_pipeline); + pipeline_create_info.rasterizer_state.fill_mode = .line; + const line_pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(line_pipeline); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .line_pipeline = line_pipeline, + .fill_pipeline = fill_pipeline, + }; + + // Finish setup. + app_state.* = state; + try sdl3.log.log("Press left to toggle wireframe", .{}); + try sdl3.log.log("Press down to toggle small viewport", .{}); + try sdl3.log.log("Press right to toggle scissor rect", .{}); + try sdl3.log.log( + "State: {{Wireframe: {any}, SmallViewport: {any}, ScissorRect: {any}}}", + .{ state.use_wireframe_mode, state.use_small_viewport, state.use_scissor_rect }, + ); + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(if (app_state.use_wireframe_mode) app_state.line_pipeline else app_state.fill_pipeline); + if (app_state.use_small_viewport) + render_pass.setViewport(small_viewport); + if (app_state.use_scissor_rect) + render_pass.setScissor(scissor_rect); + render_pass.drawPrimitives(3, 1, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + app_state.use_wireframe_mode = !app_state.use_wireframe_mode; + changed = true; + }, + .down => { + app_state.use_small_viewport = !app_state.use_small_viewport; + changed = true; + }, + .right => { + app_state.use_scissor_rect = !app_state.use_scissor_rect; + changed = true; + }, + else => {}, + }; + if (changed) { + try sdl3.log.log( + "State: {{Wireframe: {any}, SmallViewport: {any}, ScissorRect: {any}}}", + .{ app_state.use_wireframe_mode, app_state.use_small_viewport, app_state.use_scissor_rect }, + ); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseGraphicsPipeline(val.fill_pipeline); + val.device.releaseGraphicsPipeline(val.line_pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/basic-vertex-buffer.zig b/gpu_examples/src/basic-vertex-buffer.zig new file mode 100644 index 0000000..fa70f70 --- /dev/null +++ b/gpu_examples/src/basic-vertex-buffer.zig @@ -0,0 +1,245 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("positionColor.vert"); +const vert_shader_name = "Position Color"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const vertices = [_]PositionColorVertex{ + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 0, 0, 255, 255 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Basic Vertex Buffer", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = vertices_bytes.len, + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped, vertices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.drawPrimitives(3, 1, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/blit-2d-array.zig b/gpu_examples/src/blit-2d-array.zig new file mode 100644 index 0000000..a001eb3 --- /dev/null +++ b/gpu_examples/src/blit-2d-array.zig @@ -0,0 +1,424 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("texturedQuadArray.frag"); +const frag_shader_name = "Textured Quad Array"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); +const ravioli_inverted_bmp = @embedFile("images/ravioliInverted.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 0, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 0, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 1 } }, + + .{ .position = .{ 0, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ 0, -1, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + source_texture: sdl3.gpu.Texture, + destination_texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Blit 2d Array", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the images. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + const image_data_inverted = try loadImage(ravioli_inverted_bmp); + defer image_data_inverted.deinit(); + const image_inverted_bytes = image_data_inverted.getPixels().?[0 .. image_data_inverted.getWidth() * image_data_inverted.getHeight() * @sizeOf(u8) * 4]; + std.debug.assert(image_data.getWidth() == image_data_inverted.getWidth()); + std.debug.assert(image_data.getHeight() == image_data_inverted.getHeight()); + + // Create textures. + const source_texture = try device.createTexture(.{ + .texture_type = .two_dimensional_array, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 2, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Textures Source" }, + }); + errdefer device.releaseTexture(source_texture); + const destination_textures = try device.createTexture(.{ + .texture_type = .two_dimensional_array, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth() / 2), + .height = @intCast(image_data.getHeight() / 2), + .layer_count_or_depth = 2, + .num_levels = 1, + .usage = .{ .sampler = true, .color_target = true }, + .props = .{ .name = "Ravioli Textures Destination" }, + }); + errdefer device.releaseTexture(destination_textures); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer_image_inverted_data_off = transfer_buffer_image_data_off + image_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len + image_inverted_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_data_off .. transfer_buffer_image_data_off + image_bytes.len], image_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_inverted_data_off .. transfer_buffer_image_inverted_data_off + image_inverted_bytes.len], image_inverted_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_image_data_off, + }, + .{ + .texture = source_texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = @intCast(transfer_buffer_image_inverted_data_off), + }, + .{ + .texture = source_texture, + .width = @intCast(image_data_inverted.getWidth()), + .height = @intCast(image_data_inverted.getHeight()), + .depth = 1, + .layer = 1, + }, + false, + ); + } + cmd_buf.blitTexture(.{ + .source = .{ + .texture = source_texture, + .region = .{ .x = 0, .y = 0, .w = @intCast(image_data.getWidth()), .h = @intCast(image_data.getHeight()) }, + }, + .destination = .{ + .texture = destination_textures, + .region = .{ .x = 0, .y = 0, .w = @intCast(image_data.getWidth() / 2), .h = @intCast(image_data.getHeight() / 2) }, + }, + .load_op = .do_not_care, + .filter = .linear, + }); + cmd_buf.blitTexture(.{ + .source = .{ + .texture = source_texture, + .region = .{ .x = 0, .y = 0, .w = @intCast(image_data.getWidth()), .h = @intCast(image_data.getHeight()) }, + .layer_or_depth_plane = 1, + }, + .destination = .{ + .texture = destination_textures, + .region = .{ .x = 0, .y = 0, .w = @intCast(image_data.getWidth() / 2), .h = @intCast(image_data.getHeight() / 2) }, + .layer_or_depth_plane = 1, + }, + .load_op = .load, + .filter = .linear, + }); + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .source_texture = source_texture, + .destination_texture = destination_textures, + .sampler = sampler, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.source_texture, .sampler = app_state.sampler }, + }, + ); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.destination_texture, .sampler = app_state.sampler }, + }, + ); + render_pass.drawIndexedPrimitives(6, 1, 0, 4, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.destination_texture); + val.device.releaseTexture(val.source_texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/blit-cube.zig b/gpu_examples/src/blit-cube.zig new file mode 100644 index 0000000..79bd056 --- /dev/null +++ b/gpu_examples/src/blit-cube.zig @@ -0,0 +1,513 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("skybox.vert"); +const vert_shader_name = "Skybox"; +const frag_shader_source = @embedFile("skybox.frag"); +const frag_shader_name = "Skybox"; + +const window_width = 640; +const window_height = 480; + +const tex_size = 32; + +const PositionVertex = packed struct { + position: @Vector(3, f32), +}; + +const vertices = [_]PositionVertex{ + .{ .position = .{ -10, -10, -10 } }, + .{ .position = .{ 10, -10, -10 } }, + .{ .position = .{ 10, 10, -10 } }, + .{ .position = .{ -10, 10, -10 } }, + + .{ .position = .{ -10, -10, 10 } }, + .{ .position = .{ 10, -10, 10 } }, + .{ .position = .{ 10, 10, 10 } }, + .{ .position = .{ -10, 10, 10 } }, + + .{ .position = .{ -10, -10, -10 } }, + .{ .position = .{ -10, 10, -10 } }, + .{ .position = .{ -10, 10, 10 } }, + .{ .position = .{ -10, -10, 10 } }, + + .{ .position = .{ 10, -10, -10 } }, + .{ .position = .{ 10, 10, -10 } }, + .{ .position = .{ 10, 10, 10 } }, + .{ .position = .{ 10, -10, 10 } }, + + .{ .position = .{ -10, -10, -10 } }, + .{ .position = .{ -10, -10, 10 } }, + .{ .position = .{ 10, -10, 10 } }, + .{ .position = .{ 10, -10, -10 } }, + + .{ .position = .{ -10, 10, -10 } }, + .{ .position = .{ -10, 10, 10 } }, + .{ .position = .{ 10, 10, 10 } }, + .{ .position = .{ 10, 10, -10 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ 0, 1, 2, 0, 2, 3, 6, 5, 4, 7, 6, 4, 8, 9, 10, 8, 10, 11, 14, 13, 12, 15, 14, 12, 16, 17, 18, 16, 18, 19, 22, 21, 20, 23, 22, 20 }; +const indices_bytes = std.mem.asBytes(&indices); + +const images = [_][]const u8{ + @embedFile("images/cube0.bmp"), + @embedFile("images/cube1.bmp"), + @embedFile("images/cube2.bmp"), + @embedFile("images/cube3.bmp"), + @embedFile("images/cube4.bmp"), + @embedFile("images/cube5.bmp"), +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + source_texture: sdl3.gpu.Texture, + destination_texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, + cam_pos: @Vector(3, f32) = .{ 0, 0, 4 }, +}; + +const Mat4 = packed struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn lookAt(camera_position: @Vector(3, f32), camera_target: @Vector(3, f32), camera_up_vector: @Vector(3, f32)) Mat4 { + const target_to_position = camera_position - camera_target; + const a = (Vec3{ .data = target_to_position }).normalize(); + const b = (Vec3{ .data = camera_up_vector }).cross(a).normalize(); + const c = a.cross(b); + return .{ + .c0 = .{ b.data[0], c.data[0], a.data[0], 0 }, + .c1 = .{ b.data[1], c.data[1], a.data[1], 0 }, + .c2 = .{ b.data[2], c.data[2], a.data[2], 0 }, + .c3 = .{ -b.dot(.{ .data = camera_position }), -c.dot(.{ .data = camera_position }), -a.dot(.{ .data = camera_position }), 1 }, + }; + } + + pub fn mul(a: Mat4, b: Mat4) Mat4 { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ + .c0 = .{ @reduce(.Add, ar0 * b.c0), @reduce(.Add, ar1 * b.c0), @reduce(.Add, ar2 * b.c0), @reduce(.Add, ar3 * b.c0) }, + .c1 = .{ @reduce(.Add, ar0 * b.c1), @reduce(.Add, ar1 * b.c1), @reduce(.Add, ar2 * b.c1), @reduce(.Add, ar3 * b.c1) }, + .c2 = .{ @reduce(.Add, ar0 * b.c2), @reduce(.Add, ar1 * b.c2), @reduce(.Add, ar2 * b.c2), @reduce(.Add, ar3 * b.c2) }, + .c3 = .{ @reduce(.Add, ar0 * b.c3), @reduce(.Add, ar1 * b.c3), @reduce(.Add, ar2 * b.c3), @reduce(.Add, ar3 * b.c3) }, + }; + } + + pub fn perspectiveFieldOfView(field_of_view: f32, aspect_ratio: f32, near_plane_distance: f32, far_plane_distance: f32) Mat4 { + const num = 1 / @tan(field_of_view * 0.5); + return .{ + .c0 = .{ num / aspect_ratio, 0, 0, 0 }, + .c1 = .{ 0, num, 0, 0 }, + .c2 = .{ 0, 0, far_plane_distance / (near_plane_distance - far_plane_distance), -1 }, + .c3 = .{ 0, 0, (near_plane_distance * far_plane_distance) / (near_plane_distance - far_plane_distance), 0 }, + }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +const Vec3 = struct { + data: @Vector(3, f32), + + pub fn cross(a: Vec3, b: Vec3) Vec3 { + return .{ .data = .{ + a.data[1] * b.data[2] - b.data[1] * a.data[2], + -(a.data[0] * b.data[2] - b.data[0] * a.data[2]), + a.data[0] * b.data[1] - b.data[0] * a.data[1], + } }; + } + + pub fn dot(a: Vec3, b: Vec3) f32 { + const mul = a.data * b.data; + return @reduce(.Add, mul); + } + + pub fn normalize(self: Vec3) Vec3 { + const mag = @sqrt(self.dot(self)); + return .{ .data = self.data / @as(@Vector(3, f32), @splat(mag)) }; + } +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Blit Cube", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionVertex, "position"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Create textures. + const source_texture = try device.createTexture(.{ + .texture_type = .cube, + .format = .r8g8b8a8_unorm, + .width = tex_size, + .height = tex_size, + .layer_count_or_depth = images.len, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Cubemap Textures Source" }, + }); + errdefer device.releaseTexture(source_texture); + const destination_texture = try device.createTexture(.{ + .texture_type = .cube, + .format = .r8g8b8a8_unorm, + .width = tex_size, + .height = tex_size, + .layer_count_or_depth = images.len, + .num_levels = 1, + .usage = .{ .sampler = true, .color_target = true }, + .props = .{ .name = "Cubemap Textures Destination" }, + }); + errdefer device.releaseTexture(destination_texture); + + // Load images. + const cube0 = try loadImage(images[0]); + defer cube0.deinit(); + const cube1 = try loadImage(images[1]); + defer cube1.deinit(); + const cube2 = try loadImage(images[2]); + defer cube2.deinit(); + const cube3 = try loadImage(images[3]); + defer cube3.deinit(); + const cube4 = try loadImage(images[4]); + defer cube4.deinit(); + const cube5 = try loadImage(images[5]); + defer cube5.deinit(); + const cubes = [_]sdl3.surface.Surface{ cube0, cube1, cube2, cube3, cube4, cube5 }; + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_images_off = transfer_buffer_index_data_off + indices_bytes.len; + const image_size = cube0.getWidth() * cube0.getHeight() * @sizeOf(u8) * 4; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_size * images.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + for (0..images.len) |ind| { + @memcpy( + transfer_buffer_mapped[transfer_images_off + image_size * ind .. transfer_images_off + image_size * (ind + 1)], + cubes[ind].getPixels().?[0 .. cubes[ind].getWidth() * cubes[ind].getHeight() * @sizeOf(u8) * 4], + ); + } + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + for (0..images.len) |ind| { + copy_pass.uploadToTexture(.{ + .transfer_buffer = transfer_buffer, + .offset = @intCast(transfer_images_off + image_size * ind), + }, .{ + .texture = source_texture, + .layer = @intCast(ind), + .width = tex_size, + .height = tex_size, + .depth = 1, + }, false); + } + } + + // Blit to destination texture. + // This serves no real purpose other than demonstrating cube->cube blits are possible! + for (0..images.len) |ind| + cmd_buf.blitTexture(.{ + .source = .{ + .texture = source_texture, + .region = .{ .x = 0, .y = 0, .w = tex_size, .h = tex_size }, + .layer_or_depth_plane = @intCast(ind), + }, + .destination = .{ + .texture = destination_texture, + .region = .{ .x = 0, .y = 0, .w = tex_size, .h = tex_size }, + .layer_or_depth_plane = @intCast(ind), + }, + .load_op = .do_not_care, + .filter = .linear, + }); + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .source_texture = source_texture, + .destination_texture = destination_texture, + .sampler = sampler, + }; + + // Finish setup. + try sdl3.log.log("Press left/right to view the opposite direction", .{}); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.destination_texture, .sampler = app_state.sampler }, + }, + ); + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.perspectiveFieldOfView( + 75.0 * std.math.pi / 180.0, + @as(comptime_float, window_width) / @as(comptime_float, window_height), + 0.01, + 100, + ).mul(Mat4.lookAt( + app_state.cam_pos, + .{ 0, 0, 0 }, + .{ 0, 1, 0 }, + )))); + render_pass.drawIndexedPrimitives(36, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + if (key.key) |val| switch (val) { + .left, .right => { + app_state.cam_pos[2] = -app_state.cam_pos[2]; + }, + else => {}, + }; + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.destination_texture); + val.device.releaseTexture(val.source_texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/blit-mirror.zig b/gpu_examples/src/blit-mirror.zig new file mode 100644 index 0000000..d1954d1 --- /dev/null +++ b/gpu_examples/src/blit-mirror.zig @@ -0,0 +1,237 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const window_width = 640; +const window_height = 480; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + texture: sdl3.gpu.Texture, + texture_width: u32, + texture_height: u32, +}; + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Blit Mirror", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[0..image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .texture = texture, + .texture_width = @intCast(image_data.getWidth()), + .texture_height = @intCast(image_data.getHeight()), + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + { + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + } + + // Normal. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .do_not_care, + .filter = .nearest, + }); + + // Flip horizontal. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = swapchain_texture.width / 2, .y = 0, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .do_not_care, + .filter = .nearest, + .flip_mode = .{ .horizontal = true }, + }); + + // Flip vertical. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = swapchain_texture.height / 2, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .do_not_care, + .filter = .nearest, + .flip_mode = .{ .vertical = true }, + }); + + // Flip horizontal and vertical. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = swapchain_texture.width / 2, .y = swapchain_texture.height / 2, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .do_not_care, + .filter = .nearest, + .flip_mode = .{ .horizontal = true, .vertical = true }, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseTexture(val.texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/clear-3d-slice.zig b/gpu_examples/src/clear-3d-slice.zig new file mode 100644 index 0000000..b95d9a3 --- /dev/null +++ b/gpu_examples/src/clear-3d-slice.zig @@ -0,0 +1,175 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const target_width = 64; +const target_height = 64; + +const window_width = 640; +const window_height = 480; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + texture_3d: sdl3.gpu.Texture, +}; + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Clear 3d Slice", window_width, window_height, .{ .resizable = true }); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare texture. + const texture_3d = try device.createTexture(.{ + .texture_type = .three_dimensional, + .format = try device.getSwapchainTextureFormat(window), + .width = target_width, + .height = target_height, + .layer_count_or_depth = 4, + .num_levels = 1, + .usage = .{ .color_target = true, .sampler = true }, + }); + errdefer device.releaseTexture(texture_3d); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .texture_3d = texture_3d, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + const render_pass1 = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = app_state.texture_3d, + .clear_color = .{ .r = 1, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + .cycle = true, + .layer_or_depth_plane = 0, + }, + }, null); + render_pass1.end(); + const render_pass2 = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = app_state.texture_3d, + .clear_color = .{ .r = 0, .g = 1, .b = 0, .a = 1 }, + .load = .clear, + .cycle = false, + .layer_or_depth_plane = 1, + }, + }, null); + render_pass2.end(); + const render_pass3 = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = app_state.texture_3d, + .clear_color = .{ .r = 0, .g = 0, .b = 1, .a = 1 }, + .load = .clear, + .cycle = false, + .layer_or_depth_plane = 2, + }, + }, null); + render_pass3.end(); + const render_pass4 = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = app_state.texture_3d, + .clear_color = .{ .r = 1, .g = 0, .b = 1, .a = 1 }, + .load = .clear, + .cycle = false, + .layer_or_depth_plane = 3, + }, + }, null); + render_pass4.end(); + + for (0..4) |i| { + const dst_x = (i % 2) * (swapchain_texture.width / 2); + const dst_y = if (i > 1) swapchain_texture.height / 2 else 0; + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture_3d, + .layer_or_depth_plane = @intCast(i), + .region = .{ .x = 0, .y = 0, .w = target_width, .h = target_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = @intCast(dst_x), .y = @intCast(dst_y), .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .load, + .filter = .nearest, + .flip_mode = .{}, + }); + } + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseTexture(val.texture_3d); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/clear-screen-multi-window.zig b/gpu_examples/src/clear-screen-multi-window.zig new file mode 100644 index 0000000..d0089c6 --- /dev/null +++ b/gpu_examples/src/clear-screen-multi-window.zig @@ -0,0 +1,127 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const AppState = struct { + device: sdl3.gpu.Device, + window1: sdl3.video.Window, + window2: sdl3.video.Window, +}; + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo windows. + const window1 = try sdl3.video.Window.init("Window 1", 640, 480, .{}); + errdefer window1.deinit(); + const window2 = try sdl3.video.Window.init("Window 2", 640, 480, .{}); + errdefer window2.deinit(); + try device.claimWindow(window1); + try device.claimWindow(window2); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window1 = window1, + .window2 = window2, + }; + + // Finish setup. + app_state.* = state; + try sdl3.log.log("Press the escape key to quit", .{}); + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture1 = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window1); + if (swapchain_texture1.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0.3, .g = 0.4, .b = 0.5, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + } + const swapchain_texture2 = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window2); + if (swapchain_texture2.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 1, .g = 0.5, .b = 0.6, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + .key_down => |key| return if (key.key == .escape) .success else .run, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseWindow(val.window1); + val.device.releaseWindow(val.window2); + val.window1.deinit(); + val.window2.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/clear-screen.zig b/gpu_examples/src/clear-screen.zig new file mode 100644 index 0000000..44ab5e7 --- /dev/null +++ b/gpu_examples/src/clear-screen.zig @@ -0,0 +1,105 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, +}; + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Clear Screen", 640, 480, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0.3, .g = 0.3, .b = 0.5, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/compute-sampler.zig b/gpu_examples/src/compute-sampler.zig new file mode 100644 index 0000000..7658744 --- /dev/null +++ b/gpu_examples/src/compute-sampler.zig @@ -0,0 +1,348 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const comp_shader_source = @embedFile("texturedQuad.comp"); +const comp_shader_name = "Textured Quad"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const window_width = 640; +const window_height = 480; + +const sampler_names = [_][]const u8{ + "PointClamp", + "PointWrap", + "LinearClamp", + "LinearWrap", + "AnisotropicClamp", + "AnisotropicWrap", +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.ComputePipeline, + pipeline_metadata: sdl3.shadercross.ComputePipelineMetadata, + texture: sdl3.gpu.Texture, + write_texture: sdl3.gpu.Texture, + samplers: [sampler_names.len]sdl3.gpu.Sampler, + curr_sampler: usize = 0, +}; + +fn loadComputeShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, +) !struct { pipeline: sdl3.gpu.ComputePipeline, metadata: sdl3.shadercross.ComputePipelineMetadata } { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = .compute, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectComputeSpirv(spirv_code); + return .{ .pipeline = try sdl3.shadercross.compileComputePipelineFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = .compute, + }, spirv_metadata), .metadata = spirv_metadata }; +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Compute Sampler", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create textures. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + const write_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = window_width, + .height = window_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .compute_storage_write = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(write_texture); + + // Create pipeline. + const pipeline = try loadComputeShader(device, comp_shader_name, comp_shader_source); + errdefer device.releaseComputePipeline(pipeline.pipeline); + + // Create samplers. + var samplers: [sampler_names.len]sdl3.gpu.Sampler = undefined; + samplers[0] = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + errdefer device.releaseSampler(samplers[0]); + samplers[1] = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + }); + errdefer device.releaseSampler(samplers[1]); + samplers[2] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + errdefer device.releaseSampler(samplers[2]); + samplers[3] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + }); + errdefer device.releaseSampler(samplers[3]); + samplers[4] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + .max_anisotropy = 4, + }); + errdefer device.releaseSampler(samplers[4]); + samplers[5] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + .max_anisotropy = 4, + }); + errdefer device.releaseSampler(samplers[5]); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[0..image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline.pipeline, + .pipeline_metadata = pipeline.metadata, + .texture = texture, + .write_texture = write_texture, + .samplers = samplers, + }; + + // Finish setup. + try sdl3.log.log("Press left/right to switch between sampler states", .{}); + try sdl3.log.log("Sampler state: {s}", .{sampler_names[state.curr_sampler]}); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a compute pass if the swapchain texture is available. + { + const compute_pass = cmd_buf.beginComputePass( + &.{ + .{ + .texture = app_state.write_texture, + .cycle = true, + }, + }, + &.{}, + ); + defer compute_pass.end(); + compute_pass.bindPipeline(app_state.pipeline); + compute_pass.bindSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.samplers[app_state.curr_sampler] }, + }, + ); + cmd_buf.pushComputeUniformData(0, std.mem.asBytes(&@as(f32, 0.25))); + compute_pass.dispatch( + swapchain_texture.width / app_state.pipeline_metadata.threadcount_x, + swapchain_texture.height / app_state.pipeline_metadata.threadcount_y, + app_state.pipeline_metadata.threadcount_z, + ); + } + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.write_texture, + .region = .{ .x = 0, .y = 0, .w = window_width, .h = window_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width, .h = swapchain_texture.height }, + }, + .load_op = .do_not_care, + .filter = .nearest, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + if (app_state.curr_sampler == 0) { + app_state.curr_sampler = sampler_names.len - 1; + } else app_state.curr_sampler -= 1; + changed = true; + }, + .right => { + if (app_state.curr_sampler >= sampler_names.len - 1) { + app_state.curr_sampler = 0; + } else app_state.curr_sampler += 1; + changed = true; + }, + else => {}, + }; + if (changed) { + try sdl3.log.log("Sampler state: {s}", .{sampler_names[app_state.curr_sampler]}); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + for (val.samplers) |sampler| + val.device.releaseSampler(sampler); + val.device.releaseTexture(val.write_texture); + val.device.releaseTexture(val.texture); + val.device.releaseComputePipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/compute-sprite-batch.zig b/gpu_examples/src/compute-sprite-batch.zig new file mode 100644 index 0000000..fc1fcde --- /dev/null +++ b/gpu_examples/src/compute-sprite-batch.zig @@ -0,0 +1,553 @@ +// This example is slightly inferior in terms of performance compared to `pull-sprite-batch.zig`. +// Despite that, it is included as an example of a compute to graphics workflow. +// +// ALSO NOTE: DUE TO A MISCOMPILATION, THIS EXAMPLE MUST BE BUILT USING LLVM! SO USING `-Doptimize=ReleaseFast` SHOULD FIX THE BLACK SCREEN! + +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuadColorWithMatrix.vert"); +const vert_shader_name = "Textured Quad Color With Matrix"; +const frag_shader_source = @embedFile("texturedQuadColor.frag"); +const frag_shader_name = "Textured Quad Color"; +const comp_shader_source = @embedFile("spriteBatch.comp"); +const comp_shader_name = "Sprite Batch"; + +const ravioli_atlas_bmp = @embedFile("images/ravioliAtlas.bmp"); + +const window_width = 640; +const window_height = 480; + +const sprite_size = 32; +const sprite_count = 8192; + +const PositionTextureColorVertex = packed struct { + position: @Vector(4, f32), + tex_coord: @Vector(2, f32), + _: @Vector(2, f32) = .{ 0, 0 }, // Padding for STD140 requirements. + color: @Vector(4, f32), +}; + +const ComputeSpriteInstance = packed struct { + position: @Vector(3, f32), + rotation: f32, + size: @Vector(2, f32), + _: @Vector(2, f32) = .{ 0, 0 }, // Padding for STD140 requirements. + texture: @Vector(4, f32), + color: @Vector(4, f32), +}; + +const ravioli_coords = [_]@Vector(2, f32){ + .{ + 0, + 0, + }, + .{ + 0.5, + 0, + }, + .{ + 0, + 0.5, + }, + .{ + 0.5, + 0.5, + }, +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + compute_pipeline: sdl3.gpu.ComputePipeline, + compute_pipeline_metadata: sdl3.shadercross.ComputePipelineMetadata, + render_pipeline: sdl3.gpu.GraphicsPipeline, + sampler: sdl3.gpu.Sampler, + texture: sdl3.gpu.Texture, + sprite_compute_transfer_buffer: sdl3.gpu.TransferBuffer, + sprite_compute_buffer: sdl3.gpu.Buffer, + sprite_vertex_buffer: sdl3.gpu.Buffer, + sprite_index_buffer: sdl3.gpu.Buffer, + prng: std.Random.DefaultPrng, +}; + +const Mat4 = packed struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn orthographicOffCenter( + left: f32, + right: f32, + bottom: f32, + top: f32, + z_near: f32, + z_far: f32, + ) Mat4 { + return .{ + .c0 = .{ 2 / (right - left), 0, 0, 0 }, + .c1 = .{ 0, 2 / (top - bottom), 0, 0 }, + .c2 = .{ 0, 0, 1 / (z_near - z_far), 0 }, + .c3 = .{ (left + right) / (left - right), (top + bottom) / (bottom - top), z_near / (z_near - z_far), 1 }, + }; + } +}; + +fn loadComputeShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, +) !struct { pipeline: sdl3.gpu.ComputePipeline, metadata: sdl3.shadercross.ComputePipelineMetadata } { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = .compute, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectComputeSpirv(spirv_code); + return .{ .pipeline = try sdl3.shadercross.compileComputePipelineFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = .compute, + }, spirv_metadata), .metadata = spirv_metadata }; +} + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Compute Sprite Batch", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Set present mode. + var present_mode = sdl3.gpu.PresentMode.vsync; + if (device.windowSupportsPresentMode(window, .immediate)) { + present_mode = .immediate; + } else if (device.windowSupportsPresentMode(window, .immediate)) { + present_mode = .mailbox; + } + try device.setSwapchainParameters(window, .sdr, present_mode); + + // Create render pipeline. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + .blend_state = .{ + .enable_blend = true, + .color_blend = .add, + .alpha_blend = .add, + .source_color = .src_alpha, + .destination_color = .one_minus_src_alpha, + .source_alpha = .src_alpha, + .destination_alpha = .one_minus_src_alpha, + }, + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x4, + .offset = @offsetOf(PositionTextureColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureColorVertex, "tex_coord"), + }, + .{ + .location = 2, + .buffer_slot = 0, + .format = .f32x4, + .offset = @offsetOf(PositionTextureColorVertex, "color"), + }, + }, + }, + }; + const render_pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(render_pipeline); + + // Create compute pipeline. + const compute_pipeline = try loadComputeShader(device, comp_shader_name, comp_shader_source); + errdefer device.releaseComputePipeline(compute_pipeline.pipeline); + + // Load the image. + const image_data = try loadImage(ravioli_atlas_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture and sampler. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture Atlas" }, + }); + errdefer device.releaseTexture(texture); + + // Create samplers. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + errdefer device.releaseSampler(sampler); + + // Setup texture transfer buffer. + const texture_transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(image_bytes.len), + }); + defer device.releaseTransferBuffer(texture_transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(texture_transfer_buffer, false); + defer device.unmapTransferBuffer(texture_transfer_buffer); + @memcpy(transfer_buffer_mapped[0..image_bytes.len], image_bytes); + } + + // Create sprite resources. + const sprite_compute_transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @sizeOf(ComputeSpriteInstance) * sprite_count, + }); + errdefer device.releaseTransferBuffer(sprite_compute_transfer_buffer); + const sprite_compute_buffer = try device.createBuffer(.{ + .usage = .{ .compute_storage_read = true }, + .size = @sizeOf(ComputeSpriteInstance) * sprite_count, + }); + errdefer device.releaseBuffer(sprite_compute_buffer); + const sprite_vertex_buffer = try device.createBuffer(.{ + .usage = .{ .compute_storage_write = true, .vertex = true }, + .size = @sizeOf(PositionTextureColorVertex) * sprite_count * 4, + }); + errdefer device.releaseBuffer(sprite_vertex_buffer); + const sprite_index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = @sizeOf(u32) * sprite_count * 6, + }); + errdefer device.releaseBuffer(sprite_index_buffer); + + // Setup index transfer buffer. + const index_transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @sizeOf(u32) * sprite_count * 6, + }); + defer device.releaseTransferBuffer(index_transfer_buffer); + { + const transfer_buffer_mapped = @as([*]u32, @ptrCast(@alignCast(try device.mapTransferBuffer(index_transfer_buffer, false)))); + defer device.unmapTransferBuffer(index_transfer_buffer); + for (0..sprite_count) |sprite_ind| { + const index_base = sprite_ind * 6; + const val_base: u32 = @intCast(sprite_ind * 4); + transfer_buffer_mapped[index_base + 0] = val_base + 0; + transfer_buffer_mapped[index_base + 1] = val_base + 1; + transfer_buffer_mapped[index_base + 2] = val_base + 2; + transfer_buffer_mapped[index_base + 3] = val_base + 3; + transfer_buffer_mapped[index_base + 4] = val_base + 2; + transfer_buffer_mapped[index_base + 5] = val_base + 1; + } + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = texture_transfer_buffer, + .offset = 0, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = index_transfer_buffer, + .offset = 0, + }, + .{ + .buffer = sprite_index_buffer, + .offset = 0, + .size = @sizeOf(u32) * sprite_count * 6, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .compute_pipeline = compute_pipeline.pipeline, + .compute_pipeline_metadata = compute_pipeline.metadata, + .render_pipeline = render_pipeline, + .sprite_compute_transfer_buffer = sprite_compute_transfer_buffer, + .sprite_compute_buffer = sprite_compute_buffer, + .sprite_vertex_buffer = sprite_vertex_buffer, + .sprite_index_buffer = sprite_index_buffer, + .texture = texture, + .sampler = sampler, + .prng = .init(blk: { + var seed: u64 = undefined; + try std.posix.getrandom(std.mem.asBytes(&seed)); + break :blk seed; + }), + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Build sprite instance buffer. + { + const sprite_data = @as([*]ComputeSpriteInstance, @ptrCast(@alignCast(try app_state.device.mapTransferBuffer(app_state.sprite_compute_transfer_buffer, true)))); + defer app_state.device.unmapTransferBuffer(app_state.sprite_compute_transfer_buffer); + const random = app_state.prng.random(); + for (0..sprite_count) |sprite_ind| { + const ravioli_ind = random.int(u2); + sprite_data[sprite_ind] = .{ + .position = .{ random.float(f32) * window_width, random.float(f32) * window_height, 0 }, + .rotation = 2 * std.math.pi * random.float(f32), + .size = .{ sprite_size, sprite_size }, + .texture = .{ ravioli_coords[ravioli_ind][0], ravioli_coords[ravioli_ind][1], 0.5, 0.5 }, + .color = .{ 1, 1, 1, 1 }, + ._ = .{ 69, 72 }, + }; + } + std.debug.print("{any}\n", .{sprite_data[0]}); // Currently to prove miscompilation. + } + + // Upload instance data. + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = app_state.sprite_compute_transfer_buffer, + .offset = 0, + }, + .{ + .buffer = app_state.sprite_compute_buffer, + .offset = 0, + .size = @sizeOf(ComputeSpriteInstance) * sprite_count, + }, + true, + ); + } + + // Set up compute pass to build vertex buffer. + { + const compute_pass = cmd_buf.beginComputePass( + &.{}, + &.{ + .{ + .buffer = app_state.sprite_vertex_buffer, + .cycle = true, + }, + }, + ); + defer compute_pass.end(); + compute_pass.bindPipeline(app_state.compute_pipeline); + compute_pass.bindStorageBuffers( + 0, + &.{ + app_state.sprite_compute_buffer, + }, + ); + cmd_buf.pushComputeUniformData(0, std.mem.asBytes(&@as(f32, 0.25))); + compute_pass.dispatch( + sprite_count / app_state.compute_pipeline_metadata.threadcount_x, + app_state.compute_pipeline_metadata.threadcount_y, + app_state.compute_pipeline_metadata.threadcount_z, + ); + } + + // Render sprites. + { + const render_pass = cmd_buf.beginRenderPass( + &.{ + .{ + .texture = texture, + .load = .clear, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + }, + }, + null, + ); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.render_pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ + .buffer = app_state.sprite_vertex_buffer, + .offset = 0, + }, + }, + ); + render_pass.bindIndexBuffer( + .{ + .buffer = app_state.sprite_index_buffer, + .offset = 0, + }, + .indices_32bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }, + ); + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.orthographicOffCenter(0, window_width, window_height, 0, 0, -1))); + render_pass.drawIndexedPrimitives(sprite_count * 6, 1, 0, 0, 0); + } + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.sprite_index_buffer); + val.device.releaseBuffer(val.sprite_vertex_buffer); + val.device.releaseBuffer(val.sprite_compute_buffer); + val.device.releaseTransferBuffer(val.sprite_compute_transfer_buffer); + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseComputePipeline(val.compute_pipeline); + val.device.releaseGraphicsPipeline(val.render_pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/compute-uniforms.zig b/gpu_examples/src/compute-uniforms.zig new file mode 100644 index 0000000..6bdf01c --- /dev/null +++ b/gpu_examples/src/compute-uniforms.zig @@ -0,0 +1,176 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const comp_shader_source = @embedFile("gradientTexture.comp"); +const comp_shader_name = "Gradient Texture"; + +const window_width = 640; +const window_height = 480; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + gradient_render_texture: sdl3.gpu.Texture, + gradient_render_pipeline: sdl3.gpu.ComputePipeline, + gradient_render_pipeline_metadata: sdl3.shadercross.ComputePipelineMetadata, +}; + +fn loadComputeShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, +) !struct { pipeline: sdl3.gpu.ComputePipeline, metadata: sdl3.shadercross.ComputePipelineMetadata } { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = .compute, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectComputeSpirv(spirv_code); + return .{ .pipeline = try sdl3.shadercross.compileComputePipelineFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = .compute, + }, spirv_metadata), .metadata = spirv_metadata }; +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Compute Uniforms", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Create compute pipeline. + const gradient_texture_pipeline_result = try loadComputeShader(device, comp_shader_name, comp_shader_source); + errdefer device.releaseComputePipeline(gradient_texture_pipeline_result.pipeline); + + // Prepare texture. + const gradient_render_texture = try device.createTexture(.{ + .format = .r8g8b8a8_unorm, + .width = window_width, + .height = window_width, + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .compute_storage_write = true, .sampler = true }, + }); + errdefer device.releaseTexture(gradient_render_texture); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .gradient_render_texture = gradient_render_texture, + .gradient_render_pipeline = gradient_texture_pipeline_result.pipeline, + .gradient_render_pipeline_metadata = gradient_texture_pipeline_result.metadata, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + { + const compute_pass = cmd_buf.beginComputePass( + &.{ + .{ .texture = app_state.gradient_render_texture, .cycle = true }, + }, + &.{}, + ); + defer compute_pass.end(); + compute_pass.bindPipeline(app_state.gradient_render_pipeline); + cmd_buf.pushComputeUniformData(0, std.mem.asBytes(&(@as(f32, @floatFromInt(sdl3.timer.getMillisecondsSinceInit())) / 1000))); + compute_pass.dispatch( + swapchain_texture.width / app_state.gradient_render_pipeline_metadata.threadcount_x, + swapchain_texture.height / app_state.gradient_render_pipeline_metadata.threadcount_y, + app_state.gradient_render_pipeline_metadata.threadcount_z, + ); + } + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.gradient_render_texture, + .region = .{ .x = 0, .y = 0, .w = window_width, .h = window_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width, .h = swapchain_texture.height }, + }, + .load_op = .do_not_care, + .filter = .linear, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseComputePipeline(val.gradient_render_pipeline); + val.device.releaseTexture(val.gradient_render_texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/copy-and-readback.zig b/gpu_examples/src/copy-and-readback.zig new file mode 100644 index 0000000..3b72ac7 --- /dev/null +++ b/gpu_examples/src/copy-and-readback.zig @@ -0,0 +1,384 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const buffer_data = [_]u32{ + 2, + 4, + 8, + 16, + 32, + 64, + 128, +}; +const buffer_data_bytes = std.mem.asBytes(&buffer_data); + +const window_width = 640; +const window_height = 480; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + original_texture: sdl3.gpu.Texture, + texture_copy: sdl3.gpu.Texture, + texture_small: sdl3.gpu.Texture, + original_buffer: sdl3.gpu.Buffer, + buffer_copy: sdl3.gpu.Buffer, + texture_width: u32, + texture_height: u32, +}; + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Copy And Readback", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create textures. + const original_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(original_texture); + const texture_copy = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture Copy" }, + }); + errdefer device.releaseTexture(texture_copy); + const texture_small = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth() / 2), + .height = @intCast(image_data.getHeight() / 2), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .color_target = true }, + .props = .{ .name = "Ravioli Texture Small" }, + }); + errdefer device.releaseTexture(texture_small); + + // Create buffers. + const original_buffer = try device.createBuffer(.{ + .usage = .{ .graphics_storage_read = true }, + .size = buffer_data_bytes.len, + }); + errdefer device.releaseBuffer(original_buffer); + const buffer_copy = try device.createBuffer(.{ + .usage = .{ .graphics_storage_read = true }, + .size = buffer_data_bytes.len, + }); + errdefer device.releaseBuffer(buffer_copy); + + // Setup transfer buffers. + const download_transfer_buffer = try device.createTransferBuffer(.{ + .usage = .download, + .size = @intCast(image_bytes.len + buffer_data_bytes.len), + }); + defer device.releaseTransferBuffer(download_transfer_buffer); + const upload_transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(image_bytes.len + buffer_data_bytes.len), + }); + defer device.releaseTransferBuffer(upload_transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(upload_transfer_buffer, false); + defer device.unmapTransferBuffer(upload_transfer_buffer); + @memcpy(transfer_buffer_mapped[0..image_bytes.len], image_bytes); + @memcpy(transfer_buffer_mapped[image_bytes.len .. image_bytes.len + buffer_data_bytes.len], buffer_data_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = upload_transfer_buffer, + .offset = 0, + }, + .{ + .texture = original_texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + copy_pass.textureToTexture( + .{ + .texture = original_texture, + }, + .{ + .texture = texture_copy, + }, + @intCast(image_data.getWidth()), + @intCast(image_data.getHeight()), + 1, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = upload_transfer_buffer, + .offset = @intCast(image_bytes.len), + }, + .{ + .buffer = original_buffer, + .offset = 0, + .size = buffer_data_bytes.len, + }, + false, + ); + copy_pass.bufferToBuffer( + .{ + .buffer = original_buffer, + .offset = 0, + }, + .{ + .buffer = buffer_copy, + .offset = 0, + }, + buffer_data_bytes.len, + false, + ); + } + + // Render the half-size version. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = original_texture, + .region = .{ .x = 0, .y = 0, .w = @intCast(image_data.getWidth()), .h = @intCast(image_data.getHeight()) }, + }, + .destination = .{ + .texture = texture_small, + .region = .{ .x = 0, .y = 0, .w = @intCast(image_data.getWidth() / 2), .h = @intCast(image_data.getHeight() / 2) }, + }, + .load_op = .do_not_care, + .filter = .linear, + }); + + // Download original bytes from copies. + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.downloadFromTexture( + .{ + .texture = texture_copy, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + .{ + .transfer_buffer = download_transfer_buffer, + .offset = 0, + }, + ); + copy_pass.downloadFromBuffer( + .{ + .buffer = original_buffer, + .offset = 0, + .size = buffer_data_bytes.len, + }, + .{ + .transfer_buffer = download_transfer_buffer, + .offset = @intCast(image_bytes.len), + }, + ); + } + + // Wait for commands. + const fence = try cmd_buf.submitAndAcquireFence(); + defer device.releaseFence(fence); + try device.waitForFences( + true, + &.{ + fence, + }, + ); + + // Compare downloaded data. + { + const download_buffer_mapped = try device.mapTransferBuffer(download_transfer_buffer, false); + defer device.unmapTransferBuffer(download_transfer_buffer); + if (std.mem.eql(u8, image_bytes, download_buffer_mapped[0..image_bytes.len])) { + try sdl3.log.log("SUCCESS! Original texture bytes and the downloaded bytes match!", .{}); + } else { + try sdl3.log.log("FAILURE! Original texture bytes do not match downloaded bytes!", .{}); + } + if (std.mem.eql(u8, buffer_data_bytes, download_buffer_mapped[image_bytes.len .. image_bytes.len + buffer_data_bytes.len])) { + try sdl3.log.log("SUCCESS! Original buffer bytes and the downloaded bytes match!", .{}); + } else { + try sdl3.log.log("FAILURE! Original buffer bytes do not match downloaded bytes!", .{}); + } + } + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .buffer_copy = buffer_copy, + .original_buffer = original_buffer, + .original_texture = original_texture, + .texture_copy = texture_copy, + .texture_small = texture_small, + .texture_width = @intCast(image_data.getWidth()), + .texture_height = @intCast(image_data.getHeight()), + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Just clear the screen. + { + const render_pass = cmd_buf.beginRenderPass( + &.{ + .{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, + null, + ); + defer render_pass.end(); + } + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.original_texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .load, + .filter = .nearest, + }); + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture_copy, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = swapchain_texture.width / 2, .y = 0, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .load, + .filter = .nearest, + }); + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.texture_small, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width / 2, .h = app_state.texture_height / 2 }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = swapchain_texture.width / 4, .y = swapchain_texture.height / 2, .w = swapchain_texture.width / 2, .h = swapchain_texture.height / 2 }, + }, + .load_op = .load, + .filter = .nearest, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.buffer_copy); + val.device.releaseBuffer(val.original_buffer); + val.device.releaseTexture(val.texture_small); + val.device.releaseTexture(val.texture_copy); + val.device.releaseTexture(val.original_texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/copy-consistency.zig b/gpu_examples/src/copy-consistency.zig new file mode 100644 index 0000000..4b08eba --- /dev/null +++ b/gpu_examples/src/copy-consistency.zig @@ -0,0 +1,531 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("texturedQuad.frag"); +const frag_shader_name = "Textured Quad"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); +const ravioli_inverted_bmp = @embedFile("images/ravioliInverted.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 0, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 0, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 1 } }, + + .{ .position = .{ 0, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ 0, -1, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + left_vertex_buffer: sdl3.gpu.Buffer, + right_vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + left_texture: sdl3.gpu.Texture, + right_texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, + width: u32, + height: u32, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Copy Consistency", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + .blend_state = .{ + .enable_blend = true, + .source_color = .src_alpha, + .source_alpha = .src_alpha, + .destination_color = .one_minus_src_alpha, + .destination_alpha = .one_minus_src_alpha, + }, + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + + // Prepare vertex buffers. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(PositionTextureVertex) * 4, + }); + errdefer device.releaseBuffer(vertex_buffer); + const left_vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(PositionTextureVertex) * 4, + }); + errdefer device.releaseBuffer(left_vertex_buffer); + const right_vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(PositionTextureVertex) * 4, + }); + errdefer device.releaseBuffer(right_vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the images. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + const image_data_inverted = try loadImage(ravioli_inverted_bmp); + defer image_data_inverted.deinit(); + const image_inverted_bytes = image_data_inverted.getPixels().?[0 .. image_data_inverted.getWidth() * image_data_inverted.getHeight() * @sizeOf(u8) * 4]; + + // Create textures. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + const left_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture Left" }, + }); + errdefer device.releaseTexture(left_texture); + const right_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data_inverted.getWidth()), + .height = @intCast(image_data_inverted.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture Right" }, + }); + errdefer device.releaseTexture(right_texture); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_left_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer_right_image_data_off = transfer_buffer_left_image_data_off + image_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len + image_inverted_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_left_image_data_off .. transfer_buffer_left_image_data_off + image_bytes.len], image_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_right_image_data_off .. transfer_buffer_right_image_data_off + image_inverted_bytes.len], image_inverted_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = left_vertex_buffer, + .offset = 0, + .size = @sizeOf(PositionTextureVertex) * 4, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off + @sizeOf(PositionTextureVertex) * 4, + }, + .{ + .buffer = right_vertex_buffer, + .offset = 0, + .size = @sizeOf(PositionTextureVertex) * 4, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_left_image_data_off, + }, + .{ + .texture = left_texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = @intCast(transfer_buffer_right_image_data_off), + }, + .{ + .texture = right_texture, + .width = @intCast(image_data_inverted.getWidth()), + .height = @intCast(image_data_inverted.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .left_vertex_buffer = left_vertex_buffer, + .right_vertex_buffer = right_vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + .left_texture = left_texture, + .right_texture = right_texture, + .sampler = sampler, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Copy left side resources. + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.bufferToBuffer( + .{ + .buffer = app_state.left_vertex_buffer, + .offset = 0, + }, + .{ + .buffer = app_state.vertex_buffer, + .offset = 0, + }, + @sizeOf(PositionTextureVertex) * 4, + false, + ); + copy_pass.textureToTexture( + .{ + .texture = app_state.left_texture, + }, + .{ + .texture = app_state.texture, + }, + app_state.width, + app_state.height, + 1, + false, + ); + } + + // Draw the left side. + var color_target_info = sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }; + { + const render_pass = cmd_buf.beginRenderPass( + &.{ + color_target_info, + }, + null, + ); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers(0, &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + + // Copy right side resources. + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.bufferToBuffer( + .{ + .buffer = app_state.right_vertex_buffer, + .offset = 0, + }, + .{ + .buffer = app_state.vertex_buffer, + .offset = 0, + }, + @sizeOf(PositionTextureVertex) * 4, + false, + ); + copy_pass.textureToTexture( + .{ + .texture = app_state.right_texture, + }, + .{ + .texture = app_state.texture, + }, + app_state.width, + app_state.height, + 1, + false, + ); + } + + // Draw the right side. + color_target_info.load = .load; + { + const render_pass = cmd_buf.beginRenderPass( + &.{ + color_target_info, + }, + null, + ); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers(0, &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseTexture(val.left_texture); + val.device.releaseTexture(val.right_texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseBuffer(val.left_vertex_buffer); + val.device.releaseBuffer(val.right_vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/cubemap.zig b/gpu_examples/src/cubemap.zig new file mode 100644 index 0000000..a4081ef --- /dev/null +++ b/gpu_examples/src/cubemap.zig @@ -0,0 +1,452 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("skybox.vert"); +const vert_shader_name = "Skybox"; +const frag_shader_source = @embedFile("skybox.frag"); +const frag_shader_name = "Skybox"; + +const window_width = 640; +const window_height = 480; + +const tex_size = 64; + +const PositionVertex = packed struct { + position: @Vector(3, f32), +}; + +const vertices = [_]PositionVertex{ + .{ .position = .{ -10, -10, -10 } }, + .{ .position = .{ 10, -10, -10 } }, + .{ .position = .{ 10, 10, -10 } }, + .{ .position = .{ -10, 10, -10 } }, + + .{ .position = .{ -10, -10, 10 } }, + .{ .position = .{ 10, -10, 10 } }, + .{ .position = .{ 10, 10, 10 } }, + .{ .position = .{ -10, 10, 10 } }, + + .{ .position = .{ -10, -10, -10 } }, + .{ .position = .{ -10, 10, -10 } }, + .{ .position = .{ -10, 10, 10 } }, + .{ .position = .{ -10, -10, 10 } }, + + .{ .position = .{ 10, -10, -10 } }, + .{ .position = .{ 10, 10, -10 } }, + .{ .position = .{ 10, 10, 10 } }, + .{ .position = .{ 10, -10, 10 } }, + + .{ .position = .{ -10, -10, -10 } }, + .{ .position = .{ -10, -10, 10 } }, + .{ .position = .{ 10, -10, 10 } }, + .{ .position = .{ 10, -10, -10 } }, + + .{ .position = .{ -10, 10, -10 } }, + .{ .position = .{ -10, 10, 10 } }, + .{ .position = .{ 10, 10, 10 } }, + .{ .position = .{ 10, 10, -10 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ 0, 1, 2, 0, 2, 3, 6, 5, 4, 7, 6, 4, 8, 9, 10, 8, 10, 11, 14, 13, 12, 15, 14, 12, 16, 17, 18, 16, 18, 19, 22, 21, 20, 23, 22, 20 }; +const indices_bytes = std.mem.asBytes(&indices); + +const clear_colors = [_]sdl3.pixels.FColor{ + .{ .r = 1, .g = 0, .b = 0, .a = 1 }, + .{ .r = 0, .g = 1, .b = 0, .a = 1 }, + .{ .r = 0, .g = 0, .b = 1, .a = 1 }, + .{ .r = 1, .g = 1, .b = 0, .a = 1 }, + .{ .r = 1, .g = 0, .b = 1, .a = 1 }, + .{ .r = 0, .g = 1, .b = 1, .a = 1 }, +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, + cam_pos: @Vector(3, f32) = .{ 0, 0, 4 }, +}; + +const Mat4 = packed struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn lookAt(camera_position: @Vector(3, f32), camera_target: @Vector(3, f32), camera_up_vector: @Vector(3, f32)) Mat4 { + const target_to_position = camera_position - camera_target; + const a = (Vec3{ .data = target_to_position }).normalize(); + const b = (Vec3{ .data = camera_up_vector }).cross(a).normalize(); + const c = a.cross(b); + return .{ + .c0 = .{ b.data[0], c.data[0], a.data[0], 0 }, + .c1 = .{ b.data[1], c.data[1], a.data[1], 0 }, + .c2 = .{ b.data[2], c.data[2], a.data[2], 0 }, + .c3 = .{ -b.dot(.{ .data = camera_position }), -c.dot(.{ .data = camera_position }), -a.dot(.{ .data = camera_position }), 1 }, + }; + } + + pub fn mul(a: Mat4, b: Mat4) Mat4 { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ + .c0 = .{ @reduce(.Add, ar0 * b.c0), @reduce(.Add, ar1 * b.c0), @reduce(.Add, ar2 * b.c0), @reduce(.Add, ar3 * b.c0) }, + .c1 = .{ @reduce(.Add, ar0 * b.c1), @reduce(.Add, ar1 * b.c1), @reduce(.Add, ar2 * b.c1), @reduce(.Add, ar3 * b.c1) }, + .c2 = .{ @reduce(.Add, ar0 * b.c2), @reduce(.Add, ar1 * b.c2), @reduce(.Add, ar2 * b.c2), @reduce(.Add, ar3 * b.c2) }, + .c3 = .{ @reduce(.Add, ar0 * b.c3), @reduce(.Add, ar1 * b.c3), @reduce(.Add, ar2 * b.c3), @reduce(.Add, ar3 * b.c3) }, + }; + } + + pub fn perspectiveFieldOfView(field_of_view: f32, aspect_ratio: f32, near_plane_distance: f32, far_plane_distance: f32) Mat4 { + const num = 1 / @tan(field_of_view * 0.5); + return .{ + .c0 = .{ num / aspect_ratio, 0, 0, 0 }, + .c1 = .{ 0, num, 0, 0 }, + .c2 = .{ 0, 0, far_plane_distance / (near_plane_distance - far_plane_distance), -1 }, + .c3 = .{ 0, 0, (near_plane_distance * far_plane_distance) / (near_plane_distance - far_plane_distance), 0 }, + }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +const Vec3 = struct { + data: @Vector(3, f32), + + pub fn cross(a: Vec3, b: Vec3) Vec3 { + return .{ .data = .{ + a.data[1] * b.data[2] - b.data[1] * a.data[2], + -(a.data[0] * b.data[2] - b.data[0] * a.data[2]), + a.data[0] * b.data[1] - b.data[0] * a.data[1], + } }; + } + + pub fn dot(a: Vec3, b: Vec3) f32 { + const mul = a.data * b.data; + return @reduce(.Add, mul); + } + + pub fn normalize(self: Vec3) Vec3 { + const mag = @sqrt(self.dot(self)); + return .{ .data = self.data / @as(@Vector(3, f32), @splat(mag)) }; + } +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Cubemap", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionVertex, "position"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .cube, + .format = .r8g8b8a8_unorm, + .width = tex_size, + .height = tex_size, + .layer_count_or_depth = clear_colors.len, + .num_levels = 1, + .usage = .{ .sampler = true, .color_target = true }, + .props = .{ .name = "Cubemap Textures" }, + }); + errdefer device.releaseTexture(texture); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + } + for (0..clear_colors.len) |ind| { + const render_pass = cmd_buf.beginRenderPass( + &.{ + .{ + .texture = texture, + .layer_or_depth_plane = @intCast(ind), + .clear_color = clear_colors[ind], + .load = .clear, + }, + }, + null, + ); + defer render_pass.end(); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + .sampler = sampler, + }; + + // Finish setup. + try sdl3.log.log("Press left/right to view the opposite direction", .{}); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }, + ); + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.perspectiveFieldOfView( + 75.0 * std.math.pi / 180.0, + @as(comptime_float, window_width) / @as(comptime_float, window_height), + 0.01, + 100, + ).mul(Mat4.lookAt( + app_state.cam_pos, + .{ 0, 0, 0 }, + .{ 0, 1, 0 }, + )))); + render_pass.drawIndexedPrimitives(36, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + if (key.key) |val| switch (val) { + .left, .right => { + app_state.cam_pos[2] = -app_state.cam_pos[2]; + }, + else => {}, + }; + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/cull-mode.zig b/gpu_examples/src/cull-mode.zig new file mode 100644 index 0000000..e5e2c14 --- /dev/null +++ b/gpu_examples/src/cull-mode.zig @@ -0,0 +1,324 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("positionColor.vert"); +const vert_shader_name = "Position Color"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const vertices: struct { + cw: [3]PositionColorVertex = .{ + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 0, 0, 255, 255 } }, + }, + ccw: [3]PositionColorVertex = .{ + .{ .position = .{ 0, 1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ -1, -1, 0 }, .color = .{ 0, 0, 255, 255 } }, + }, +} = .{}; +const vertices_bytes = std.mem.asBytes(&vertices); +const vertices_cw_offset = @offsetOf(@TypeOf(vertices), "cw"); +const vertices_ccw_offset = @offsetOf(@TypeOf(vertices), "ccw"); +const vertices_cw_size = @sizeOf(@FieldType(@TypeOf(vertices), "cw")); +const vertices_ccw_size = @sizeOf(@FieldType(@TypeOf(vertices), "ccw")); + +const mode_names = [_][:0]const u8{ + "CW_CullNone", + "CW_CullFront", + "CW_CullBack", + "CCW_CullNone", + "CCW_CullFront", + "CCW_CullBack", +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipelines: [mode_names.len]sdl3.gpu.GraphicsPipeline, + vertex_buffer_cw: sdl3.gpu.Buffer, + vertex_buffer_ccw: sdl3.gpu.Buffer, + curr_mode: usize = 0, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Cull Mode", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + var pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + }; + + // Prepare vertex buffers. + const vertex_buffer_cw = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_cw_size, + }); + errdefer device.releaseBuffer(vertex_buffer_cw); + const vertex_buffer_ccw = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_ccw_size, + }); + errdefer device.releaseBuffer(vertex_buffer_ccw); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = vertices_bytes.len, + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped, vertices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = vertices_cw_offset, + }, + .{ + .buffer = vertex_buffer_cw, + .offset = 0, + .size = vertices_cw_size, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = vertices_ccw_offset, + }, + .{ + .buffer = vertex_buffer_ccw, + .offset = 0, + .size = vertices_ccw_size, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + + try sdl3.log.log("Press Left/Right to switch between modes", .{}); + try sdl3.log.log("Current Mode: {s}", .{mode_names[0]}); + + // Prevent potential errdefer leaks by making the pipelines here. + var pipelines: [mode_names.len]sdl3.gpu.GraphicsPipeline = undefined; + for (0..pipelines.len) |i| { + pipeline_create_info.rasterizer_state.cull_mode = @enumFromInt(i % 3); + pipeline_create_info.rasterizer_state.front_face = if (i > 2) .clockwise else .counter_clockwise; + pipelines[i] = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipelines[i]); // TODO: Prevent possible leak here? + } + + // Set state. + state.* = .{ + .device = device, + .window = window, + .pipelines = pipelines, + .vertex_buffer_cw = vertex_buffer_cw, + .vertex_buffer_ccw = vertex_buffer_ccw, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + + // Choose the pipeline currently set. + render_pass.bindGraphicsPipeline(app_state.pipelines[app_state.curr_mode]); + + // Bind the vertex buffers then draw the primitives. + render_pass.setViewport(.{ .region = .{ .x = 0, .y = 0, .w = 320, .h = 480 } }); + render_pass.bindVertexBuffers(0, &.{ + .{ .buffer = app_state.vertex_buffer_cw, .offset = 0 }, + }); + render_pass.drawPrimitives(3, 1, 0, 0); + render_pass.setViewport(.{ .region = .{ .x = 320, .y = 0, .w = 320, .h = 480 } }); + render_pass.bindVertexBuffers(0, &.{ + .{ .buffer = app_state.vertex_buffer_ccw, .offset = 0 }, + }); + render_pass.drawPrimitives(3, 1, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) + if (key.key) |val| switch (val) { + .left => { + if (app_state.curr_mode == 0) { + app_state.curr_mode = app_state.pipelines.len - 1; + } else app_state.curr_mode -= 1; + try sdl3.log.log("Current Mode: {s}", .{mode_names[app_state.curr_mode]}); + }, + .right => { + if (app_state.curr_mode >= app_state.pipelines.len - 1) { + app_state.curr_mode = 0; + } else app_state.curr_mode += 1; + try sdl3.log.log("Current Mode: {s}", .{mode_names[app_state.curr_mode]}); + }, + else => {}, + }; + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.vertex_buffer_ccw); + val.device.releaseBuffer(val.vertex_buffer_cw); + for (val.pipelines) |pipeline| { + val.device.releaseGraphicsPipeline(pipeline); + } + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/custom-sampling.zig.wip b/gpu_examples/src/custom-sampling.zig.wip new file mode 100644 index 0000000..296cd7e --- /dev/null +++ b/gpu_examples/src/custom-sampling.zig.wip @@ -0,0 +1,356 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("customSampling.frag"); +const frag_shader_name = "Custom Sampling"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const sampler_names = [_][]const u8{ + "Normal", + "Custom", +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + custom_sampler: bool = false, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Custom Sampling", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .graphics_storage_read = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_data_off .. transfer_buffer_image_data_off + image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_image_data_off, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + }; + + // Finish setup. + try sdl3.log.log("Press left/right to switch sampler modes", .{}); + try sdl3.log.log("Sampler state: {s}", .{sampler_names[@intFromBool(state.custom_sampler)]}); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentStorageTextures( + 0, + &.{ + app_state.texture, + }, + ); // This isn't working! Textured image count wrong somehow? + cmd_buf.pushFragmentUniformData(0, std.mem.asBytes(&@as(u32, @intFromBool(app_state.custom_sampler)))); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + if (key.key) |val| switch (val) { + .left, .right => { + app_state.custom_sampler = !app_state.custom_sampler; + try sdl3.log.log("Sampler state: {s}", .{sampler_names[@intFromBool(app_state.custom_sampler)]}); + }, + else => {}, + }; + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/depth-sampler.zig b/gpu_examples/src/depth-sampler.zig new file mode 100644 index 0000000..cf727b1 --- /dev/null +++ b/gpu_examples/src/depth-sampler.zig @@ -0,0 +1,618 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const scene_vert_shader_source = @embedFile("positionColorTransform.vert"); +const scene_vert_shader_name = "Position Color Transform"; +const scene_frag_shader_source = @embedFile("solidColorDepth.frag"); +const scene_frag_shader_name = "Solid Color Depth"; +const effect_vert_shader_source = @embedFile("texturedQuad.vert"); +const effect_vert_shader_name = "Textured Quad"; +const effect_frag_shader_source = @embedFile("depthOutline.frag"); +const effect_frag_shader_name = "Depth Outline"; + +const window_width = 640; +const window_height = 480; + +const tex_size = 64; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const scene_vertices = [_]PositionColorVertex{ + .{ .position = .{ -10, -10, -10 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 10, -10, -10 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 10, 10, -10 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ -10, 10, -10 }, .color = .{ 255, 0, 0, 255 } }, + + .{ .position = .{ -10, -10, 10 }, .color = .{ 255, 255, 0, 255 } }, + .{ .position = .{ 10, -10, 10 }, .color = .{ 255, 255, 0, 255 } }, + .{ .position = .{ 10, 10, 10 }, .color = .{ 255, 255, 0, 255 } }, + .{ .position = .{ -10, 10, 10 }, .color = .{ 255, 255, 0, 255 } }, + + .{ .position = .{ -10, -10, -10 }, .color = .{ 255, 0, 255, 255 } }, + .{ .position = .{ -10, 10, -10 }, .color = .{ 255, 0, 255, 255 } }, + .{ .position = .{ -10, 10, 10 }, .color = .{ 255, 0, 255, 255 } }, + .{ .position = .{ -10, -10, 10 }, .color = .{ 255, 0, 255, 255 } }, + + .{ .position = .{ 10, -10, -10 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 10, 10, -10 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 10, 10, 10 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 10, -10, 10 }, .color = .{ 0, 255, 0, 255 } }, + + .{ .position = .{ -10, -10, -10 }, .color = .{ 0, 255, 255, 255 } }, + .{ .position = .{ -10, -10, 10 }, .color = .{ 0, 255, 255, 255 } }, + .{ .position = .{ 10, -10, 10 }, .color = .{ 0, 255, 255, 255 } }, + .{ .position = .{ 10, -10, -10 }, .color = .{ 0, 255, 255, 255 } }, + + .{ .position = .{ -10, 10, -10 }, .color = .{ 0, 0, 255, 255 } }, + .{ .position = .{ -10, 10, 10 }, .color = .{ 0, 0, 255, 255 } }, + .{ .position = .{ 10, 10, 10 }, .color = .{ 0, 0, 255, 255 } }, + .{ .position = .{ 10, 10, -10 }, .color = .{ 0, 0, 255, 255 } }, +}; +const scene_vertices_bytes = std.mem.asBytes(&scene_vertices); + +const effect_vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const effect_vertices_bytes = std.mem.asBytes(&effect_vertices); + +const scene_indices = [_]u16{ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 }; +const scene_indices_bytes = std.mem.asBytes(&scene_indices); + +const effect_indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const effect_indices_bytes = std.mem.asBytes(&effect_indices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + scene_pipeline: sdl3.gpu.GraphicsPipeline, + scene_vertex_buffer: sdl3.gpu.Buffer, + scene_index_buffer: sdl3.gpu.Buffer, + scene_color_texture: sdl3.gpu.Texture, + scene_depth_texture: sdl3.gpu.Texture, + effect_pipeline: sdl3.gpu.GraphicsPipeline, + effect_vertex_buffer: sdl3.gpu.Buffer, + effect_index_buffer: sdl3.gpu.Buffer, + effect_sampler: sdl3.gpu.Sampler, + scene_width: u32, + scene_height: u32, +}; + +const Mat4 = packed struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn lookAt(camera_position: @Vector(3, f32), camera_target: @Vector(3, f32), camera_up_vector: @Vector(3, f32)) Mat4 { + const target_to_position = camera_position - camera_target; + const a = (Vec3{ .data = target_to_position }).normalize(); + const b = (Vec3{ .data = camera_up_vector }).cross(a).normalize(); + const c = a.cross(b); + return .{ + .c0 = .{ b.data[0], c.data[0], a.data[0], 0 }, + .c1 = .{ b.data[1], c.data[1], a.data[1], 0 }, + .c2 = .{ b.data[2], c.data[2], a.data[2], 0 }, + .c3 = .{ -b.dot(.{ .data = camera_position }), -c.dot(.{ .data = camera_position }), -a.dot(.{ .data = camera_position }), 1 }, + }; + } + + pub fn mul(a: Mat4, b: Mat4) Mat4 { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ + .c0 = .{ @reduce(.Add, ar0 * b.c0), @reduce(.Add, ar1 * b.c0), @reduce(.Add, ar2 * b.c0), @reduce(.Add, ar3 * b.c0) }, + .c1 = .{ @reduce(.Add, ar0 * b.c1), @reduce(.Add, ar1 * b.c1), @reduce(.Add, ar2 * b.c1), @reduce(.Add, ar3 * b.c1) }, + .c2 = .{ @reduce(.Add, ar0 * b.c2), @reduce(.Add, ar1 * b.c2), @reduce(.Add, ar2 * b.c2), @reduce(.Add, ar3 * b.c2) }, + .c3 = .{ @reduce(.Add, ar0 * b.c3), @reduce(.Add, ar1 * b.c3), @reduce(.Add, ar2 * b.c3), @reduce(.Add, ar3 * b.c3) }, + }; + } + + pub fn perspectiveFieldOfView(field_of_view: f32, aspect_ratio: f32, near_plane_distance: f32, far_plane_distance: f32) Mat4 { + const num = 1 / @tan(field_of_view * 0.5); + return .{ + .c0 = .{ num / aspect_ratio, 0, 0, 0 }, + .c1 = .{ 0, num, 0, 0 }, + .c2 = .{ 0, 0, far_plane_distance / (near_plane_distance - far_plane_distance), -1 }, + .c3 = .{ 0, 0, (near_plane_distance * far_plane_distance) / (near_plane_distance - far_plane_distance), 0 }, + }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } +}; + +const Vec3 = struct { + data: @Vector(3, f32), + + pub fn cross(a: Vec3, b: Vec3) Vec3 { + return .{ .data = .{ + a.data[1] * b.data[2] - b.data[1] * a.data[2], + -(a.data[0] * b.data[2] - b.data[0] * a.data[2]), + a.data[0] * b.data[1] - b.data[0] * a.data[1], + } }; + } + + pub fn dot(a: Vec3, b: Vec3) f32 { + const mul = a.data * b.data; + return @reduce(.Add, mul); + } + + pub fn normalize(self: Vec3) Vec3 { + const mag = @sqrt(self.dot(self)); + return .{ .data = self.data / @as(@Vector(3, f32), @splat(mag)) }; + } +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Depth Sampler", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const scene_vertex_shader = try loadGraphicsShader(device, scene_vert_shader_name, scene_vert_shader_source, .vertex); + defer device.releaseShader(scene_vertex_shader); + const scene_fragment_shader = try loadGraphicsShader(device, scene_frag_shader_name, scene_frag_shader_source, .fragment); + defer device.releaseShader(scene_fragment_shader); + const scene_pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + .depth_stencil_format = .depth16_unorm, + }, + .depth_stencil_state = .{ + .enable_depth_test = true, + .enable_depth_write = true, + .compare = .less, + .write_mask = 0xff, + }, + .vertex_shader = scene_vertex_shader, + .fragment_shader = scene_fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + }; + const scene_pipeline = try device.createGraphicsPipeline(scene_pipeline_create_info); + errdefer device.releaseGraphicsPipeline(scene_pipeline); + + const effect_vertex_shader = try loadGraphicsShader(device, effect_vert_shader_name, effect_vert_shader_source, .vertex); + defer device.releaseShader(effect_vertex_shader); + const effect_fragment_shader = try loadGraphicsShader(device, effect_frag_shader_name, effect_frag_shader_source, .fragment); + defer device.releaseShader(effect_fragment_shader); + const effect_pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + .blend_state = .{ + .enable_blend = true, + .source_color = .one, + .destination_color = .one_minus_src_alpha, + .color_blend = .add, + .source_alpha = .one, + .destination_alpha = .one_minus_src_alpha, + .alpha_blend = .add, + }, + }, + }, + }, + .vertex_shader = effect_vertex_shader, + .fragment_shader = effect_fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const effect_pipeline = try device.createGraphicsPipeline(effect_pipeline_create_info); + errdefer device.releaseGraphicsPipeline(effect_pipeline); + + // Create textures. + const scene_width = window_width / 4; + const scene_height = window_height / 4; + const scene_color_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = scene_width, + .height = scene_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .color_target = true }, + .props = .{ .name = "Scene Color Texture" }, + }); + errdefer device.releaseTexture(scene_color_texture); + const scene_depth_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .depth16_unorm, + .width = scene_width, + .height = scene_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .depth_stencil_target = true }, + .props = .{ .name = "Scene Depth Texture" }, + }); + errdefer device.releaseTexture(scene_depth_texture); + + // Create sampler. + const effect_sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + }); + + // Prepare vertex buffers. + const scene_vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = scene_vertices_bytes.len, + }); + errdefer device.releaseBuffer(scene_vertex_buffer); + const effect_vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = effect_vertices_bytes.len, + }); + errdefer device.releaseBuffer(effect_vertex_buffer); + + // Create the index buffers. + const scene_index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = scene_indices_bytes.len, + }); + errdefer device.releaseBuffer(scene_index_buffer); + const effect_index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = effect_indices_bytes.len, + }); + errdefer device.releaseBuffer(effect_index_buffer); + + // Setup transfer buffer. + const transfer_buffer_scene_vertex_data_off = 0; + const transfer_buffer_scene_index_data_off = transfer_buffer_scene_vertex_data_off + scene_vertices_bytes.len; + const transfer_buffer_effect_vertex_data_off = transfer_buffer_scene_index_data_off + scene_indices_bytes.len; + const transfer_buffer_effect_index_data_off = transfer_buffer_effect_vertex_data_off + effect_vertices_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(scene_vertices_bytes.len + scene_indices_bytes.len + effect_vertices_bytes.len + effect_indices_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_scene_vertex_data_off .. transfer_buffer_scene_vertex_data_off + scene_vertices_bytes.len], scene_vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_scene_index_data_off .. transfer_buffer_scene_index_data_off + scene_indices_bytes.len], scene_indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_effect_vertex_data_off .. transfer_buffer_effect_vertex_data_off + effect_vertices_bytes.len], effect_vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_effect_index_data_off .. transfer_buffer_effect_index_data_off + effect_indices_bytes.len], effect_indices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_scene_vertex_data_off, + }, + .{ + .buffer = scene_vertex_buffer, + .offset = 0, + .size = scene_vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_effect_vertex_data_off, + }, + .{ + .buffer = effect_vertex_buffer, + .offset = 0, + .size = effect_vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_scene_index_data_off, + }, + .{ + .buffer = scene_index_buffer, + .offset = 0, + .size = scene_indices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_effect_index_data_off, + }, + .{ + .buffer = effect_index_buffer, + .offset = 0, + .size = effect_indices_bytes.len, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .scene_pipeline = scene_pipeline, + .scene_vertex_buffer = scene_vertex_buffer, + .scene_index_buffer = scene_index_buffer, + .scene_color_texture = scene_color_texture, + .scene_depth_texture = scene_depth_texture, + .effect_pipeline = effect_pipeline, + .effect_vertex_buffer = effect_vertex_buffer, + .effect_index_buffer = effect_index_buffer, + .effect_sampler = effect_sampler, + .scene_width = scene_width, + .scene_height = scene_height, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Setup camera. + const time = @as(f32, @floatFromInt(sdl3.timer.getMillisecondsSinceInit())) / sdl3.timer.milliseconds_per_second; + const near_plane = 20; + const far_plane = 60; + const proj = Mat4.perspectiveFieldOfView( + 75 * std.math.pi / 180.0, + @as(f32, @floatFromInt(app_state.scene_width)) / @as(f32, @floatFromInt(app_state.scene_height)), + near_plane, + far_plane, + ); + const view = Mat4.lookAt( + .{ @cos(time) * 30, 30, @sin(time) * 30 }, + .{ 0, 0, 0 }, + .{ 0, 1, 0 }, + ); + const proj_view = proj.mul(view); + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&proj_view)); + cmd_buf.pushFragmentUniformData(0, std.mem.asBytes(&@as(@Vector(2, f32), .{ near_plane, far_plane }))); + + // Start a render pass for the scene. + { + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = app_state.scene_color_texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 0 }, + .load = .clear, + }, + }, .{ + .texture = app_state.scene_depth_texture, + .cycle = true, + .clear_depth = 1, + .clear_stencil = 0, + .load = .clear, + .store = .store, + .stencil_load = .clear, + .stencil_store = .store, + }); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.scene_pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.scene_vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.scene_index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.drawIndexedPrimitives(36, 1, 0, 0, 0); + } + + // Start a render pass for the effect. + { + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0.2, .g = 0.5, .b = 0.4, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.effect_pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.effect_vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.effect_index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers(0, &.{ + .{ .texture = app_state.scene_color_texture, .sampler = app_state.effect_sampler }, + .{ .texture = app_state.scene_depth_texture, .sampler = app_state.effect_sampler }, + }); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.effect_sampler); + val.device.releaseBuffer(val.effect_index_buffer); + val.device.releaseBuffer(val.effect_vertex_buffer); + val.device.releaseGraphicsPipeline(val.effect_pipeline); + val.device.releaseTexture(val.scene_depth_texture); + val.device.releaseTexture(val.scene_color_texture); + val.device.releaseBuffer(val.scene_index_buffer); + val.device.releaseBuffer(val.scene_vertex_buffer); + val.device.releaseGraphicsPipeline(val.scene_pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/draw-indirect.zig b/gpu_examples/src/draw-indirect.zig new file mode 100644 index 0000000..07afd54 --- /dev/null +++ b/gpu_examples/src/draw-indirect.zig @@ -0,0 +1,344 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("positionColor.vert"); +const vert_shader_name = "Position Color"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const vertices = [_]PositionColorVertex{ + + // Indexed indirect. + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 1, 1, 0 }, .color = .{ 0, 0, 255, 255 } }, + .{ .position = .{ -1, 1, 0 }, .color = .{ 255, 255, 255, 255 } }, + + // Indirect 1. + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, -1, 0 }, .color = .{ 0, 0, 255, 255 } }, + .{ .position = .{ 0.5, 1, 0 }, .color = .{ 255, 0, 0, 255 } }, + + // Indirect 2. + .{ .position = .{ -1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, -1, 0 }, .color = .{ 0, 0, 255, 255 } }, + .{ .position = .{ -0.5, 1, 0 }, .color = .{ 255, 0, 0, 255 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const indexed_draw_command = sdl3.gpu.IndexedIndirectDrawCommand{ + .num_indices = 6, + .num_instances = 1, +}; +const indexed_draw_command_bytes = std.mem.asBytes(&indexed_draw_command); + +const draw_commands = [_]sdl3.gpu.IndirectDrawCommand{ + .{ .num_vertices = 3, .num_instances = 1, .first_vertex = 4 }, + .{ .num_vertices = 3, .num_instances = 1, .first_vertex = 7 }, +}; +const draw_commands_bytes = std.mem.asBytes(&draw_commands); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + draw_buffer: sdl3.gpu.Buffer, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Draw Indirect", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Create the index buffer. + const draw_buffer = try device.createBuffer(.{ + .usage = .{ .indirect = true }, + .size = indexed_draw_command_bytes.len + draw_commands_bytes.len, + }); + errdefer device.releaseBuffer(draw_buffer); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_indexed_draw_command_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer_draw_commands_off = transfer_buffer_indexed_draw_command_off + indexed_draw_command_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + indexed_draw_command_bytes.len + draw_commands_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_indexed_draw_command_off .. transfer_buffer_indexed_draw_command_off + indexed_draw_command_bytes.len], indexed_draw_command_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_draw_commands_off .. transfer_buffer_draw_commands_off + draw_commands_bytes.len], draw_commands_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_indexed_draw_command_off, + }, + .{ + .buffer = draw_buffer, + .offset = 0, + .size = indexed_draw_command_bytes.len + draw_commands_bytes.len, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .draw_buffer = draw_buffer, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.drawIndexedPrimitivesIndirect(app_state.draw_buffer, 0, 1); + render_pass.drawPrimitivesIndirect(app_state.draw_buffer, indexed_draw_command_bytes.len, 2); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.draw_buffer); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/generate-mipmaps.zig b/gpu_examples/src/generate-mipmaps.zig new file mode 100644 index 0000000..105e854 --- /dev/null +++ b/gpu_examples/src/generate-mipmaps.zig @@ -0,0 +1,194 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const cube_bmp = @embedFile("images/cube0.bmp"); + +const window_width = 640; +const window_height = 480; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + mipmap_texture: sdl3.gpu.Texture, + texture_width: u32, + texture_height: u32, +}; + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Generate Mipmaps", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Load the image. + const image_data = try loadImage(cube_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const mipmap_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 3, + .usage = .{ .sampler = true, .color_target = true }, + .props = .{ .name = "Cube Texture" }, + }); + errdefer device.releaseTexture(mipmap_texture); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[0..image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .texture = mipmap_texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + cmd_buf.generateMipmapsForTexture(mipmap_texture); + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .mipmap_texture = mipmap_texture, + .texture_width = @intCast(image_data.getWidth()), + .texture_height = @intCast(image_data.getHeight()), + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + { + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + } + + // Blit the smallest mip level. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.mipmap_texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width / 4, .h = app_state.texture_height / 4 }, + .mip_level = 2, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width, .h = swapchain_texture.height }, + }, + .load_op = .do_not_care, + .filter = .nearest, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseTexture(val.mipmap_texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/images/cube0.bmp b/gpu_examples/src/images/cube0.bmp new file mode 100644 index 0000000..aa8207e Binary files /dev/null and b/gpu_examples/src/images/cube0.bmp differ diff --git a/gpu_examples/src/images/cube1.bmp b/gpu_examples/src/images/cube1.bmp new file mode 100644 index 0000000..886e38e Binary files /dev/null and b/gpu_examples/src/images/cube1.bmp differ diff --git a/gpu_examples/src/images/cube2.bmp b/gpu_examples/src/images/cube2.bmp new file mode 100644 index 0000000..02b5639 Binary files /dev/null and b/gpu_examples/src/images/cube2.bmp differ diff --git a/gpu_examples/src/images/cube3.bmp b/gpu_examples/src/images/cube3.bmp new file mode 100644 index 0000000..050de9a Binary files /dev/null and b/gpu_examples/src/images/cube3.bmp differ diff --git a/gpu_examples/src/images/cube4.bmp b/gpu_examples/src/images/cube4.bmp new file mode 100644 index 0000000..b41cba1 Binary files /dev/null and b/gpu_examples/src/images/cube4.bmp differ diff --git a/gpu_examples/src/images/cube5.bmp b/gpu_examples/src/images/cube5.bmp new file mode 100644 index 0000000..98fc0ee Binary files /dev/null and b/gpu_examples/src/images/cube5.bmp differ diff --git a/gpu_examples/src/images/latency.bmp b/gpu_examples/src/images/latency.bmp new file mode 100644 index 0000000..c1fed11 Binary files /dev/null and b/gpu_examples/src/images/latency.bmp differ diff --git a/gpu_examples/src/images/memorial.hdr b/gpu_examples/src/images/memorial.hdr new file mode 100644 index 0000000..c135ae9 Binary files /dev/null and b/gpu_examples/src/images/memorial.hdr differ diff --git a/gpu_examples/src/images/ravioli.bmp b/gpu_examples/src/images/ravioli.bmp new file mode 100644 index 0000000..ad13692 Binary files /dev/null and b/gpu_examples/src/images/ravioli.bmp differ diff --git a/gpu_examples/src/images/ravioliAtlas.bmp b/gpu_examples/src/images/ravioliAtlas.bmp new file mode 100644 index 0000000..1c6afe0 Binary files /dev/null and b/gpu_examples/src/images/ravioliAtlas.bmp differ diff --git a/gpu_examples/src/images/ravioliInverted.bmp b/gpu_examples/src/images/ravioliInverted.bmp new file mode 100644 index 0000000..2d718a8 Binary files /dev/null and b/gpu_examples/src/images/ravioliInverted.bmp differ diff --git a/gpu_examples/src/instanced-indexed.zig b/gpu_examples/src/instanced-indexed.zig new file mode 100644 index 0000000..d046506 --- /dev/null +++ b/gpu_examples/src/instanced-indexed.zig @@ -0,0 +1,333 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("positionColorInstanced.vert"); +const vert_shader_name = "Position Color Instanced"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const vertices = [_]PositionColorVertex{ + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 0, 0, 255, 255 } }, + + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 165, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 128, 0, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 0, 255, 255, 255 } }, + + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 255, 255, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 255, 255, 255, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 255, 255, 255, 255 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 3, + 4, + 5, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + use_vertex_offset: bool = false, + use_index_offset: bool = false, + use_index_buffer: bool = false, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Instanced Indexed", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = vertices_bytes.len + indices_bytes.len, + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[0..vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[vertices_bytes.len..], indices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = vertices_bytes.len, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + }; + + // Finish setup. + try sdl3.log.log("Press left to toggle vertex offset", .{}); + try sdl3.log.log("Press right to toggle index offset", .{}); + try sdl3.log.log("Press up to toggle using the index buffer", .{}); + try sdl3.log.log( + "State: {{VertexOffset: {any}, IndexOffset: {any}, UseIndexBuffer: {any}}}", + .{ state.use_vertex_offset, state.use_index_offset, state.use_index_buffer }, + ); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + + // Bind the vertex buffers then draw the primitives. + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + const vertex_offset: u32 = if (app_state.use_vertex_offset) 3 else 0; + const index_offset: u32 = if (app_state.use_index_offset) 3 else 0; + if (app_state.use_index_buffer) { + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.drawIndexedPrimitives(3, 16, index_offset, @intCast(vertex_offset), 0); + } else { + render_pass.drawPrimitives(3, 16, vertex_offset, 0); + } + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + app_state.use_vertex_offset = !app_state.use_vertex_offset; + changed = true; + }, + .right => { + app_state.use_index_offset = !app_state.use_index_offset; + changed = true; + }, + .up => { + app_state.use_index_buffer = !app_state.use_index_buffer; + changed = true; + }, + else => {}, + }; + if (changed) { + try sdl3.log.log( + "State: {{VertexOffset: {any}, IndexOffset: {any}, UseIndexBuffer: {any}}}", + .{ app_state.use_vertex_offset, app_state.use_index_offset, app_state.use_index_buffer }, + ); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/latency.zig b/gpu_examples/src/latency.zig new file mode 100644 index 0000000..3981561 --- /dev/null +++ b/gpu_examples/src/latency.zig @@ -0,0 +1,265 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const latency_bmp = @embedFile("images/latency.bmp"); + +const window_width = 640; +const window_height = 480; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + lag_texture: sdl3.gpu.Texture, + texture_width: u32, + texture_height: u32, + lag_x: f32 = 1, + allows_frames_in_flight: u32 = 2, + capture_cursor: bool = false, + fullscreen: bool = false, +}; + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Latency", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Load the image. + const image_data = try loadImage(latency_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const lag_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Latency Texture" }, + }); + errdefer device.releaseTexture(lag_texture); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[0..image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .texture = lag_texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .lag_texture = lag_texture, + .texture_width = @intCast(image_data.getWidth()), + .texture_height = @intCast(image_data.getHeight()), + }; + try device.setAllowedFramesInFlight(state.allows_frames_in_flight); + + // Finish setup. + try sdl3.log.log("Press left/right to change the number of allowed frames in flight", .{}); + try sdl3.log.log("Press down to toggle capturing the mouse cursor", .{}); + try sdl3.log.log("Press up to toggle fullscreen mode", .{}); + try sdl3.log.log("When the mouse cursor is captured the color directly above the cursor's point in the result of the test", .{}); + try sdl3.log.log("Negative lag can occur when the cursor is below the tear line when tearing is enabled as the cursor is only moved during V-blank so it lags the framebuffer update", .{}); + try sdl3.log.log(" Gray: -1 frames lag", .{}); + try sdl3.log.log(" White: 0 frames lag", .{}); + try sdl3.log.log(" Green: 1 frames lag", .{}); + try sdl3.log.log(" Yellow: 2 frames lag", .{}); + try sdl3.log.log(" Red: 3 frames lag", .{}); + try sdl3.log.log(" Cyan: 4 frames lag", .{}); + try sdl3.log.log(" Purple: 5 frames lag", .{}); + try sdl3.log.log(" Blue: 6 frames lag", .{}); + try sdl3.log.log( + "State: {{CaptureMouseCursor: {any}, AllowedFramesInFlight: {any}, Fullscreen: {any}}}", + .{ state.capture_cursor, state.allows_frames_in_flight, state.fullscreen }, + ); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + const cursor = sdl3.mouse.getGlobalState(); + const window_pos = try app_state.window.getPosition(); + var cursorX = cursor.x - @as(f32, @floatFromInt(window_pos.x)); + const cursorY = cursor.y - @as(f32, @floatFromInt(window_pos.y)); + + // Move cursor to a known position. + if (app_state.capture_cursor) { + cursorX = app_state.lag_x; + sdl3.mouse.warpInWindow(app_state.window, cursorX, cursorY); + if (app_state.lag_x >= @as(f32, @floatFromInt(swapchain_texture.width - app_state.texture_width))) { + app_state.lag_x = 1; + } else { + app_state.lag_x += 1; + } + } + + // Draw a sprite directly under the cursor if permitted by the blitting engine. + if (cursorX >= 1 and cursorX <= @as(f32, @floatFromInt(swapchain_texture.width - app_state.texture_width)) and cursorY >= 5 and cursorY <= @as(f32, @floatFromInt(swapchain_texture.height - app_state.texture_height + 5))) { + cmd_buf.blitTexture(.{ + .source = .{ + .texture = app_state.lag_texture, + .region = .{ .x = 0, .y = 0, .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = @intFromFloat(cursorX - 1), .y = @intFromFloat(cursorY - 5), .w = app_state.texture_width, .h = app_state.texture_height }, + }, + .load_op = .clear, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .filter = .nearest, + }); + } else { + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + } + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + if (app_state.allows_frames_in_flight <= 1) { + app_state.allows_frames_in_flight = 3; + } else app_state.allows_frames_in_flight -= 1; + try app_state.device.setAllowedFramesInFlight(app_state.allows_frames_in_flight); + changed = true; + }, + .right => { + if (app_state.allows_frames_in_flight >= 3) { + app_state.allows_frames_in_flight = 1; + } else app_state.allows_frames_in_flight += 1; + try app_state.device.setAllowedFramesInFlight(app_state.allows_frames_in_flight); + changed = true; + }, + .down => { + app_state.capture_cursor = !app_state.capture_cursor; + changed = true; + }, + .up => { + app_state.fullscreen = !app_state.fullscreen; + try app_state.window.setFullscreen(app_state.fullscreen); + changed = true; + }, + else => {}, + }; + if (changed) { + try sdl3.log.log( + "State: {{CaptureMouseCursor: {any}, AllowedFramesInFlight: {any}, Fullscreen: {any}}}", + .{ app_state.capture_cursor, app_state.allows_frames_in_flight, app_state.fullscreen }, + ); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseTexture(val.lag_texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/texture-2d-array.zig b/gpu_examples/src/texture-2d-array.zig new file mode 100644 index 0000000..e57d25f --- /dev/null +++ b/gpu_examples/src/texture-2d-array.zig @@ -0,0 +1,372 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("texturedQuadArray.frag"); +const frag_shader_name = "Textured Quad Array"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); +const ravioli_inverted_bmp = @embedFile("images/ravioliInverted.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Texture 2d Array", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the images. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + const image_data_inverted = try loadImage(ravioli_inverted_bmp); + defer image_data_inverted.deinit(); + const image_inverted_bytes = image_data_inverted.getPixels().?[0 .. image_data_inverted.getWidth() * image_data_inverted.getHeight() * @sizeOf(u8) * 4]; + std.debug.assert(image_data.getWidth() == image_data_inverted.getWidth()); + std.debug.assert(image_data.getHeight() == image_data_inverted.getHeight()); + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional_array, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 2, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Textures" }, + }); + errdefer device.releaseTexture(texture); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer_image_inverted_data_off = transfer_buffer_image_data_off + image_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len + image_inverted_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_data_off .. transfer_buffer_image_data_off + image_bytes.len], image_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_inverted_data_off .. transfer_buffer_image_inverted_data_off + image_inverted_bytes.len], image_inverted_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_image_data_off, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = @intCast(transfer_buffer_image_inverted_data_off), + }, + .{ + .texture = texture, + .width = @intCast(image_data_inverted.getWidth()), + .height = @intCast(image_data_inverted.getHeight()), + .depth = 1, + .layer = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + .sampler = sampler, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }, + ); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/textured-animated-quad.zig b/gpu_examples/src/textured-animated-quad.zig new file mode 100644 index 0000000..362f7b6 --- /dev/null +++ b/gpu_examples/src/textured-animated-quad.zig @@ -0,0 +1,432 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuadWithMatrix.vert"); +const vert_shader_name = "Textured Quad With Matrix"; +const frag_shader_source = @embedFile("texturedQuadWithMultiplyColor.frag"); +const frag_shader_name = "Textured Quad With Multiply Color"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -0.5, -0.5, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 0.5, -0.5, 0 }, .tex_coord = .{ 1, 0 } }, + .{ .position = .{ 0.5, 0.5, 0 }, .tex_coord = .{ 1, 1 } }, + .{ .position = .{ -0.5, 0.5, 0 }, .tex_coord = .{ 0, 1 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const FragMultiplyUniform = packed struct { + data: @Vector(4, f32), +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, +}; + +const Mat4 = packed struct { + c0: @Vector(4, f32), + c1: @Vector(4, f32), + c2: @Vector(4, f32), + c3: @Vector(4, f32), + + pub fn mul(a: Mat4, b: Mat4) Mat4 { + const ar0 = a.row(0); + const ar1 = a.row(1); + const ar2 = a.row(2); + const ar3 = a.row(3); + return .{ + .c0 = .{ @reduce(.Add, ar0 * b.c0), @reduce(.Add, ar1 * b.c0), @reduce(.Add, ar2 * b.c0), @reduce(.Add, ar3 * b.c0) }, + .c1 = .{ @reduce(.Add, ar0 * b.c1), @reduce(.Add, ar1 * b.c1), @reduce(.Add, ar2 * b.c1), @reduce(.Add, ar3 * b.c1) }, + .c2 = .{ @reduce(.Add, ar0 * b.c2), @reduce(.Add, ar1 * b.c2), @reduce(.Add, ar2 * b.c2), @reduce(.Add, ar3 * b.c2) }, + .c3 = .{ @reduce(.Add, ar0 * b.c3), @reduce(.Add, ar1 * b.c3), @reduce(.Add, ar2 * b.c3), @reduce(.Add, ar3 * b.c3) }, + }; + } + + pub fn rotationZ(radians: f32) Mat4 { + return .{ + .c0 = .{ @cos(radians), @sin(radians), 0, 0 }, + .c1 = .{ -@sin(radians), @cos(radians), 0, 0 }, + .c2 = .{ 0, 0, 1, 0 }, + .c3 = .{ 0, 0, 0, 1 }, + }; + } + + pub fn row(mat: Mat4, ind: comptime_int) @Vector(4, f32) { + return switch (ind) { + 0 => .{ mat.c0[0], mat.c1[0], mat.c2[0], mat.c3[0] }, + 1 => .{ mat.c0[1], mat.c1[1], mat.c2[1], mat.c3[1] }, + 2 => .{ mat.c0[2], mat.c1[2], mat.c2[2], mat.c3[2] }, + 3 => .{ mat.c0[3], mat.c1[3], mat.c2[3], mat.c3[3] }, + else => @compileError("Invalid row number"), + }; + } + + pub fn translation(amount: @Vector(3, f32)) Mat4 { + return .{ + .c0 = .{ 1, 0, 0, 0 }, + .c1 = .{ 0, 1, 0, 0 }, + .c2 = .{ 0, 0, 1, 0 }, + .c3 = .{ amount[0], amount[1], amount[2], 1 }, + }; + } +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Textured Animated Quad", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + .blend_state = .{ + .enable_blend = true, + .alpha_blend = .add, + .color_blend = .add, + .source_color = .src_alpha, + .source_alpha = .src_alpha, + .destination_color = .one_minus_src_alpha, + .destination_alpha = .one_minus_src_alpha, + }, + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + errdefer device.releaseSampler(sampler); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_data_off .. transfer_buffer_image_data_off + image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_image_data_off, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + .sampler = sampler, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + const time = @as(f32, @floatFromInt(sdl3.timer.getMillisecondsSinceInit())) / 1000; + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }, + ); + + // Top-left. + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.translation(.{ -0.5, -0.5, 0 }).mul(Mat4.rotationZ(time)))); + cmd_buf.pushFragmentUniformData(0, std.mem.asBytes(&FragMultiplyUniform{ .data = .{ 1, 0.5 + @sin(time) * 0.5, 1, 1 } })); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + + // Top-right. + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.translation(.{ 0.5, -0.5, 0 }).mul(Mat4.rotationZ((2 * std.math.pi) - time)))); + cmd_buf.pushFragmentUniformData(0, std.mem.asBytes(&FragMultiplyUniform{ .data = .{ 1, 0.5 + @cos(time) * 0.5, 1, 1 } })); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + + // Bottom-left. + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.translation(.{ -0.5, 0.5, 0 }).mul(Mat4.rotationZ(time)))); + cmd_buf.pushFragmentUniformData(0, std.mem.asBytes(&FragMultiplyUniform{ .data = .{ 1, 0.5 + @sin(time) * 0.2, 1, 1 } })); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + + // Bottom-right. + cmd_buf.pushVertexUniformData(0, std.mem.asBytes(&Mat4.translation(.{ 0.5, 0.5, 0 }).mul(Mat4.rotationZ(time)))); + cmd_buf.pushFragmentUniformData(0, std.mem.asBytes(&FragMultiplyUniform{ .data = .{ 1, 0.5 + @cos(time), 1, 1 } })); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/textured-quad.zig b/gpu_examples/src/textured-quad.zig new file mode 100644 index 0000000..51761c1 --- /dev/null +++ b/gpu_examples/src/textured-quad.zig @@ -0,0 +1,434 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("texturedQuad.frag"); +const frag_shader_name = "Textured Quad"; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 4, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 4, 4 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 4 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const sampler_names = [_][]const u8{ + "PointClamp", + "PointWrap", + "LinearClamp", + "LinearWrap", + "AnisotropicClamp", + "AnisotropicWrap", +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + samplers: [sampler_names.len]sdl3.gpu.Sampler, + curr_sampler: usize = 0, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Textured Quad", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create samplers. + var samplers: [sampler_names.len]sdl3.gpu.Sampler = undefined; + samplers[0] = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + errdefer device.releaseSampler(samplers[0]); + samplers[1] = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + }); + errdefer device.releaseSampler(samplers[1]); + samplers[2] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + }); + errdefer device.releaseSampler(samplers[2]); + samplers[3] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + }); + errdefer device.releaseSampler(samplers[3]); + samplers[4] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .clamp_to_edge, + .address_mode_v = .clamp_to_edge, + .address_mode_w = .clamp_to_edge, + .max_anisotropy = 4, + }); + errdefer device.releaseSampler(samplers[4]); + samplers[5] = try device.createSampler(.{ + .min_filter = .linear, + .mag_filter = .linear, + .mipmap_mode = .linear, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + .max_anisotropy = 4, + }); + errdefer device.releaseSampler(samplers[5]); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_data_off .. transfer_buffer_image_data_off + image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_image_data_off, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + .samplers = samplers, + }; + + // Finish setup. + try sdl3.log.log("Press left/right to switch between sampler states", .{}); + try sdl3.log.log("Sampler state: {s}", .{sampler_names[state.curr_sampler]}); + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.samplers[app_state.curr_sampler] }, + }, + ); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + if (app_state.curr_sampler == 0) { + app_state.curr_sampler = sampler_names.len - 1; + } else app_state.curr_sampler -= 1; + changed = true; + }, + .right => { + if (app_state.curr_sampler >= sampler_names.len - 1) { + app_state.curr_sampler = 0; + } else app_state.curr_sampler += 1; + changed = true; + }, + else => {}, + }; + if (changed) { + try sdl3.log.log("Sampler state: {s}", .{sampler_names[app_state.curr_sampler]}); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + for (val.samplers) |sampler| + val.device.releaseSampler(sampler); + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/tone-mapping.zig.wip b/gpu_examples/src/tone-mapping.zig.wip new file mode 100644 index 0000000..c23ff89 --- /dev/null +++ b/gpu_examples/src/tone-mapping.zig.wip @@ -0,0 +1,391 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const window_width = 640; +const window_height = 480; + +const linear_to_srgb_source = @embedFile("linearToSrgb.comp"); +const linear_to_srgb_name = "Linear To SRGB"; +const linear_to_st2084_source = @embedFile("linearToSt2084.comp"); +const linear_to_st2084_name = "Linear To ST2084"; + +const swapchain_compositions = [_]struct { + val: sdl3.gpu.SwapchainComposition, + name: [:0]const u8, +}{ + .{ .val = .sdr, .name = "SDR" }, + .{ .val = .sdr_linear, .name = "SDR Linear" }, + .{ .val = .hdr_extended_linear, .name = "HDR Extended Linear" }, + .{ .val = .hdr10_st2084, .name = "HDR10 ST2084" }, +}; + +const tonemap_operators = [_]struct { + shader_source: [:0]const u8, + shader_name: [:0]const u8, + name: [:0]const u8, +}{ + .{ .shader_source = @embedFile("toneMapReinhard.comp"), .shader_name = "Tone Map Reinhard", .name = "Reinhard" }, + .{ .shader_source = @embedFile("toneMapExtendedReinhardLuminance.comp"), .shader_name = "Extended Reinhard Luminance", .name = "ExtendedReinhardLuminance" }, + .{ .shader_source = @embedFile("toneMapHable.comp"), .shader_name = "Hable", .name = "Hable" }, + .{ .shader_source = @embedFile("toneMapAces.comp"), .shader_name = "ACES", .name = "ACES" }, +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + hdr_texture: sdl3.gpu.Texture, + tonemap_texture: sdl3.gpu.Texture, + transfer_texture: sdl3.gpu.Texture, + linear_to_srgb_pipeline: sdl3.gpu.ComputePipeline, + linear_to_srgb_metadata: sdl3.shadercross.ComputePipelineMetadata, + linear_to_st2048_pipeline: sdl3.gpu.ComputePipeline, + linear_to_st2048_metadata: sdl3.shadercross.ComputePipelineMetadata, + tonemap_pipelines: [tonemap_operators.len]sdl3.gpu.ComputePipeline, + tonemap_metadata: [tonemap_operators.len]sdl3.shadercross.ComputePipelineMetadata, + width: usize, + height: usize, + last_successful_swapchain_composition: @typeInfo(@TypeOf(swapchain_compositions)).array.child, + curr_swapchain_composition: usize = 0, + curr_tonemap_operator: usize = 0, +}; + +fn loadComputeShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, +) !struct { pipeline: sdl3.gpu.ComputePipeline, metadata: sdl3.shadercross.ComputePipelineMetadata } { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = .compute, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectComputeSpirv(spirv_code); + return .{ .pipeline = try sdl3.shadercross.compileComputePipelineFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = .compute, + }, spirv_metadata), .metadata = spirv_metadata }; +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Tone Mapping", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Load HDR texture. + const hdr_data: sdl3.surface.Surface = undefined; // TODO!!! + defer hdr_data.deinit(); + const hdr_bytes = hdr_data.getPixels().?[0 .. hdr_data.getWidth() * hdr_data.getHeight() * @sizeOf(f32) * 4]; + const width = hdr_data.getWidth(); + const height = hdr_data.getHeight(); + try window.setSize(hdr_data.getWidth(), hdr_data.getHeight()); + + // Create GPU textures. + const hdr_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r32g32b32a32_float, + .width = @intCast(width), + .height = @intCast(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .compute_storage_read = true }, + .props = .{ .name = "HDR Texture" }, + }); + errdefer device.releaseTexture(hdr_texture); + const tonemap_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r16g16b16a16_float, + .width = @intCast(width), + .height = @intCast(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .compute_storage_read = true, .compute_storage_write = true }, + .props = .{ .name = "Tonemap Texture" }, + }); + errdefer device.releaseTexture(tonemap_texture); + const transfer_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(width), + .height = @intCast(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true, .compute_storage_write = true }, + .props = .{ .name = "Transfer Texture" }, + }); + errdefer device.releaseTexture(transfer_texture); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(hdr_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[0..hdr_bytes.len], hdr_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .texture = hdr_texture, + .width = @intCast(width), + .height = @intCast(height), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Load pipelines. + const linear_to_srgb_res = try loadComputeShader(device, linear_to_srgb_name, linear_to_srgb_source); + errdefer device.releaseComputePipeline(linear_to_srgb_res.pipeline); + const linear_to_st2084_res = try loadComputeShader(device, linear_to_st2084_name, linear_to_st2084_source); + errdefer device.releaseComputePipeline(linear_to_st2084_res.pipeline); + + var tonemap_metadata: [tonemap_operators.len]sdl3.shadercross.ComputePipelineMetadata = undefined; + var tonemap_pipelines: [tonemap_operators.len]sdl3.gpu.ComputePipeline = undefined; + + const tonemap_res_0 = try loadComputeShader(device, tonemap_operators[0].shader_name, tonemap_operators[0].shader_source); + errdefer device.releaseComputePipeline(tonemap_res_0.pipeline); + tonemap_metadata[0] = tonemap_res_0.metadata; + tonemap_pipelines[0] = tonemap_res_0.pipeline; + + const tonemap_res_1 = try loadComputeShader(device, tonemap_operators[1].shader_name, tonemap_operators[1].shader_source); + errdefer device.releaseComputePipeline(tonemap_res_1.pipeline); + tonemap_metadata[1] = tonemap_res_1.metadata; + tonemap_pipelines[1] = tonemap_res_1.pipeline; + + const tonemap_res_2 = try loadComputeShader(device, tonemap_operators[2].shader_name, tonemap_operators[2].shader_source); + errdefer device.releaseComputePipeline(tonemap_res_2.pipeline); + tonemap_metadata[2] = tonemap_res_2.metadata; + tonemap_pipelines[2] = tonemap_res_2.pipeline; + + const tonemap_res_3 = try loadComputeShader(device, tonemap_operators[3].shader_name, tonemap_operators[3].shader_source); + errdefer device.releaseComputePipeline(tonemap_res_3.pipeline); + tonemap_metadata[3] = tonemap_res_3.metadata; + tonemap_pipelines[3] = tonemap_res_3.pipeline; + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .hdr_texture = hdr_texture, + .tonemap_texture = tonemap_texture, + .transfer_texture = transfer_texture, + .linear_to_srgb_pipeline = linear_to_srgb_res.pipeline, + .linear_to_srgb_metadata = linear_to_srgb_res.metadata, + .linear_to_st2048_pipeline = linear_to_st2084_res.pipeline, + .linear_to_st2048_metadata = linear_to_st2084_res.metadata, + .tonemap_pipelines = tonemap_pipelines, + .tonemap_metadata = tonemap_metadata, + .width = width, + .height = height, + .last_successful_swapchain_composition = swapchain_compositions[0], + }; + + // Log state. + try sdl3.log.log("Press left/right to cycle swapchain composition", .{}); + try sdl3.log.log("Press up/down to cycle tonemap operators", .{}); + try sdl3.log.log( + "State: {SwapchainComposition: {any}, TonemapOperator: {any}}}", + .{ state.last_successful_swapchain_composition.name, tonemap_operators[app_state.curr_tonemap_operator].name }, + ); + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Tonemap. + { + const compute_pass = cmd_buf.beginComputePass( + &.{ + .{ .texture = app_state.tonemap_texture, .cycle = true }, + }, + &.{}, + ); + defer compute_pass.end(); + compute_pass.bindPipeline(app_state.tonemap_pipelines[app_state.curr_tonemap_operator]); + compute_pass.bindStorageTextures(0, &.{app_state.hdr_texture}); + const metadata = app_state.tonemap_metadata[app_state.curr_tonemap_operator]; + compute_pass.dispatch(app_state.width / metadata.threadcount_x, app_state.height / metadata.threadcount_y, metadata.threadcount_z); + } + + // Transfer to target color space if necessary. + var blit_src_texture = app_state.tonemap_texture; + const curr_swapchain_composition = swapchain_compositions[app_state.curr_swapchain_composition].val; + if (curr_swapchain_composition == .sdr or curr_swapchain_composition == .hdr10_st2084) { + const compute_pass = cmd_buf.beginComputePass( + &.{ + .{ .texture = app_state.transfer_texture, .cycle = true }, + }, + &.{}, + ); + defer compute_pass.end(); + if (curr_swapchain_composition == .sdr) { + compute_pass.bindPipeline(app_state.linear_to_srgb_pipeline); + } else compute_pass.bindPipeline(app_state.linear_to_st2048_pipeline); + const metadata = if (curr_swapchain_composition == .sdr) app_state.linear_to_srgb_metadata else app_state.linear_to_st2048_metadata; + + compute_pass.bindStorageTextures(0, &.{app_state.tonemap_texture}); + compute_pass.dispatch(app_state.width / metadata.threadcount_x, app_state.height / metadata.threadcount_y, metadata.threadcount_z); + blit_src_texture = app_state.transfer_texture; + } + + // Blit to swapchain. + cmd_buf.blitTexture(.{ + .source = .{ + .texture = blit_src_texture, + .region = .{ .x = 0, .y = 0, .w = app_state.width, .h = app_state.height }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width, .h = swapchain_texture.height }, + }, + .load_op = .do_not_care, + .filter = .nearest, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var swapchain_composition_changed = false; + var tonemap_operator_changed = false; + if (key.key) |val| switch (val) { + .left => { + if (app_state.curr_swapchain_composition == 0) { + app_state.curr_swapchain_composition = swapchain_compositions.len - 1; + } else app_state.curr_swapchain_composition -= 1; + swapchain_composition_changed = true; + }, + .right => { + if (app_state.curr_swapchain_composition >= swapchain_compositions.len - 1) { + app_state.curr_swapchain_composition = 0; + } else app_state.curr_swapchain_composition += 1; + swapchain_composition_changed = true; + }, + .up => { + if (app_state.curr_tonemap_operator == 0) { + app_state.curr_tonemap_operator = tonemap_operators.len - 1; + } else app_state.curr_tonemap_operator -= 1; + tonemap_operator_changed = true; + }, + .down => { + if (app_state.curr_tonemap_operator >= tonemap_operators.len - 1) { + app_state.curr_tonemap_operator = 0; + } else app_state.curr_tonemap_operator += 1; + tonemap_operator_changed = true; + }, + else => {}, + }; + if (swapchain_composition_changed) { + if (app_state.device.windowSupportsSwapchainComposition(app_state.window, swapchain_compositions[app_state.curr_swapchain_composition].val)) { + app_state.last_successful_swapchain_composition = swapchain_compositions[app_state.curr_swapchain_composition]; + try app_state.device.setSwapchainParameters(app_state.window, app_state.last_successful_swapchain_composition.val, .vsync); + } else { + swapchain_composition_changed = false; + try sdl3.log.Category.gpu.logError("Swapchain composition to {s} unsupported", .{swapchain_compositions[app_state.curr_swapchain_composition].name}); + } + } + if (swapchain_composition_changed or tonemap_operator_changed) { + try sdl3.log.log( + "State: {SwapchainComposition: {any}, TonemapOperator: {any}}}", + .{ app_state.last_successful_swapchain_composition.name, tonemap_operators[app_state.curr_tonemap_operator].name }, + ); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + for (val.tonemap_pipelines) |pipeline| + val.device.releaseComputePipeline(pipeline); + val.device.releaseComputePipeline(val.linear_to_st2048_pipeline); + val.device.releaseComputePipeline(val.linear_to_srgb_pipeline); + val.device.releaseTexture(val.transfer_texture); + val.device.releaseTexture(val.tone_map_texture); + val.device.releaseTexture(val.hdr_texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/triangle-msaa.zig b/gpu_examples/src/triangle-msaa.zig new file mode 100644 index 0000000..9dccbcc --- /dev/null +++ b/gpu_examples/src/triangle-msaa.zig @@ -0,0 +1,285 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("rawTriangle.vert"); +const vert_shader_name = "Raw Triangle"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const window_width = 640; +const window_height = 480; +const small_viewport = sdl3.gpu.Viewport{ + .region = .{ .x = 100, .y = 120, .w = 320, .h = 240 }, + .min_depth = 0.1, + .max_depth = 1.0, +}; +const scissor_rect = sdl3.rect.IRect{ .x = 320, .y = 240, .w = 320, .h = 240 }; + +const max_num_samples = 4; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipelines: [max_num_samples]sdl3.gpu.GraphicsPipeline, + msaaRenderTextures: [max_num_samples]sdl3.gpu.Texture, + resolve_texture: sdl3.gpu.Texture, + sample_counts: usize, + curr_sample_count_ind: usize = 0, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Triangle MSAA", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const swapchain_format = try device.getSwapchainTextureFormat(window); + var pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = swapchain_format, + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + }; + var pipelines: [max_num_samples]sdl3.gpu.GraphicsPipeline = undefined; + var textures: [max_num_samples]sdl3.gpu.Texture = undefined; + var sample_counts: usize = 0; + + // Hacky way to cleanup resources in case of failure. + const Cb = struct { + fn cleanup( + p_device: sdl3.gpu.Device, + p_pipelines: [max_num_samples]sdl3.gpu.GraphicsPipeline, + p_textures: [max_num_samples]sdl3.gpu.Texture, + num_pipelines: usize, + num_textures: usize, + ) void { + for (0..num_pipelines) |ind| + p_device.releaseGraphicsPipeline(p_pipelines[ind]); + + for (0..num_textures) |ind| + p_device.releaseTexture(p_textures[ind]); + } + }; + for (0..max_num_samples) |num_samples| { + const sample_count: sdl3.gpu.SampleCount = @enumFromInt(num_samples); + if (!device.textureSupportsSampleCount(swapchain_format, sample_count)) + continue; + pipeline_create_info.multisample_state = .{ .sample_count = sample_count }; + + pipelines[sample_counts] = device.createGraphicsPipeline(pipeline_create_info) catch { + Cb.cleanup(device, pipelines, textures, sample_counts, sample_counts); + return .failure; + }; + textures[sample_counts] = device.createTexture(.{ + .texture_type = .two_dimensional, + .width = window_width, + .height = window_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .format = swapchain_format, + .usage = .{ .color_target = true, .sampler = sample_count == .no_multisampling }, + .sample_count = sample_count, + }) catch { + Cb.cleanup(device, pipelines, textures, sample_counts + 1, sample_counts); + return .failure; + }; + sample_counts += 1; + } + errdefer Cb.cleanup(device, pipelines, textures, sample_counts, sample_counts); + + // Create resolve texture. + const resolve_texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .width = window_width, + .height = window_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .format = swapchain_format, + .usage = .{ .color_target = true, .sampler = true }, + }); + errdefer device.releaseTexture(resolve_texture); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipelines = pipelines, + .msaaRenderTextures = textures, + .resolve_texture = resolve_texture, + .sample_counts = sample_counts, + }; + + // Finish setup. + app_state.* = state; + try sdl3.log.log("Press left/right to cycle between sample counts", .{}); + try sdl3.log.log( + "Sample Count: {s}", + .{@tagName(@as(sdl3.gpu.SampleCount, @enumFromInt(state.curr_sample_count_ind)))}, + ); + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const resolve = @as(sdl3.gpu.SampleCount, @enumFromInt(app_state.curr_sample_count_ind)) != .no_multisampling; + { + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = app_state.msaaRenderTextures[app_state.curr_sample_count_ind], + .clear_color = .{ .r = 1, .g = 1, .b = 1, .a = 1 }, + .load = .clear, + .store = if (resolve) .resolve else .store, + .resolve_texture = if (resolve) app_state.resolve_texture else .null, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipelines[app_state.curr_sample_count_ind]); + render_pass.drawPrimitives(3, 1, 0, 0); + } + cmd_buf.blitTexture(.{ + .source = .{ + .texture = if (resolve) app_state.resolve_texture else app_state.msaaRenderTextures[app_state.curr_sample_count_ind], + .region = .{ .x = window_width / 4, .y = 0, .w = window_width / 2, .h = window_height / 2 }, + }, + .destination = .{ + .texture = texture, + .region = .{ .x = 0, .y = 0, .w = swapchain_texture.width, .h = swapchain_texture.height }, + }, + .load_op = .do_not_care, + .filter = .linear, + }); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + if (app_state.curr_sample_count_ind == 0) { + app_state.curr_sample_count_ind = app_state.sample_counts - 1; + } else app_state.curr_sample_count_ind -= 1; + changed = true; + }, + .right => { + if (app_state.curr_sample_count_ind >= app_state.sample_counts - 1) { + app_state.curr_sample_count_ind = 0; + } else app_state.curr_sample_count_ind += 1; + changed = true; + }, + else => {}, + }; + if (changed) { + try sdl3.log.log( + "Sample Count: {s}", + .{@tagName(@as(sdl3.gpu.SampleCount, @enumFromInt(app_state.curr_sample_count_ind)))}, + ); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + for (0..val.sample_counts) |ind| { + val.device.releaseGraphicsPipeline(val.pipelines[ind]); + val.device.releaseTexture(val.msaaRenderTextures[ind]); + } + val.device.releaseTexture(val.resolve_texture); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_examples/src/window-resize.zig b/gpu_examples/src/window-resize.zig new file mode 100644 index 0000000..2631815 --- /dev/null +++ b/gpu_examples/src/window-resize.zig @@ -0,0 +1,202 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +const vert_shader_source = @embedFile("rawTriangle.vert"); +const vert_shader_name = "Raw Triangle"; +const frag_shader_source = @embedFile("solidColor.frag"); +const frag_shader_name = "Solid Color"; + +const resolutions = [_]@Vector(2, u32){ + .{ 640, 480 }, + .{ 1280, 720 }, + .{ 1024, 1024 }, + .{ 1600, 900 }, + .{ 1920, 1080 }, + .{ 3200, 1800 }, + .{ 3840, 2160 }, +}; + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + resolution_index: usize = 0, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Window Resize", resolutions[0][0], resolutions[0][1], .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline = try device.createGraphicsPipeline(.{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + }); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + }; + + // Finish setup. + app_state.* = state; + try sdl3.log.log("Press left/right to resize the window", .{}); + try sdl3.log.log( + "Resolution: {d}x{d}", + .{ resolutions[state.resolution_index][0], resolutions[state.resolution_index][1] }, + ); + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.drawPrimitives(3, 1, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + switch (curr_event) { + .key_down => |key| { + if (!key.repeat) { + var changed = false; + if (key.key) |val| switch (val) { + .left => { + if (app_state.resolution_index == 0) { + app_state.resolution_index = resolutions.len - 1; + } else app_state.resolution_index -= 1; + changed = true; + }, + .right => { + if (app_state.resolution_index >= resolutions.len - 1) { + app_state.resolution_index = 0; + } else app_state.resolution_index += 1; + changed = true; + }, + else => {}, + }; + if (changed) { + try app_state.window.setSize(resolutions[app_state.resolution_index][0], resolutions[app_state.resolution_index][1]); + try app_state.window.setPosition(.{ .centered = null }, .{ .centered = null }); + try app_state.window.sync(); + try sdl3.log.log( + "Resolution: {d}x{d}", + .{ resolutions[app_state.resolution_index][0], resolutions[app_state.resolution_index][1] }, + ); + } + } + }, + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_template/build.zig b/gpu_template/build.zig new file mode 100644 index 0000000..2c2c4fa --- /dev/null +++ b/gpu_template/build.zig @@ -0,0 +1,176 @@ +const std = @import("std"); + +const ShaderFormat = enum { + vertex, + fragment, + compute, +}; + +const OutputFormat = enum { + dxbc, + dxil, + msl, + spirv, + hlsl, +}; + +fn shadercross(b: *std.Build, sdl3: *std.Build.Dependency) *std.Build.Step.Compile { + const exe = b.addExecutable(.{ + .name = "shadercross", + .root_module = b.createModule(.{ + .target = b.graph.host, + .link_libc = true, + }), + }); + exe.root_module.linkLibrary(sdl3.builder.dependency("sdl", .{ .target = b.graph.host }).artifact("SDL3")); + + const upstream = sdl3.builder.dependency("sdl_shadercross", .{ .target = b.graph.host }); + exe.root_module.addIncludePath(upstream.path("include")); + exe.root_module.addIncludePath(upstream.path("src")); + + if (b.lazyDependency("mach_dxcompiler", .{ + .target = b.graph.target, + .spirv = true, + .skip_executables = true, + .skip_tests = true, + .from_source = true, + .shared = false, + })) |dxcompiler| { + exe.linkLibrary(dxcompiler.artifact("machdxcompiler")); + exe.defineCMacro("SDL_SHADERCROSS_DXC", "1"); + } + + exe.root_module.addCSourceFiles(.{ + .root = upstream.path("src"), + .files = &.{ + "SDL_shadercross.c", + "cli.c", + }, + .flags = &.{ + "-DDXC=1", + }, + }); + + exe.installHeadersDirectory(upstream.path("include"), "", .{}); + + const spirv_headers = sdl3.builder.dependency("spirv_headers", .{ + .target = b.graph.host, + }); + const spirv_cross = sdl3.builder.dependency("spirv_cross", .{ + .target = b.graph.host, + .optimize = .ReleaseFast, // There is a C bug in spirv-cross upstream! Ignore undefined behavior for now. + .spv_cross_reflect = true, + .spv_cross_cpp = false, + }); + exe.linkLibrary(spirv_cross.artifact("spirv-cross-c")); + exe.addIncludePath(spirv_headers.path("include/spirv/1.2/")); + + return exe; +} + +fn setupShader( + b: *std.Build, + module: *std.Build.Module, + name: []const u8, + shadercross_exe: *std.Build.Step.Compile, + debug: bool, + output_format: OutputFormat, +) !void { + const name_ext = name[std.mem.indexOf(u8, name, ".").?..]; + const format: ShaderFormat = if (std.mem.eql(u8, name_ext, ".vert")) + .vertex + else if (std.mem.eql(u8, name_ext, ".frag")) + .fragment + else + .compute; + const run_shadercross = b.addRunArtifact(shadercross_exe); + const upper = try std.ascii.allocUpperString(b.allocator, @tagName(output_format)); + run_shadercross.addFileArg(b.path(b.fmt("{s}/{s}.hlsl", .{ "shaders", name }))); + run_shadercross.addArgs(&.{ "--source", "HLSL", "--entrypoint", "main", "--stage", @tagName(format), "--dest", upper }); + if (debug) + run_shadercross.addArg("--debug"); + run_shadercross.addArg("--output"); + const output = run_shadercross.addOutputFileArg(b.fmt("{s}.{s}", .{ name, @tagName(output_format) })); + module.addAnonymousImport(name, .{ .root_source_file = output }); +} + +fn buildShaders( + b: *std.Build, + exe: *std.Build.Step.Compile, + shadercross_exe: *std.Build.Step.Compile, + debug: bool, + output_format: OutputFormat, +) !void { + var dir = (try std.fs.openDirAbsolute(b.path("shaders").getPath(b), .{ .iterate = true })); + defer dir.close(); + var dir_iterator = try dir.walk(b.allocator); + defer dir_iterator.deinit(); + while (try dir_iterator.next()) |file| { + if (file.kind == .file) { + const extension = ".hlsl"; + if (!std.mem.endsWith(u8, file.basename, extension)) + continue; + try setupShader( + b, + exe.root_module, + file.basename[0..(file.basename.len - extension.len)], + shadercross_exe, + debug, + output_format, + ); + } + } +} + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + const exe = b.addExecutable(.{ + .name = "template", + .root_module = exe_mod, + }); + + const sdl3 = b.dependency("sdl3", .{ + .target = target, + .optimize = optimize, + .callbacks = true, + .ext_image = true, + }); + exe.root_module.addImport("sdl3", sdl3.module("sdl3")); + const shadercross_exe = shadercross( + b, + sdl3, + ); + const options = b.addOptions(); + const gpu_debug = b.option(bool, "gpu_debug", "If to have enable GPU debug mode") orelse false; + const shader_debug = b.option(bool, "shader_debug", "If to have debug info in the shaders") orelse false; + const shader_format = b.option(OutputFormat, "shader_format", "Output shader format") orelse .spirv; + options.addOption(bool, "gpu_debug", gpu_debug); + options.addOption(bool, "shader_debug", shader_debug); + options.addOption(OutputFormat, "shader_format", shader_format); + try buildShaders( + b, + exe, + shadercross_exe, + shader_debug, + shader_format, + ); + b.installArtifact(exe); + exe.root_module.addOptions("options", options); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/gpu_template/build.zig.zon b/gpu_template/build.zig.zon new file mode 100644 index 0000000..9f6fd9b --- /dev/null +++ b/gpu_template/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = .gpu_template, + .version = "0.0.0", + .dependencies = .{ + .sdl3 = .{ + .path = "../", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, + .fingerprint = 0xa5bd7236fc5f4b29, +} diff --git a/gpu_template/shaders/positionColor.vert.hlsl b/gpu_template/shaders/positionColor.vert.hlsl new file mode 100644 index 0000000..8b106f1 --- /dev/null +++ b/gpu_template/shaders/positionColor.vert.hlsl @@ -0,0 +1,19 @@ +struct Input +{ + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; +}; + +struct Output +{ + float4 Color : TEXCOORD0; + float4 Position : SV_Position; +}; + +Output main(Input input) +{ + Output output; + output.Color = input.Color; + output.Position = float4(input.Position, 1.0f); + return output; +} diff --git a/gpu_template/shaders/solidColor.frag.hlsl b/gpu_template/shaders/solidColor.frag.hlsl new file mode 100644 index 0000000..a8e6d9f --- /dev/null +++ b/gpu_template/shaders/solidColor.frag.hlsl @@ -0,0 +1,4 @@ +float4 main(float4 Color : TEXCOORD0) : SV_Target0 +{ + return Color; +} diff --git a/gpu_template/src/main.zig b/gpu_template/src/main.zig new file mode 100644 index 0000000..7e2cd16 --- /dev/null +++ b/gpu_template/src/main.zig @@ -0,0 +1,257 @@ +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +/// Allocator we will use. +const allocator = std.heap.smp_allocator; + +// Load shaders. +const Shader = @import("shader.zig"); + +fn importShader(shader: anytype) Shader { + return .{ + .code = shader.code, + .format = shader.format, + .num_samplers = shader.num_samplers, + .num_storage_buffers = shader.num_storage_buffers, + .num_storage_textures = shader.num_storage_textures, + .num_uniform_buffers = shader.num_uniform_buffers, + .stage = shader.stage, + }; +} +const vert_shader = importShader(@import("positionColor.vert.zig")); +const frag_shader = importShader(@import("solidColor.frag.zig")); + +const window_width = 640; +const window_height = 480; + +const PositionColorVertex = packed struct { + position: @Vector(3, f32), + color: @Vector(4, u8), +}; + +const vertices = [_]PositionColorVertex{ + .{ .position = .{ -1, -1, 0 }, .color = .{ 255, 0, 0, 255 } }, + .{ .position = .{ 1, -1, 0 }, .color = .{ 0, 255, 0, 255 } }, + .{ .position = .{ 0, 1, 0 }, .color = .{ 0, 0, 255, 255 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, +}; + +fn loadGraphicsShader( + device: sdl3.gpu.Device, + name: ?[:0]const u8, + shader_code: [:0]const u8, + stage: sdl3.shadercross.ShaderStage, +) !sdl3.gpu.Shader { + const spirv_code = if (options.spirv) shader_code else try sdl3.shadercross.compileSpirvFromHlsl(.{ + .defines = null, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .include_dir = null, + .name = name, + .shader_stage = stage, + .source = shader_code, + }); + const spirv_metadata = try sdl3.shadercross.reflectGraphicsSpirv(spirv_code); + return try sdl3.shadercross.compileGraphicsShaderFromSpirv(device, .{ + .bytecode = spirv_code, + .enable_debug = options.gpu_debug, + .entry_point = "main", + .name = name, + .shader_stage = stage, + }, spirv_metadata); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + try sdl3.init(.{ .video = true }); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports SPIR-V. + const shader_formats = sdl3.shadercross.getSpirvShaderFormats() orelse @panic("No formats available"); + const device = try sdl3.gpu.Device.init(shader_formats, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Basic Vertex Buffer", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try loadGraphicsShader(device, vert_shader_name, vert_shader_source, .vertex); + defer device.releaseShader(vertex_shader); + const fragment_shader = try loadGraphicsShader(device, frag_shader_name, frag_shader_source, .fragment); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionColorVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionColorVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .u8x4_normalized, + .offset = @offsetOf(PositionColorVertex, "color"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Setup transfer buffer. + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = vertices_bytes.len, + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped, vertices_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = 0, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture.texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.drawPrimitives(3, 1, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + if (app_state) |val| { + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } +} diff --git a/gpu_template/src/shader.zig b/gpu_template/src/shader.zig new file mode 100644 index 0000000..c954b9a --- /dev/null +++ b/gpu_template/src/shader.zig @@ -0,0 +1,16 @@ +const sdl3 = @import("sdl3"); + +/// Shader code. +code: []const u8, +/// The format of the shader code. +format: sdl3.gpu.ShaderFormatFlags, +/// The stage the shader program corresponds to. +stage: sdl3.gpu.ShaderStage, +/// The number of samplers defined in the shader. +num_samplers: u32 = 0, +/// The number of storage textures defined in the shader. +num_storage_textures: u32 = 0, +/// The number of storage buffers defined in the shader. +num_storage_buffers: u32 = 0, +/// The number of uniform buffers defined in the shader. +num_uniform_buffers: u32 = 0, diff --git a/gpu_zig_reflection/build.zig b/gpu_zig_reflection/build.zig new file mode 100644 index 0000000..2a957d2 --- /dev/null +++ b/gpu_zig_reflection/build.zig @@ -0,0 +1,199 @@ +const std = @import("std"); + +const ShaderFormat = enum { + vertex, + fragment, + compute, +}; + +const OutputFormat = enum { + dxbc, + dxil, + msl, + spirv, +}; + +fn shadercross(b: *std.Build, sdl3: *std.Build.Dependency) *std.Build.Step.Compile { + const exe = b.addExecutable(.{ + .name = "shadercross", + .root_module = b.createModule(.{ + .target = b.graph.host, + .link_libc = true, + }), + }); + exe.root_module.linkLibrary(sdl3.builder.dependency("sdl", .{ .target = b.graph.host }).artifact("SDL3")); + + const upstream = sdl3.builder.dependency("sdl_shadercross", .{ .target = b.graph.host }); + exe.root_module.addIncludePath(upstream.path("include")); + exe.root_module.addIncludePath(upstream.path("src")); + + exe.root_module.addCSourceFiles(.{ + .root = upstream.path("src"), + .files = &.{ + "SDL_shadercross.c", + "cli.c", + }, + .flags = &.{ + "-DDXC=1", + }, + }); + + exe.installHeadersDirectory(upstream.path("include"), "", .{}); + + const spirv_headers = sdl3.builder.dependency("spirv_headers", .{ + .target = b.graph.host, + }); + const spirv_cross = sdl3.builder.dependency("spirv_cross", .{ + .target = b.graph.host, + .optimize = .ReleaseFast, // There is a C bug in spirv-cross upstream! Ignore undefined behavior for now. + .spv_cross_reflect = true, + .spv_cross_cpp = false, + }); + exe.linkLibrary(spirv_cross.artifact("spirv-cross-c")); + exe.addIncludePath(spirv_headers.path("include/spirv/1.2/")); + + return exe; +} + +fn setupShader( + b: *std.Build, + module: *std.Build.Module, + name: []const u8, + shadercross_exe: *std.Build.Step.Compile, + debug: bool, + output_format: OutputFormat, +) !void { + const obj = b.addObject(.{ + .name = name, + .root_module = b.addModule(name, .{ + .root_source_file = b.path(b.fmt("shaders/{s}.zig", .{name})), + .target = b.resolveTargetQuery(.{ + .cpu_arch = .spirv64, + .cpu_model = .{ .explicit = &std.Target.spirv.cpu.vulkan_v1_2 }, + .cpu_features_add = std.Target.spirv.featureSet(&.{}), + .os_tag = .vulkan, + .ofmt = .spirv, + }), + }), + .use_llvm = false, + .use_lld = false, + }); + var shader_out = obj.getEmittedBin(); + + // Optimize SPIR-V if possible. + if (b.findProgram(&.{"spirv-opt"}, &.{})) |spirv_opt| { + + // Remove duplicate type definitions that might be done by inline assembly. + const spirv_fix = b.addSystemCommand(&.{ spirv_opt, "--remove-duplicates", "--skip-validation" }); + spirv_fix.addFileArg(obj.getEmittedBin()); + spirv_fix.addArg("-o"); + const fixed_spirv = spirv_fix.addOutputFileArg(b.fmt("{s}-fixed.spv", .{name})); + + // Optimize the SPIRV. + const spirv_opt_cmd = b.addSystemCommand(&.{ spirv_opt, "-O" }); + spirv_opt_cmd.addFileArg(fixed_spirv); + spirv_opt_cmd.addArg("-o"); + shader_out = spirv_opt_cmd.addOutputFileArg(b.fmt("{s}-opt.spv", .{name})); + } else |err| switch (err) { + error.FileNotFound => std.debug.print("spirv-opt not found, shader output will be unoptimized!\n", .{}), + } + + if (output_format != .spirv) { + const name_ext = name[std.mem.indexOf(u8, name, ".").?..]; + const format: ShaderFormat = if (std.mem.eql(u8, name_ext, ".vert")) + .vertex + else if (std.mem.eql(u8, name_ext, ".frag")) + .fragment + else + .compute; + const run_shadercross = b.addRunArtifact(shadercross_exe); + const upper = try std.ascii.allocUpperString(b.allocator, @tagName(output_format)); + run_shadercross.addFileArg(shader_out); + run_shadercross.addArgs(&.{ "--source", "SPIRV", "--entrypoint", "main", "--stage", @tagName(format), "--dest", upper }); + if (debug) + run_shadercross.addArg("--debug"); + run_shadercross.addArg("--output"); + shader_out = run_shadercross.addOutputFileArg(b.fmt("{s}.{s}", .{ name, @tagName(output_format) })); + } + module.addAnonymousImport(name, .{ .root_source_file = shader_out }); +} + +fn buildShaders( + b: *std.Build, + exe: *std.Build.Step.Compile, + shadercross_exe: *std.Build.Step.Compile, + debug: bool, + output_format: OutputFormat, +) !void { + var dir = (try std.fs.openDirAbsolute(b.path("shaders").getPath(b), .{ .iterate = true })); + defer dir.close(); + var dir_iterator = dir.iterate(); + while (try dir_iterator.next()) |file| { + if (file.kind == .file) { + const extension = ".zig"; + if (!std.mem.endsWith(u8, file.name, extension)) + continue; + try setupShader( + b, + exe.root_module, + file.name[0..(file.name.len - extension.len)], + shadercross_exe, + debug, + output_format, + ); + } + } +} + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + const exe = b.addExecutable(.{ + .name = "template", + .root_module = exe_mod, + }); + + const sdl3 = b.dependency("sdl3", .{ + .target = target, + .optimize = optimize, + .callbacks = true, + .ext_image = true, + }); + exe.root_module.addImport("sdl3", sdl3.module("sdl3")); + const shadercross_exe = shadercross( + b, + sdl3, + ); + const options = b.addOptions(); + const gpu_debug = b.option(bool, "gpu_debug", "If to have enable GPU debug mode") orelse false; + const shader_debug = b.option(bool, "shader_debug", "If to have debug info in the shaders") orelse false; + const shader_format = b.option(OutputFormat, "shader_format", "Output shader format") orelse .spirv; + options.addOption(bool, "gpu_debug", gpu_debug); + options.addOption(bool, "shader_debug", shader_debug); + options.addOption(OutputFormat, "shader_format", shader_format); + try buildShaders( + b, + exe, + shadercross_exe, + shader_debug, + shader_format, + ); + b.installArtifact(exe); + exe.root_module.addOptions("options", options); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/gpu_zig_reflection/build.zig.zon b/gpu_zig_reflection/build.zig.zon new file mode 100644 index 0000000..e97dea5 --- /dev/null +++ b/gpu_zig_reflection/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = .gpu_zig_reflection, + .version = "0.0.0", + .dependencies = .{ + .sdl3 = .{ + .path = "../", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, + .fingerprint = 0xaf93ee63e171683e, +} diff --git a/gpu_zig_reflection/shaders/common/common.zig b/gpu_zig_reflection/shaders/common/common.zig new file mode 100644 index 0000000..1b770c7 --- /dev/null +++ b/gpu_zig_reflection/shaders/common/common.zig @@ -0,0 +1,368 @@ +/// SPIR-V on zig currently can not use cosine, use some inline assembly to steal from GLSL's extended instruction set. +/// +/// ## Function Parameters +/// * `val`: The scalar or vector to get the cosine of. +/// +/// ## Return Value +/// Returns a scalar or vector with the cosines of each element. +pub fn cos(val: anytype) @TypeOf(val) { + // https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html + return asm volatile ( + \\%glsl_ext = OpExtInstImport "GLSL.std.450" + \\%ret = OpExtInst %val_type %glsl_ext 14 %val + : [ret] "" (-> @TypeOf(val)), + : [val] "" (val), + [val_type] "t" (@TypeOf(val)), + ); +} + +/// SPIR-V on zig currently can not use sine, use some inline assembly to steal from GLSL's extended instruction set. +/// +/// ## Function Parameters +/// * `val`: The scalar or vector to get the sine of. +/// +/// ## Return Value +/// Returns a scalar or vector with the sines of each element. +pub fn sin(val: anytype) @TypeOf(val) { + // https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html + return asm volatile ( + \\%glsl_ext = OpExtInstImport "GLSL.std.450" + \\%ret = OpExtInst %val_type %glsl_ext 13 %val + : [ret] "" (-> @TypeOf(val)), + : [val] "" (val), + [val_type] "t" (@TypeOf(val)), + ); +} + +/// Create a runtime array for a type. +/// +/// ## Function Parameters +/// * `set`: The binding set of the array. +/// * `bind`: The binding slot of the array. +/// * `Type`: Type of element stored in the array. +/// +/// ## Return Value +/// Returns the runtime array. +pub fn RuntimeArray( + comptime set: u32, + comptime bind: u32, + comptime Type: type, +) type { + return struct { + /// Read from a runtime array storage buffer. + /// + /// ## Function Parameters + /// * `index`: Index to access the element in the runtime array. + /// + /// ## Return Value + /// Returns the value at the given index in the array. + pub fn read( + index: u32, + ) Type { + return asm volatile ( + \\%int = OpTypeInt 32 1 + \\%zero = OpConstant %int 0 + \\%uniform_ptr_type = OpTypePointer StorageBuffer %entry_type + \\%arr = OpTypeRuntimeArray %entry_type + \\%compute_buffer = OpTypeStruct %arr + \\%uniform_type = OpTypePointer StorageBuffer %compute_buffer + \\%uniform = OpVariable %uniform_type StorageBuffer + \\ OpDecorate %uniform DescriptorSet $set + \\ OpDecorate %uniform Binding $bind + \\%access = OpAccessChain %uniform_ptr_type %uniform %zero %index + \\%ret = OpLoad %entry_type %access + : [ret] "" (-> Type), + : [entry_type] "t" (Type), + [index] "" (index), + [set] "c" (set), + [bind] "c" (bind), + ); + } + + /// Write to a runtime array storage buffer. + /// + /// ## Function Parameters + /// * `index`: Index to access the element in the runtime array. + /// * `val`: Value to write to the given index in the runtime array. + pub fn write( + index: u32, + val: Type, + ) void { + asm volatile ( + \\%int = OpTypeInt 32 1 + \\%zero = OpConstant %int 0 + \\%uniform_ptr_type = OpTypePointer StorageBuffer %entry_type + \\%arr = OpTypeRuntimeArray %entry_type + \\%compute_buffer = OpTypeStruct %arr + \\%uniform_type = OpTypePointer StorageBuffer %compute_buffer + \\%uniform = OpVariable %uniform_type StorageBuffer + \\ OpDecorate %uniform DescriptorSet $set + \\ OpDecorate %uniform Binding $bind + \\%access = OpAccessChain %uniform_ptr_type %uniform %zero %index + \\ OpStore %access %val + : + : [entry_type] "t" (Type), + [index] "" (index), + [val] "" (val), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a 2d sampler. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d sampler object. +pub fn Sampler2d( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Get the texture size of a 2d sampler. + /// + /// ## Function Parameters + /// * `lod`: The LOD to sample at. + /// + /// ## Return Value + /// Returns the sampler texture size. + pub fn size( + lod: i32, + ) @Vector(2, i32) { + return asm volatile ( + \\ OpCapability ImageQuery + \\%float = OpTypeFloat 32 + \\%int = OpTypeInt 32 1 + \\%v2int = OpTypeVector %int 2 + \\%img_type = OpTypeImage %float 2D 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%loaded_image = OpImage %img_type %loaded_sampler + \\%ret = OpImageQuerySizeLod %v2int %loaded_image %lod + : [ret] "" (-> @Vector(2, i32)), + : [set] "c" (set), + [bind] "c" (bind), + [lod] "" (lod), + ); + } + + /// Sample the 2d sampler at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn texture( + uv: @Vector(2, f32), + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleImplicitLod %v4float %loaded_sampler %uv + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [set] "c" (set), + [bind] "c" (bind), + ); + } + + /// Sample a 2d sampler at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// * `lod`: The LOD to sample with. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn textureLod( + uv: @Vector(2, f32), + lod: f32, + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleExplicitLod %v4float %loaded_sampler %uv Lod %lod + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [lod] "" (lod), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a 2d sampler array. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d sampler array object. +pub fn Sampler2dArray( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Sample the 2d sampler array at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn texture( + uv: @Vector(3, f32), + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 1 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleImplicitLod %v4float %loaded_sampler %uv + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a cube sampler. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d sampler object. +pub fn SamplerCube( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Sample a cube sampler at a given UV. + /// + /// ## Function Parameters + /// * `uv`: The UV to sample at. + /// + /// ## Return Value + /// Returns the sampled color value. + pub fn texture( + uv: @Vector(3, f32), + ) @Vector(4, f32) { + return asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float Cube 0 0 0 1 Unknown + \\%sampler_type = OpTypeSampledImage %img_type + \\%sampler_ptr = OpTypePointer UniformConstant %sampler_type + \\%tex = OpVariable %sampler_ptr UniformConstant + \\ OpDecorate %tex DescriptorSet $set + \\ OpDecorate %tex Binding $bind + \\%loaded_sampler = OpLoad %sampler_type %tex + \\%ret = OpImageSampleImplicitLod %v4float %loaded_sampler %uv + : [ret] "" (-> @Vector(4, f32)), + : [uv] "" (uv), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} + +/// Create a 2d texture in RGBA8 format. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// +/// ## Return Value +/// The 2d RGBA8 texture object. +pub fn Texture2dRgba8( + comptime set: u32, + comptime bind: u32, +) type { + return struct { + /// Get the texture size of a 2d RGBA8 texture. + /// + /// ## Return Value + /// Returns the texture size. + pub fn size() @Vector(2, i32) { + return asm volatile ( + \\ OpCapability ImageQuery + \\%float = OpTypeFloat 32 + \\%int = OpTypeInt 32 1 + \\%v2int = OpTypeVector %int 2 + \\%img_type = OpTypeImage %float 2D 0 0 0 2 Rgba8 + \\%img_ptr = OpTypePointer UniformConstant %img_type + \\%img = OpVariable %img_ptr UniformConstant + \\ OpDecorate %img DescriptorSet $set + \\ OpDecorate %img Binding $bind + \\%loaded_image = OpLoad %img_type %img + \\%ret = OpImageQuerySize %v2int %loaded_image + : [ret] "" (-> @Vector(2, i32)), + : [set] "c" (set), + [bind] "c" (bind), + ); + } + + /// Store to a 2d RGBA8 texture. + /// + /// ## Function Parameters + /// * `uv`: The UV to store to. + /// * `pixel`: The pixel data to store. + pub fn store( + uv: @Vector(2, u32), + pixel: @Vector(4, f32), + ) void { + asm volatile ( + \\%float = OpTypeFloat 32 + \\%v4float = OpTypeVector %float 4 + \\%img_type = OpTypeImage %float 2D 0 0 0 2 Rgba8 + \\%img_ptr = OpTypePointer UniformConstant %img_type + \\%img = OpVariable %img_ptr UniformConstant + \\ OpDecorate %img DescriptorSet $set + \\ OpDecorate %img Binding $bind + \\%loaded_image = OpLoad %img_type %img + \\ OpImageWrite %loaded_image %uv %pixel + : + : [uv] "" (uv), + [pixel] "" (pixel), + [set] "c" (set), + [bind] "c" (bind), + ); + } + }; +} diff --git a/gpu_zig_reflection/shaders/texturedQuad.frag.zig b/gpu_zig_reflection/shaders/texturedQuad.frag.zig new file mode 100644 index 0000000..7bd25b3 --- /dev/null +++ b/gpu_zig_reflection/shaders/texturedQuad.frag.zig @@ -0,0 +1,16 @@ +const common = @import("common/common.zig"); +const std = @import("std"); + +const tex0 = common.Sampler2d(2, 0); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = tex0.texture(tex_coord_in); +} diff --git a/gpu_zig_reflection/shaders/texturedQuad.vert.zig b/gpu_zig_reflection/shaders/texturedQuad.vert.zig new file mode 100644 index 0000000..eb7ca40 --- /dev/null +++ b/gpu_zig_reflection/shaders/texturedQuad.vert.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +extern var position_in: @Vector(3, f32) addrspace(.input); +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var tex_coord_out: @Vector(2, f32) addrspace(.output); + +export fn main() callconv(.spirv_vertex) void { + std.gpu.location(&position_in, 0); + std.gpu.location(&tex_coord_in, 1); + + std.gpu.location(&tex_coord_out, 0); + + std.gpu.position_out.* = .{ position_in[0], position_in[1], position_in[2], 1 }; + tex_coord_out = tex_coord_in; +} diff --git a/gpu_zig_reflection/src/images/ravioli.bmp b/gpu_zig_reflection/src/images/ravioli.bmp new file mode 100644 index 0000000..ad13692 Binary files /dev/null and b/gpu_zig_reflection/src/images/ravioli.bmp differ diff --git a/gpu_zig_reflection/src/main.zig b/gpu_zig_reflection/src/main.zig new file mode 100644 index 0000000..f76b6ba --- /dev/null +++ b/gpu_zig_reflection/src/main.zig @@ -0,0 +1,360 @@ +const builtin = @import("builtin"); +const options = @import("options"); +const sdl3 = @import("sdl3"); +const std = @import("std"); + +comptime { + _ = sdl3.main_callbacks; +} + +// Disable main hack. +pub const _start = void; +pub const WinMainCRTStartup = void; + +// Allocator setup. +var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + +fn getAllocator() std.mem.Allocator { + return if (builtin.mode == .Debug) debug_allocator.allocator() else std.heap.smp_allocator; +} + +const init_flags = sdl3.InitFlags{ .video = true }; + +const vert_shader_source = @embedFile("texturedQuad.vert"); +const vert_shader_name = "Textured Quad"; +const frag_shader_source = @embedFile("texturedQuad.frag"); +const frag_shader_name = "Textured Quad"; + +const shader_format = switch (options.shader_format) { + .dxbc => sdl3.gpu.ShaderFormatFlags{ .dxbc = true }, + .dxil => sdl3.gpu.ShaderFormatFlags{ .dxil = true }, + .msl => sdl3.gpu.ShaderFormatFlags{ .msl = true }, + .spirv => sdl3.gpu.ShaderFormatFlags{ .spirv = true }, +}; + +const ravioli_bmp = @embedFile("images/ravioli.bmp"); + +const window_width = 640; +const window_height = 480; + +const PositionTextureVertex = packed struct { + position: @Vector(3, f32), + tex_coord: @Vector(2, f32), +}; + +const vertices = [_]PositionTextureVertex{ + .{ .position = .{ -1, 1, 0 }, .tex_coord = .{ 0, 0 } }, + .{ .position = .{ 1, 1, 0 }, .tex_coord = .{ 4, 0 } }, + .{ .position = .{ 1, -1, 0 }, .tex_coord = .{ 4, 4 } }, + .{ .position = .{ -1, -1, 0 }, .tex_coord = .{ 0, 4 } }, +}; +const vertices_bytes = std.mem.asBytes(&vertices); + +const indices = [_]u16{ + 0, + 1, + 2, + 0, + 2, + 3, +}; +const indices_bytes = std.mem.asBytes(&indices); + +const AppState = struct { + device: sdl3.gpu.Device, + window: sdl3.video.Window, + pipeline: sdl3.gpu.GraphicsPipeline, + vertex_buffer: sdl3.gpu.Buffer, + index_buffer: sdl3.gpu.Buffer, + texture: sdl3.gpu.Texture, + sampler: sdl3.gpu.Sampler, +}; + +pub fn loadImage( + bmp: []const u8, +) !sdl3.surface.Surface { + const image_data_raw = try sdl3.surface.Surface.initFromBmpIo(try sdl3.io_stream.Stream.initFromConstMem(bmp), true); + defer image_data_raw.deinit(); + return image_data_raw.convertFormat(sdl3.pixels.Format.packed_abgr_8_8_8_8); +} + +pub fn init( + app_state: *?*AppState, + args: [][*:0]u8, +) !sdl3.AppResult { + _ = args; + + // SDL3 setup. + const allocator = getAllocator(); + _ = try sdl3.setMemoryFunctionsByAllocator(allocator); + try sdl3.init(init_flags); + errdefer sdl3.quit(init_flags); + sdl3.errors.error_callback = &sdl3.extras.sdlErrZigLog; + sdl3.log.setLogOutputFunction(void, &sdl3.extras.sdlLogZigLog, null); + + // Get our GPU device that supports what we support. + const device = try sdl3.gpu.Device.init(shader_format, options.gpu_debug, null); + errdefer device.deinit(); + + // Make our demo window. + const window = try sdl3.video.Window.init("Textured Quad", window_width, window_height, .{}); + errdefer window.deinit(); + try device.claimWindow(window); + + // Prepare pipelines. + const vertex_shader = try device.createShader(.{ + .code = vert_shader_source, + .entry_point = "main", + .format = shader_format, + .stage = .vertex, + .props = .{ .name = vert_shader_name }, + }); + defer device.releaseShader(vertex_shader); + const fragment_shader = try device.createShader(.{ + .code = frag_shader_source, + .entry_point = "main", + .format = shader_format, + .stage = .fragment, + .props = .{ .name = frag_shader_name }, + .num_samplers = 1, + }); + defer device.releaseShader(fragment_shader); + const pipeline_create_info = sdl3.gpu.GraphicsPipelineCreateInfo{ + .target_info = .{ + .color_target_descriptions = &.{ + .{ + .format = try device.getSwapchainTextureFormat(window), + }, + }, + }, + .vertex_shader = vertex_shader, + .fragment_shader = fragment_shader, + .vertex_input_state = .{ + .vertex_buffer_descriptions = &.{ + .{ + .slot = 0, + .pitch = @sizeOf(PositionTextureVertex), + .input_rate = .vertex, + }, + }, + .vertex_attributes = &.{ + .{ + .location = 0, + .buffer_slot = 0, + .format = .f32x3, + .offset = @offsetOf(PositionTextureVertex, "position"), + }, + .{ + .location = 1, + .buffer_slot = 0, + .format = .f32x2, + .offset = @offsetOf(PositionTextureVertex, "tex_coord"), + }, + }, + }, + }; + const pipeline = try device.createGraphicsPipeline(pipeline_create_info); + errdefer device.releaseGraphicsPipeline(pipeline); + + // Create sampler. + const sampler = try device.createSampler(.{ + .min_filter = .nearest, + .mag_filter = .nearest, + .mipmap_mode = .nearest, + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + }); + errdefer device.releaseSampler(sampler); + + // Prepare vertex buffer. + const vertex_buffer = try device.createBuffer(.{ + .usage = .{ .vertex = true }, + .size = vertices_bytes.len, + }); + errdefer device.releaseBuffer(vertex_buffer); + + // Create the index buffer. + const index_buffer = try device.createBuffer(.{ + .usage = .{ .index = true }, + .size = indices_bytes.len, + }); + errdefer device.releaseBuffer(index_buffer); + + // Load the image. + const image_data = try loadImage(ravioli_bmp); + defer image_data.deinit(); + const image_bytes = image_data.getPixels().?[0 .. image_data.getWidth() * image_data.getHeight() * @sizeOf(u8) * 4]; + + // Create texture. + const texture = try device.createTexture(.{ + .texture_type = .two_dimensional, + .format = .r8g8b8a8_unorm, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = .{ .sampler = true }, + .props = .{ .name = "Ravioli Texture" }, + }); + errdefer device.releaseTexture(texture); + + // Setup transfer buffer. + const transfer_buffer_vertex_data_off = 0; + const transfer_buffer_index_data_off = transfer_buffer_vertex_data_off + vertices_bytes.len; + const transfer_buffer_image_data_off = transfer_buffer_index_data_off + indices_bytes.len; + const transfer_buffer = try device.createTransferBuffer(.{ + .usage = .upload, + .size = @intCast(vertices_bytes.len + indices_bytes.len + image_bytes.len), + }); + defer device.releaseTransferBuffer(transfer_buffer); + { + const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false); + defer device.unmapTransferBuffer(transfer_buffer); + @memcpy(transfer_buffer_mapped[transfer_buffer_vertex_data_off .. transfer_buffer_vertex_data_off + vertices_bytes.len], vertices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_index_data_off .. transfer_buffer_index_data_off + indices_bytes.len], indices_bytes); + @memcpy(transfer_buffer_mapped[transfer_buffer_image_data_off .. transfer_buffer_image_data_off + image_bytes.len], image_bytes); + } + + // Upload transfer data. + const cmd_buf = try device.acquireCommandBuffer(); + { + const copy_pass = cmd_buf.beginCopyPass(); + defer copy_pass.end(); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_vertex_data_off, + }, + .{ + .buffer = vertex_buffer, + .offset = 0, + .size = vertices_bytes.len, + }, + false, + ); + copy_pass.uploadToBuffer( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_index_data_off, + }, + .{ + .buffer = index_buffer, + .offset = 0, + .size = indices_bytes.len, + }, + false, + ); + copy_pass.uploadToTexture( + .{ + .transfer_buffer = transfer_buffer, + .offset = transfer_buffer_image_data_off, + }, + .{ + .texture = texture, + .width = @intCast(image_data.getWidth()), + .height = @intCast(image_data.getHeight()), + .depth = 1, + }, + false, + ); + } + try cmd_buf.submit(); + + // Prepare app state. + const state = try allocator.create(AppState); + errdefer allocator.destroy(state); + state.* = .{ + .device = device, + .window = window, + .pipeline = pipeline, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .texture = texture, + .sampler = sampler, + }; + + // Finish setup. + app_state.* = state; + return .run; +} + +pub fn iterate( + app_state: *AppState, +) !sdl3.AppResult { + + // Get command buffer and swapchain texture. + const cmd_buf = try app_state.device.acquireCommandBuffer(); + const swapchain_texture, _, _ = try cmd_buf.waitAndAcquireSwapchainTexture(app_state.window); + if (swapchain_texture) |texture| { + + // Start a render pass if the swapchain texture is available. Make sure to clear it. + const render_pass = cmd_buf.beginRenderPass(&.{ + sdl3.gpu.ColorTargetInfo{ + .texture = texture, + .clear_color = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load = .clear, + }, + }, null); + defer render_pass.end(); + render_pass.bindGraphicsPipeline(app_state.pipeline); + render_pass.bindVertexBuffers( + 0, + &.{ + .{ .buffer = app_state.vertex_buffer, .offset = 0 }, + }, + ); + render_pass.bindIndexBuffer( + .{ .buffer = app_state.index_buffer, .offset = 0 }, + .indices_16bit, + ); + render_pass.bindFragmentSamplers( + 0, + &.{ + .{ .texture = app_state.texture, .sampler = app_state.sampler }, + }, + ); + render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0); + } + + // Finally submit the command buffer. + try cmd_buf.submit(); + + return .run; +} + +pub fn event( + app_state: *AppState, + curr_event: sdl3.events.Event, +) !sdl3.AppResult { + _ = app_state; + switch (curr_event) { + .terminating => return .success, + .quit => return .success, + else => {}, + } + return .run; +} + +pub fn quit( + app_state: ?*AppState, + result: sdl3.AppResult, +) void { + _ = result; + const allocator = getAllocator(); + if (app_state) |val| { + val.device.releaseSampler(val.sampler); + val.device.releaseTexture(val.texture); + val.device.releaseBuffer(val.index_buffer); + val.device.releaseBuffer(val.vertex_buffer); + val.device.releaseGraphicsPipeline(val.pipeline); + val.device.releaseWindow(val.window); + val.window.deinit(); + val.device.deinit(); + allocator.destroy(val); + } + sdl3.quit(init_flags); + sdl3.shutdown(); + if (builtin.mode == .Debug) + _ = debug_allocator.detectLeaks(); +} diff --git a/src/gpu.zig b/src/gpu.zig index b1924c3..abafe8f 100644 --- a/src/gpu.zig +++ b/src/gpu.zig @@ -110,13 +110,13 @@ pub const BlitInfo = struct { load_op: LoadOperation, /// The color to clear the destination region to before the blit. /// Ignored if `load_op` is not `gpu.LoadOperation.clear`. - clear_color: pixels.FColor, + clear_color: pixels.FColor = .{}, /// The flip mode for the source region. - flip_mode: surface.FlipMode, + flip_mode: surface.FlipMode = .{}, /// The filter mode used when blitting. filter: Filter, /// True cycles the destination texture if it is already bound. - cycle: bool, + cycle: bool = false, /// Convert from SDL. pub fn fromSdl(value: c.SDL_GPUBlitInfo) BlitInfo { @@ -153,10 +153,10 @@ pub const BlitRegion = struct { /// The texture. texture: Texture, /// The mip level index of the region. - mip_level: u32, + mip_level: u32 = 0, /// The layer index or depth plane of the region. /// This value is treated as a layer index on 2D array and cube textures, and as a depth plane on 3D textures. - layer_or_depth_plane: u32, + layer_or_depth_plane: u32 = 0, /// The region. region: rect.Rect(u32), @@ -3048,11 +3048,11 @@ pub const IndexedIndirectDrawCommand = extern struct { /// The number of instances to draw. num_instances: u32, /// The base index within the index buffer. - first_index: u32, + first_index: u32 = 0, /// The value added to the vertex index before indexing into the vertex buffer. - vertex_offset: i32, + vertex_offset: i32 = 0, /// The ID of the first instance to draw. - first_instance: u32, + first_instance: u32 = 0, // Size tests. comptime { @@ -3109,9 +3109,9 @@ pub const IndirectDrawCommand = extern struct { /// The number of instances to draw. num_instances: u32, /// The index of the first vertex to draw. - first_vertex: u32, + first_vertex: u32 = 0, /// The ID of the first instance to draw. - first_instance: u32, + first_instance: u32 = 0, // Size tests. comptime { @@ -4150,7 +4150,7 @@ pub const StorageBufferReadWriteBinding = extern struct { /// Must have been created with `gpu.BufferUsageFlags.compute_storage_write`. buffer: Buffer, /// If true, cycles the buffer if it is already bound. - cycle: bool, + cycle: bool = false, _1: u8 = 0, _2: u8 = 0, _3: u8 = 0, @@ -4173,11 +4173,11 @@ pub const StorageTextureReadWriteBinding = extern struct { /// The texture to bind. Must have been created with `gpu.TextureUsageFlags.compute_storage_write` or `gpu.TextureUsageFlags.compute_storage_simultaneous_read_write`. texture: Texture, /// The mip level index to bind. - mip_level: u32, + mip_level: u32 = 0, /// The layer index to bind. - layer: u32, + layer: u32 = 0, /// If true, cycles the buffer if it is already bound. - cycle: bool, + cycle: bool = false, _1: u8 = 0, _2: u8 = 0, _3: u8 = 0, @@ -4244,6 +4244,9 @@ pub const SwapchainComposition = enum(c.SDL_GPUSwapchainComposition) { pub const Texture = packed struct { value: ?*c.SDL_GPUTexture, + /// A null texture. + pub const @"null" = Texture{ .value = null }; + // Size tests. comptime { std.debug.assert(@sizeOf(*c.SDL_GPUTexture) == @sizeOf(Texture)); diff --git a/src/sdl3.zig b/src/sdl3.zig index a78a821..e4ece48 100644 --- a/src/sdl3.zig +++ b/src/sdl3.zig @@ -716,6 +716,9 @@ pub const Scancode = @import("scancode.zig").Scancode; /// This causes SDL to scan the system for sensors, and load appropriate drivers. pub const sensor = @import("sensor.zig"); +/// Extension library for cross-compiling shaders at runtime. +pub const shadercross = if (extension_options.shadercross) @import("shadercross.zig") else void; + /// The storage API is a high-level API designed to abstract away the portability issues that come up when using something lower-level. /// /// See https://wiki.libsdl.org/SDL3/CategoryStorage for more details. diff --git a/src/shadercross.zig b/src/shadercross.zig new file mode 100644 index 0000000..b7f8284 --- /dev/null +++ b/src/shadercross.zig @@ -0,0 +1,568 @@ +const c = @import("c"); +const errors = @import("errors.zig"); +const gpu = @import("gpu.zig"); +const properties = @import("properties.zig"); +const std = @import("std"); + +/// Version of SDL shadercross to use. +/// +/// ## Version +/// This constant is provided by SDL3 shadercross. +pub const version: struct { + major: u32, + minor: u32, + micro: u32, +} = .{ + .major = c.SDL_SHADERCROSS_MAJOR_VERSION, + .minor = c.SDL_SHADERCROSS_MINOR_VERSION, + .micro = c.SDL_SHADERCROSS_MICRO_VERSION, +}; + +/// Compute pipeline metadata. +/// +/// ## Version +/// This struct is provided by SDL3 shadercross. +pub const ComputePipelineMetadata = extern struct { + /// The number of samplers defined in the shader. + num_samplers: u32, + /// The number of readonly storage textures defined in the shader. + num_readonly_storage_textures: u32, + /// The number of readonly storage buffers defined in the shader. + num_readonly_storage_buffers: u32, + /// The number of read-write storage textures defined in the shader. + num_readwrite_storage_textures: u32, + /// The number of read-write storage buffers defined in the shader. + num_readwrite_storage_buffers: u32, + /// The number of uniform buffers defined in the shader. + num_uniform_buffers: u32, + /// The number of threads in the X dimension. + threadcount_x: u32, + /// The number of threads in the Y dimension. + threadcount_y: u32, + /// The number of threads in the Z dimension. + threadcount_z: u32, + + // Size tests. + comptime { + errors.assertStructsEqual(ComputePipelineMetadata, c.SDL_ShaderCross_ComputePipelineMetadata); + } +}; + +/// Metadata for an IO variable. +/// +/// ## Version +/// This enum is provided by SDL3 shadercross. +pub const IoVarMetadata = extern struct { + /// The UTF-8 name of the variable. + name: [*:0]const u8, + /// The location of the variable. + location: u32, + /// The vector type of the variable. + vector_type: IoVarType, + /// The number of components in the vector type of the variable. + vector_size: u32, + + // Size test. + comptime { + errors.assertStructsEqual(IoVarMetadata, c.SDL_ShaderCross_IOVarMetadata); + } +}; + +/// Type of IO variable. +/// +/// ## Version +/// This enum is provided by SDL3 shadercross. +pub const IoVarType = enum(c_uint) { + i8 = c.SDL_SHADERCROSS_IOVAR_TYPE_INT8, + u8 = c.SDL_SHADERCROSS_IOVAR_TYPE_UINT8, + i16 = c.SDL_SHADERCROSS_IOVAR_TYPE_INT16, + u16 = c.SDL_SHADERCROSS_IOVAR_TYPE_UINT16, + i32 = c.SDL_SHADERCROSS_IOVAR_TYPE_INT32, + u32 = c.SDL_SHADERCROSS_IOVAR_TYPE_UINT32, + i64 = c.SDL_SHADERCROSS_IOVAR_TYPE_INT64, + u64 = c.SDL_SHADERCROSS_IOVAR_TYPE_UINT64, + f16 = c.SDL_SHADERCROSS_IOVAR_TYPE_FLOAT16, + f32 = c.SDL_SHADERCROSS_IOVAR_TYPE_FLOAT32, + f64 = c.SDL_SHADERCROSS_IOVAR_TYPE_FLOAT64, + + /// Convert from SDL. + pub fn fromSdl(value: c.SDL_ShaderCross_IOVarType) ?IoVarType { + if (value == c.SDL_SHADERCROSS_IOVAR_TYPE_UNKNOWN) + return null; + return @enumFromInt(value); + } + + /// Convert to an SDL value. + pub fn toSdl(self: ?IoVarType) c.SDL_ShaderCross_IOVarType { + if (self) |val| { + return @intFromEnum(val); + } + return c.SDL_SHADERCROSS_IOVAR_TYPE_UNKNOWN; + } +}; + +/// Metadata used for the graphics shader stage. +/// +/// ## Version +/// This struct is provided by SDL3 shadercross. +pub const GraphicsShaderMetadata = struct { + /// The number of samplers defined in the shader. + num_samplers: u32, + /// The number of storage textures defined in the shader. + num_storage_textures: u32, + /// The number of storage buffers defined in the shader. + num_storage_buffers: u32, + /// The number of uniform buffers defined in the shader. + num_uniform_buffers: u32, + /// The inputs defined in the shader. + inputs: []IoVarMetadata, + /// The outputs defined in the shader. + outputs: []IoVarMetadata, + + /// Convert from SDL. + pub fn fromSdl(value: c.SDL_ShaderCross_GraphicsShaderMetadata) GraphicsShaderMetadata { + return .{ + .num_samplers = value.num_samplers, + .num_storage_textures = value.num_storage_textures, + .num_storage_buffers = value.num_storage_buffers, + .num_uniform_buffers = value.num_uniform_buffers, + .inputs = @as([*]IoVarMetadata, @ptrCast(value.inputs))[0..@intCast(value.num_inputs)], + .outputs = @as([*]IoVarMetadata, @ptrCast(value.outputs))[0..@intCast(value.num_outputs)], + }; + } + + /// Convert to SDL. + pub fn toSdl(self: GraphicsShaderMetadata) c.SDL_ShaderCross_GraphicsShaderMetadata { + return .{ + .num_samplers = self.num_samplers, + .num_storage_textures = self.num_storage_textures, + .num_storage_buffers = self.num_storage_buffers, + .num_uniform_buffers = self.num_uniform_buffers, + .num_inputs = @intCast(self.inputs.len), + .inputs = @ptrCast(self.inputs.ptr), + .num_outputs = @intCast(self.outputs.len), + .outputs = @ptrCast(self.outputs.ptr), + }; + } +}; + +/// An HLSL define. +/// +/// ## Version +/// This struct is provided by SDL3 shadercross. +pub const HlslDefine = extern struct { + /// The define name. + /// Should only be `null` for the terminator of defines. + name: ?[*:0]u8, + /// An optional value for the define, can be `null`. + value: ?[*:0]u8, + + // Size tests. + comptime { + errors.assertStructsEqual(HlslDefine, c.SDL_ShaderCross_HLSL_Define); + } +}; + +/// HLSL information. +/// +/// ## Version +/// This struct is provided by SDL3 shadercross. +pub const HlslInfo = struct { + /// The HLSL source code for the shader. + source: [:0]const u8, + /// The entry point function name for the shader in UTF-8. + entry_point: [:0]const u8, + /// The include directory for shader code. + include_dir: ?[:0]const u8, + /// An array of defines, can be `null`. + /// If not `null`, must be terminated with a fully `null` define struct. + defines: ?[*]HlslDefine, + /// The shader stage to compile the shader with. + shader_stage: ShaderStage, + /// Allows debug info to be emitted when relevant. + /// Can be useful for graphics debuggers like RenderDoc. + enable_debug: bool, + /// A UTF-8 name to associate with the shader. + name: ?[:0]const u8, + + /// Convert to SDL. + pub fn toSdl(self: HlslInfo) c.SDL_ShaderCross_HLSL_Info { + return .{ + .source = self.source.ptr, + .entrypoint = self.entry_point.ptr, + .include_dir = if (self.include_dir) |val| val.ptr else null, + .defines = if (self.defines) |val| @ptrCast(val) else null, + .shader_stage = @intFromEnum(self.shader_stage), + .enable_debug = self.enable_debug, + .name = if (self.name) |val| val.ptr else null, + }; + } +}; + +/// Shader stage. +/// +/// ## Version +/// This enum is provided by SDL3 shadercross. +pub const ShaderStage = enum(c_uint) { + vertex = c.SDL_SHADERCROSS_SHADERSTAGE_VERTEX, + fragment = c.SDL_SHADERCROSS_SHADERSTAGE_FRAGMENT, + compute = c.SDL_SHADERCROSS_SHADERSTAGE_COMPUTE, +}; + +/// SPIR-V cross-compilation info. +/// +/// ## Version +/// This struct is provided by SDL3 shadercross. +pub const SpirvInfo = struct { + /// The SPIRV bytecode. + bytecode: []const u8, + /// The entry point function name for the shader in UTF-8. + entry_point: [:0]const u8, + /// The shader stage to transpile the shader with. + shader_stage: ShaderStage, + /// Allows debug info to be emitted when relevant. + /// Can be useful for graphics debuggers like RenderDoc. + enable_debug: bool, + /// A UTF-8 name to associate with the shader. + name: ?[:0]const u8, + /// Properties for extensions. + props: ?Properties = null, + + /// Properties for the SPIRV info. + /// + /// ## Version + /// This struct is provided by zig-sdl3. + pub const Properties = struct { + pssl_compatibility: ?bool, + msl_version: ?[:0]const u8, + + /// Convert to SDL. + pub fn toProperties( + self: Properties, + ) !properties.Group { + const ret = try properties.Group.init(); + if (self.pssl_compatibility) |val| + try ret.set(c.SDL_SHADERCROSS_PROP_SPIRV_PSSL_COMPATIBILITY, .{ .boolean = val }); + if (self.msl_version) |val| + try ret.set(c.SDL_SHADERCROSS_PROP_SPIRV_MSL_VERSION, .{ .string = val }); + return ret; + } + }; + + /// To an SDL value. + pub fn toSdl(self: SpirvInfo) c.SDL_ShaderCross_SPIRV_Info { + return .{ + .bytecode = self.bytecode.ptr, + .bytecode_size = self.bytecode.len, + .entrypoint = self.entry_point.ptr, + .shader_stage = @intFromEnum(self.shader_stage), + .enable_debug = self.enable_debug, + .name = if (self.name) |val| val.ptr else null, + .props = 0, + }; + } +}; + +/// Compile to DXBC bytecode from HLSL code via a SPIRV-Cross round trip. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the DXBC bytecode. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned buffer once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileDxbcFromHlsl( + info: HlslInfo, +) ![]u8 { + const info_sdl = info.toSdl(); + var size: usize = undefined; + return @as([*]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_CompileDXBCFromHLSL(&info_sdl, &size))))[0..@intCast(size)]; +} + +/// Compile to DXIL bytecode from HLSL code via a SPIRV-Cross round trip. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the DXIL bytecode. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned buffer once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileDxilFromHlsl( + info: HlslInfo, +) ![]u8 { + const info_sdl = info.toSdl(); + var size: usize = undefined; + return @as([*]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_CompileDXILFromHLSL(&info_sdl, &size))))[0..@intCast(size)]; +} + +/// Compile DXBC bytecode from SPIRV code. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the DXBC bytecode. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned buffer once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileDxbcFromSpirv( + info: SpirvInfo, +) ![]u8 { + const info_sdl = info.toSdl(); + var size: usize = undefined; + return @as([*]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_CompileDXBCFromSPIRV(&info_sdl, &size))))[0..@intCast(size)]; +} + +/// Compile DXIL bytecode from SPIRV code. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the DXIL bytecode. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned buffer once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileDxilFromSpirv( + info: SpirvInfo, +) ![]u8 { + const info_sdl = info.toSdl(); + var size: usize = undefined; + return @as([*]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_CompileDXILFromSPIRV(&info_sdl, &size))))[0..@intCast(size)]; +} + +/// Compile an SDL GPU compute pipeline from SPIRV code. +/// +/// ## Function Parameters +/// * `device`: The SDL GPU device. +/// * `info`: The shader to transpile. +/// * `metadata`: Shader metadata, can be obtained via `reflectGraphicsSpirv()`. +/// +/// ## Return Value +/// A compiled GPU pipeline. +/// +/// ## Remarks +/// If your shader source is HLSL, you should obtain SPIR-V bytecode from `compileSpirvFromHlsl()`. +/// +/// ## Thread Safety +/// It is safe to call this function from any thread. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileComputePipelineFromSpirv( + device: gpu.Device, + info: SpirvInfo, + metadata: ComputePipelineMetadata, +) !gpu.ComputePipeline { + const info_sdl = info.toSdl(); + return .{ .value = try errors.wrapCallNull(*c.SDL_GPUComputePipeline, c.SDL_ShaderCross_CompileComputePipelineFromSPIRV(device.value, &info_sdl, @ptrCast(&metadata), 0)) }; +} + +/// Compile an SDL GPU shader from SPIRV code. +/// +/// ## Function Parameters +/// * `device`: The SDL GPU device. +/// * `info`: The shader to transpile. +/// * `metadata`: Shader metadata, can be obtained via `reflectGraphicsSpirv()`. +/// +/// ## Return Value +/// A compiled GPU shader. +/// +/// ## Remarks +/// If your shader source is HLSL, you should obtain SPIR-V bytecode from `compileSpirvFromHlsl()`. +/// +/// ## Thread Safety +/// It is safe to call this function from any thread. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileGraphicsShaderFromSpirv( + device: gpu.Device, + info: SpirvInfo, + metadata: GraphicsShaderMetadata, +) !gpu.Shader { + const info_sdl = info.toSdl(); + const metadata_sdl = metadata.toSdl(); + return .{ .value = try errors.wrapCallNull(*c.SDL_GPUShader, c.SDL_ShaderCross_CompileGraphicsShaderFromSPIRV(device.value, &info_sdl, &metadata_sdl, 0)) }; +} + +/// Compile to SPIRV bytecode from HLSL code. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the SPIRV bytecode. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned buffer once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn compileSpirvFromHlsl( + info: HlslInfo, +) ![]u8 { + const info_sdl = info.toSdl(); + var size: usize = undefined; + return @as([*]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_CompileSPIRVFromHLSL(&info_sdl, &size))))[0..@intCast(size)]; +} + +/// De-initialize SDL shadercross. +/// +/// ## Thread Safety +/// This should only be called once, from a single thread. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn deinit() void { + c.SDL_ShaderCross_Quit(); +} + +/// Get the supported shader formats that HLSL cross-compilation can output +/// +/// ## Return Value +/// GPU shader fromats supported by HLSL cross-compilation. +/// +/// ## Thread Safety +/// It is safe to call this function from any thread. +pub fn getHlslShaderFormats() ?gpu.ShaderFormatFlags { + return gpu.ShaderFormatFlags.fromSdl(c.SDL_ShaderCross_GetHLSLShaderFormats()); +} + +/// Get the supported shader formats that SPIRV cross-compilation can output. +/// +/// ## Return Value +/// GPU shader fromats supported by SPIRV cross-compilation. +/// +/// ## Thread Safety +/// It is safe to call this function from any thread. +pub fn getSpirvShaderFormats() ?gpu.ShaderFormatFlags { + return gpu.ShaderFormatFlags.fromSdl(c.SDL_ShaderCross_GetSPIRVShaderFormats()); +} + +/// Initialize SDL shadercross. +/// +/// ## Thread Safety +/// This should only be called once, from a single thread. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn init() !void { + return errors.wrapCallBool(c.SDL_ShaderCross_Init()); +} + +/// Reflect compute pipeline info from SPIRV code. +/// +/// ## Function Parameters +/// * `bytecode`: The SPIRV bytecode. +/// +/// ## Return Value +/// Returns the metadata. +/// +/// ## Remarks +/// If your shader source is HLSL, you should obtain SPIR-V bytecode from `compileSpirvFromHlsl()`. +/// +/// ## Thread Safety +/// It is safe to call this function from any thread. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn reflectComputeSpirv( + bytecode: []const u8, +) !ComputePipelineMetadata { + const ret = try errors.wrapCallNull(*c.SDL_ShaderCross_ComputePipelineMetadata, c.SDL_ShaderCross_ReflectComputeSPIRV(bytecode.ptr, bytecode.len, 0)); + defer c.SDL_free(ret); + return @as(*ComputePipelineMetadata, @ptrCast(ret)).*; +} + +/// Reflect graphics shader info from SPIRV code. +/// +/// ## Function Parameters +/// * `bytecode`: The SPIRV bytecode. +/// +/// ## Return Value +/// Returns the graphics shader metadata. +/// +/// ## Remarks +/// If your shader source is HLSL, you should obtain SPIR-V bytecode from `compileSpirvFromHlsl()`. +/// +/// ## Thread Safety +/// It is safe to call this function from any thread. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn reflectGraphicsSpirv( + bytecode: []const u8, +) !GraphicsShaderMetadata { + const ret = try errors.wrapCallNull(*c.SDL_ShaderCross_GraphicsShaderMetadata, c.SDL_ShaderCross_ReflectGraphicsSPIRV(bytecode.ptr, bytecode.len, 0)); + defer c.SDL_free(ret); + return GraphicsShaderMetadata.fromSdl(ret.*); +} + +/// Transpile to MSL code from SPIRV code. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the MSL source. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned string once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn transpileMslFromSpirv( + info: SpirvInfo, +) ![:0]u8 { + const info_sdl = info.toSdl(); + return std.mem.span(@as([*:0]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_TranspileMSLFromSPIRV(&info_sdl))))); +} + +/// Transpile to HLSL code from SPIRV code. +/// +/// ## Function Parameters +/// * `info`: The shader to transpile. +/// +/// ## Return Value +/// Returns the HLSL source. +/// This needs to be freed with `free()`. +/// +/// ## Remarks +/// You must `free()` the returned string once you are done with it. +/// +/// ## Version +/// This function is provided by SDL3 shadercross. +pub fn transpileHlslFromSpirv( + info: SpirvInfo, +) ![:0]u8 { + const info_sdl = info.toSdl(); + return std.mem.span(@as([*:0]u8, @ptrCast(try errors.wrapCallNull(*anyopaque, c.SDL_ShaderCross_TranspileHLSLFromSPIRV(&info_sdl))))); +} + +// Shadercross testing. +test "Shadercross" { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/template/build.zig.zon b/template/build.zig.zon index 49bd420..4460a27 100644 --- a/template/build.zig.zon +++ b/template/build.zig.zon @@ -7,8 +7,6 @@ .hash = "N-V-__8AAOG3BQCJ9cn-N2swm2o5cLmDhmdHmtwNngOChK78", }, .sdl3 = .{ - // .url = "git+https://github.com/Gota7/zig-sdl3#v0.1.0", - // .hash = "sdl3-0.0.1-NmT1Q5LaIAC1lVdodEX_pnLOI44bfRvW1ZuOlqrMY17E" .path = "../", }, .zemscripten = .{