diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 15df466..c2418fc 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 @@ -45,9 +57,9 @@ jobs: with: version: ${{ env.ZIG_VERSION }} - name: Build Template Linux - run: cd template && zig build + run: cd template && zig build -Dext_shadercross=true - name: Build Template Windows - run: cd template && zig build -Dtarget=x86_64-windows + run: cd template && zig build -Dext_shadercross=true -Dtarget=x86_64-windows test: name: Testing runs-on: ubuntu-latest diff --git a/build.zig b/build.zig index 7c79750..07b318d 100644 --- a/build.zig +++ b/build.zig @@ -70,6 +70,8 @@ pub fn build(b: *std.Build) !void { extension_options.addOption(bool, "main", sdl3_main); const ext_image = b.option(bool, "ext_image", "Enable SDL_image extension") orelse false; extension_options.addOption(bool, "image", ext_image); + const ext_shadercross = b.option(bool, "ext_shadercross", "Enable SDL_shadercross extension") orelse false; + extension_options.addOption(bool, "shadercross", ext_shadercross); const c_source_code = b.fmt( \\#include @@ -78,9 +80,11 @@ pub fn build(b: *std.Build) !void { \\#include \\ \\{s} + \\{s} , .{ if (!sdl3_main) "#define SDL_MAIN_NOIMPL\n" else "", if (ext_image) "#include \n" else "", + if (ext_shadercross) "#include \n" else "", }); const c_source_file_step = b.addWriteFiles(); const c_source_path = c_source_file_step.add("c.c", c_source_code); @@ -108,6 +112,9 @@ pub fn build(b: *std.Build) !void { if (ext_image) { setupSdlImage(b, sdl3, translate_c, sdl_dep_lib, c_sdl_preferred_linkage, cfg); } + if (ext_shadercross) { + setupSdlShadercross(b, sdl3, translate_c, sdl_dep_lib, c_sdl_preferred_linkage, cfg); + } _ = setupDocs(b, sdl3); _ = setupTest(b, cfg, extension_options, c_module); @@ -210,6 +217,49 @@ pub fn setupSdlImage(b: *std.Build, sdl3: *std.Build.Module, translate_c: *std.B sdl3.linkLibrary(lib); } +// 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 a251fd0..8eaea57 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -12,6 +12,18 @@ .hash = "N-V-__8AAP6WIgI4b7FYvyJ6F7gg38lmxqvHSQPGyTlGuFuT", .lazy = true, }, + .sdl_shadercross = .{ + .url = "git+https://github.com/libsdl-org/SDL_shadercross#392d12afc1ef084c5cd656307180027399b7a54e", + .hash = "N-V-__8AAH9GBgCySY9rt5VJj3A-OGl0Z05BPZ9PgS5Pt1zm", + }, + .spirv_cross = .{ + .url = "git+https://github.com/Gota7/spirv_cross-zig#75cfd40f81f63a678d70d7e3de021edd944eb553", + .hash = "spirv_cross-0.0.0-bzNVZC4YAADQdsxyfpxC9Iw8xxietH7xoEQxbHY43bmo", + }, + .spirv_headers = .{ + .url = "git+https://github.com/KhronosGroup/SPIRV-Headers#1bfd27101e4578d0284061bdf8f09fb4755c7c2d", + .hash = "N-V-__8AADq1MQCCoUh-qOkYXGvqKFClPzdiedYrsGFYzTV9", + }, }, .paths = .{ "build.zig", 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..4257318 --- /dev/null +++ b/gpu_examples/build.zig @@ -0,0 +1,165 @@ +const std = @import("std"); + +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 actual_format = if (format == .zig and std.mem.eql(u8, suffix, "comp")) .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(try std.fmt.allocPrint(b.allocator, "shaders/{s}.{s}", .{ name, if (actual_format == .glsl) "glsl" else "hlsl" }))); + glslang_cmd.addArg("-o"); + const glslang_cmd_out = glslang_cmd.addOutputFileArg(try std.fmt.allocPrint(b.allocator, "{s}.spv", .{name})); + + module.addAnonymousImport(name, .{ .root_source_file = glslang_cmd_out }); + }, + .hlsl_runtime => module.addAnonymousImport(name, .{ .root_source_file = b.path(try std.fmt.allocPrint(b.allocator, "shaders/{s}.hlsl", .{name})) }), + .zig => { + const obj = b.addObject(.{ + .name = name, + .root_module = b.addModule(name, .{ + .root_source_file = b.path(try std.fmt.allocPrint(b.allocator, "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| { + const spirv_opt_cmd = b.addSystemCommand(&.{ spirv_opt, "-O" }); + spirv_opt_cmd.addFileArg(obj.getEmittedBin()); + spirv_opt_cmd.addArg("-o"); + shader_out = spirv_opt_cmd.addOutputFileArg(try std.fmt.allocPrint(b.allocator, "{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(try std.fmt.allocPrint(b.allocator, "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; + 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/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/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..7e9dcdb --- /dev/null +++ b/gpu_examples/shaders/fillTexture.comp.zig @@ -0,0 +1,34 @@ +// Zig compute shaders not possible atm due to `std.gpu.executionMode` not working :< +// Keeping this here for archival purposes atm. + +const std = @import("std"); + +fn store2d( + comptime set: u32, + comptime bind: u32, + 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), + ); +} + +export fn main() callconv(.spirv_kernel) void { + // std.gpu.executionMode(main, .{ .local_size = .{ .x = 8, .y = 8, .z = 1 } }); + + store2d(1, 0, .{ 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..99c9a84 --- /dev/null +++ b/gpu_examples/shaders/gradientTexture.comp.zig @@ -0,0 +1 @@ +// See `fillTexture.comp.zig`. 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/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..4da386a --- /dev/null +++ b/gpu_examples/shaders/skybox.frag.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +extern var tex_coord_in: @Vector(3, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +/// Sample a cube sampler at a given UV. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// * `uv`: The UV to sample at. +/// +/// ## Return Value +/// Returns the sampled color value. +fn samplerCube( + comptime set: u32, + comptime bind: u32, + 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), + ); +} + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = samplerCube(2, 0, 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/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..99c9a84 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.comp.zig @@ -0,0 +1 @@ +// See `fillTexture.comp.zig`. 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..b60afb9 --- /dev/null +++ b/gpu_examples/shaders/texturedQuad.frag.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +/// Sample a 2d sampler at a given UV. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// * `uv`: The UV to sample at. +/// +/// ## Return Value +/// Returns the sampled color value. +fn sampler2d( + comptime set: u32, + comptime bind: u32, + 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), + ); +} + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = sampler2d(2, 0, 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..fe0026c --- /dev/null +++ b/gpu_examples/shaders/texturedQuadArray.frag.zig @@ -0,0 +1,49 @@ +const std = @import("std"); + +extern var tex_coord_in: @Vector(2, f32) addrspace(.input); + +extern var color_out: @Vector(4, f32) addrspace(.output); + +/// Sample a 2d sampler array at a given UV. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// * `uv`: The UV to sample at. +/// +/// ## Return Value +/// Returns the sampled color value. +fn sampler2dArray( + comptime set: u32, + comptime bind: u32, + 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), + ); +} + +export fn main() callconv(.spirv_fragment) void { + std.gpu.location(&tex_coord_in, 0); + + std.gpu.location(&color_out, 0); + + color_out = sampler2dArray(2, 0, .{ + tex_coord_in[0], + tex_coord_in[1], + if (tex_coord_in[1] > 0.5) 1 else 0, + }); +} 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..12c794d --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMatrix.vert.zig @@ -0,0 +1,35 @@ +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 { + c: [4]@Vector(4, f32), + + pub fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const Vec = @Vector(4, f32); + var sum: Vec = @splat(0); + inline for (0..4) |i| { + sum += a.c[i] * @as(Vec, @splat(b[i])); + } + return sum; + } +}; + +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..030db4f --- /dev/null +++ b/gpu_examples/shaders/texturedQuadWithMultiplyColor.frag.zig @@ -0,0 +1,51 @@ +const std = @import("std"); + +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); + +/// Sample a 2d sampler at a given UV. +/// +/// ## Function Parameters +/// * `set`: The descriptor set. +/// * `bind`: The binding slot. +/// * `uv`: The UV to sample at. +/// +/// ## Return Value +/// Returns the sampled color value. +fn sampler2d( + comptime set: u32, + comptime bind: u32, + 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), + ); +} + +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 * sampler2d(2, 0, 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-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/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/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..8655b1a --- /dev/null +++ b/gpu_examples/src/textured-animated-quad.zig @@ -0,0 +1,427 @@ +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 = struct { + c: [4]@Vector(4, f32), + + fn mulVec(a: Mat4, b: @Vector(4, f32)) @Vector(4, f32) { + const Vec = @Vector(4, f32); + var sum: Vec = @splat(0); + inline for (0..4) |i| { + sum += a.c[i] * @as(Vec, @splat(b[i])); + } + return sum; + } + + pub fn mul(a: Mat4, b: Mat4) Mat4 { + var res: Mat4 = undefined; + inline for (0..4) |i| { + res.c[i] = a.mulVec(b.c[i]); + } + return res; + } + + pub fn rotationZ(radians: f32) Mat4 { + return .{ + .c = .{ + .{ @cos(radians), @sin(radians), 0, 0 }, + .{ -@sin(radians), @cos(radians), 0, 0 }, + .{ 0, 0, 1, 0 }, + .{ 0, 0, 0, 1 }, + }, + }; + } + + pub fn translation(amount: @Vector(3, f32)) Mat4 { + return .{ + .c = .{ + .{ 1, 0, 0, 0 }, + .{ 0, 1, 0, 0 }, + .{ 0, 0, 1, 0 }, + .{ 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..7080982 --- /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("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(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/src/gpu.zig b/src/gpu.zig index 63d63c3..466e4a8 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 bf83b82..ac2af80 100644 --- a/src/sdl3.zig +++ b/src/sdl3.zig @@ -713,6 +713,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. @@ -1170,7 +1173,7 @@ pub fn runOnMainThread( ) !void { const Cb = struct { fn run(user_data_c: ?*anyopaque) callconv(.c) void { - callback(@alignCast(@ptrCast(user_data_c))); + callback(@ptrCast(@alignCast(user_data_c))); } }; const ret = c.SDL_RunOnMainThread(Cb.run, user_data, wait_complete); @@ -1770,7 +1773,7 @@ fn sdlAlloc(ptr: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: _ = ret_addr; const ret = c.SDL_malloc(len); if (ret) |val| { - return @as([*]u8, @alignCast(@ptrCast(val))); + return @as([*]u8, @ptrCast(@alignCast(val))); } return null; } @@ -1790,7 +1793,7 @@ fn sdlRemap(ptr: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len _ = ret_addr; const ret = c.SDL_realloc(memory.ptr, new_len); if (ret) |val| { - return @as([*]u8, @alignCast(@ptrCast(val))); + return @as([*]u8, @ptrCast(@alignCast(val))); } return null; } @@ -1860,12 +1863,12 @@ pub fn setMemoryFunctions( mem: ?*anyopaque, size: usize, ) callconv(.c) ?*anyopaque { - return realloc_fn(@alignCast(@ptrCast(mem)), size); + return realloc_fn(@ptrCast(@alignCast(mem)), size); } pub fn free( mem: ?*anyopaque, ) callconv(.c) void { - return free_fn(@alignCast(@ptrCast(mem.?))); + return free_fn(@ptrCast(@alignCast(mem.?))); } }; const ret = c.SDL_SetMemoryFunctions( 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 74ad5dd..f04026d 100644 --- a/template/build.zig.zon +++ b/template/build.zig.zon @@ -3,8 +3,6 @@ .version = "0.0.0", .dependencies = .{ .sdl3 = .{ - // .url = "git+https://github.com/Gota7/zig-sdl3#v0.1.0", - // .hash = "sdl3-0.0.1-NmT1Q5LaIAC1lVdodEX_pnLOI44bfRvW1ZuOlqrMY17E" .path = "../", }, },