Completly random Illegal instruction error

I was upgrading my sdl3 playground from 0.15.0-dev.1254+c9ce1debe to 0.15.1, and now everything works, but I SOMETIMES get Illegal instruction error. The thing is - it’s feels like it’s comepletly random - modifying completly unrelated code seems to either get rid of it or cause it to appear. I can move reading bmp and converting it to surface (loadTextureToSurface) file closer to texture creation in my zig code and error disappears. I can move calling convention of a frag function to a constant in my SHADER and error appears again. I change the order of frag and vert function (shader entry points) and it disappears. What is going on? Can I even trust this stack trace that’s telling me it’s happening in device.createTexture (most of the time stack trace is the same)?

[system:info] App name: SDL Application
[system:info] App version: <unspecified>
[system:info] App ID: <unspecified>
[system:info] SDL revision: SDL3-3.2.20 (https://github.com/castholm/SDL 0.2.6)
[gpu:info] SDL_GPU Driver: Vulkan
[gpu:info] Vulkan Device: NVIDIA GeForce RTX 4080
[gpu:info] Vulkan Driver: NVIDIA 570.153.02
[gpu:info] Vulkan Conformance: 1.4.0
Illegal instruction at address 0x1d4d493
/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/vulkan/SDL_gpu_vulkan.c:10556:67: 0x1d4d493 in VULKAN_Submit (/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/vulkan/SDL_gpu_vulkan.c)
        (renderer->claimedWindowCount > 0 && vulkanCommandBuffer->swapchainRequested) ||
                                                                  ^
/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/vulkan/SDL_gpu_vulkan.c:5895:9: 0x1d65c05 in VULKAN_INTERNAL_CreateTexture (/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/vulkan/SDL_gpu_vulkan.c)
        VULKAN_Submit((SDL_GPUCommandBuffer *)barrierCommandBuffer);
        ^
/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/vulkan/SDL_gpu_vulkan.c:6835:15: 0x1d31d24 in VULKAN_CreateTexture (/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/vulkan/SDL_gpu_vulkan.c)
    texture = VULKAN_INTERNAL_CreateTexture(
              ^
/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/SDL_gpu.c:1300:12: 0x1429913 in SDL_CreateGPUTexture_REAL (/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/gpu/SDL_gpu.c)
    return device->CreateTexture(
           ^
/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/dynapi/SDL_dynapi_procs.h:138:1: 0x1383889 in SDL_CreateGPUTexture (/home/maxcross/.cache/zig/p/sdl-0.2.6+3.2.20-7uIn9NgjfwHH5a6HhyLHat2nHU3OP5B05QHhKJKuxEex/src/dynapi/SDL_dynapi.c)
SDL_DYNAPI_PROC(SDL_GPUTexture*,SDL_CreateGPUTexture,(SDL_GPUDevice *a, const SDL_GPUTextureCreateInfo *b),(a,b),return)
^
/home/maxcross/.cache/zig/p/sdl3-0.1.1-NmT1QwTgIADixpCS2g5pBBON5AEfnjOox94dU0dtT-tq/src/gpu.zig:2114:87: 0x12b2419 in createTexture (sdl3.zig)
            .value = try errors.wrapCallNull(*c.SDL_GPUTexture, c.SDL_CreateGPUTexture(
                                                                                      ^
/home/maxcross/projects/zig/zig-sdl3/src/main2.zig:89:45: 0x12b6539 in main (main2.zig)
    const texture = try device.createTexture(.{
                                            ^
/home/maxcross/.cache/zig/p/N-V-__8AAN5NhBR0oTsvnwjPdeNiiDLtEsfXRHd1fv-R3TOv/lib/std/start.zig:627:37: 0x12bce71 in main (std.zig)
            const result = root.main() catch |err| {
                                    ^
???:?:?: 0x7f006142a47d in ??? (libc.so.6)
Unwind information for `libc.so.6:0x7f006142a47d` was not available, trace may be incomplete

zig code:

const std = @import("std");
const sdl = @import("sdl3");

const zm = @import("zmath");
const ext = @import("ext.zig");

pub fn main() !void {
    sdl.errors.error_callback = &ext.sdlErr;
    sdl.log.setAllPriorities(.info);
    sdl.log.setLogOutputFunction(anyopaque, ext.sdlLog, null);

    // const shader = @import("shaders/textured_quad.shader.zig");
    // std.debug.print("hasDescl: {}\n", .{@hasDecl(shader, "vertex")});

    defer sdl.shutdown();
    const init_flags: sdl.InitFlags = .{ .video = true, .gamepad = true };
    try sdl.init(init_flags);
    defer sdl.quit(init_flags);

    const device = try sdl.gpu.Device.init(.{ .spirv = true }, false, null);
    defer device.deinit();

    const window = try sdl.video.Window.init("Hello, there!", 640, 480, .{});
    defer window.deinit();
    try device.claimWindow(window);
    defer device.releaseWindow(window);

    const vertex_shader = try ext.createShader(device, "textured_quad.shader", "vert", .vertex, 0, 1, 0, 0);
    defer device.releaseShader(vertex_shader);
    const fragment_shader = try ext.createShader(device, "textured_quad.shader", "frag", .fragment, 1, 0, 0, 0);
    defer device.releaseShader(fragment_shader);

    // const vertex_shader = try ext.createShader(device, "textured_quad.vert", "main", .vertex, 0, 1, 0, 0);
    // defer device.releaseShader(vertex_shader);
    // const fragment_shader = try ext.createShader(device, "textured_quad.frag", "main", .fragment, 1, 0, 0, 0);
    // defer device.releaseShader(fragment_shader);

    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,
    });
    defer device.releaseSampler(sampler);

    const VertexBufferData = packed struct {
        position: @Vector(3, f32),
        uv: @Vector(2, f32),
    };
    const pipeline = try device.createGraphicsPipeline(.{
        .target_info = .{ .color_target_descriptions = &.{.{ .format = device.getSwapchainTextureFormat(window) }} },
        .vertex_input_state = .{
            .vertex_buffer_descriptions = &ext.vertexBufferDescriptionsForVertexBufferObjects(&.{VertexBufferData}, 0),
            .vertex_attributes = &ext.attributesForVertextBufferObjects(&.{VertexBufferData}, 0, 0),
        },
        .vertex_shader = vertex_shader,
        .fragment_shader = fragment_shader,
    });
    defer device.releaseGraphicsPipeline(pipeline);

    const vertex_data = [_]VertexBufferData{
        .{ .position = .{ -1, 1, 0 }, .uv = .{ 0, 0 } },
        .{ .position = .{ 1, 1, 0 }, .uv = .{ 1, 0 } },
        .{ .position = .{ 1, -1, 0 }, .uv = .{ 1, 1 } },
        .{ .position = .{ -1, -1, 0 }, .uv = .{ 0, 1 } },
    };
    const vertex_data_size: u32 = @intCast(@sizeOf(@TypeOf(vertex_data)));
    const vertex_buffer = try device.createBuffer(.{
        .size = vertex_data_size,
        .usage = .{ .vertex = true },
        .props = .{ .name = "Raviolli buffer" },
    });
    defer device.releaseBuffer(vertex_buffer);

    const index_data = [_]u16{ 0, 1, 2, 0, 2, 3 };
    const index_data_size: u32 = @intCast(@sizeOf(@TypeOf(index_data)));
    const index_buffer = try device.createBuffer(.{
        .size = index_data_size,
        .usage = .{ .index = true },
        .props = .{ .name = "Raviolli index buffer" },
    });
    defer device.releaseBuffer(index_buffer);

    const surface = try ext.loadTextureToSurface("src/images/ravioli.bmp");
    defer surface.deinit();

    const texture = try device.createTexture(.{
        .texture_type = .two_dimensional,
        .format = .r8g8b8a8_unorm,
        .width = @intCast(surface.getWidth()),
        .height = @intCast(surface.getHeight()),
        .layer_count_or_depth = 1,
        .num_levels = 1,
        .usage = .{ .sampler = true },
        .props = .{ .name = "Raviolli texture" },
    });
    defer device.releaseTexture(texture);

    const transfer_buffer = try device.createTransferBuffer(.{ .size = vertex_data_size + index_data_size, .usage = .upload });
    defer device.releaseTransferBuffer(transfer_buffer);
    {
        defer device.unmapTransferBuffer(transfer_buffer);
        const transfer_buffer_mapped = try device.mapTransferBuffer(transfer_buffer, false);
        const transfer_buffer_vertex_data: *[vertex_data.len]VertexBufferData = @ptrFromInt(@intFromPtr(transfer_buffer_mapped));
        transfer_buffer_vertex_data.* = vertex_data;
        const transfer_buffer_index_data: *[index_data.len]u16 = @ptrFromInt(@intFromPtr(transfer_buffer_mapped) + vertex_data_size);
        transfer_buffer_index_data.* = index_data;
    }

    const pixels = surface.getPixels() orelse return error.NoPixels;

    const texture_transfer_buffer = try device.createTransferBuffer(.{ .usage = .upload, .size = @intCast(pixels.len) });
    defer device.releaseTransferBuffer(texture_transfer_buffer);
    {
        defer device.unmapTransferBuffer(texture_transfer_buffer);
        const texture_transfer_buffer_mapped = try device.mapTransferBuffer(texture_transfer_buffer, false);
        const pixels_mapped: [*]u8 = @ptrCast(@alignCast(texture_transfer_buffer_mapped));
        @memcpy(pixels_mapped, pixels);
    }

    const upload_command_buffer = try device.acquireCommandBuffer();
    {
        const copy_pass = upload_command_buffer.beginCopyPass();
        defer copy_pass.end();

        copy_pass.uploadToBuffer(
            .{ .transfer_buffer = transfer_buffer, .offset = 0 },
            .{ .buffer = vertex_buffer, .offset = 0, .size = vertex_data_size },
            false,
        );
        copy_pass.uploadToBuffer(
            .{ .transfer_buffer = transfer_buffer, .offset = vertex_data_size },
            .{ .buffer = index_buffer, .offset = 0, .size = index_data_size },
            false,
        );
        copy_pass.uploadToTexture(
            .{ .transfer_buffer = texture_transfer_buffer, .offset = 0 },
            .{ .texture = texture, .width = @intCast(surface.getWidth()), .height = @intCast(surface.getHeight()), .depth = 1 },
            false,
        );
    }
    try upload_command_buffer.submit();

    // std.debug.print("111111111\n", .{});

    var ns: u64 = 0;
    var pos = zm.f32x4(0, 0, 1, 0);
    var rot: f32 = 0;
    const keyboard = ext.KeyboardState.init();
    main_loop: while (true) {
        const ns_current = sdl.timer.getNanosecondsSinceInit();
        defer ns = ns_current;
        const dt_ns: f32 = @floatFromInt(ns_current - ns);
        const dt = dt_ns / std.time.ns_per_s;

        while (sdl.events.poll()) |event| {
            switch (event) {
                .quit, .terminating => break :main_loop,
                .key_down => |key_down| if (key_down.key) |key| switch (key) {
                    .escape => break :main_loop,
                    else => {},
                },
                else => {},
            }
        }
        if (keyboard.isKeyDown(.w)) pos += zm.f32x4(0, 0, dt, 0);
        if (keyboard.isKeyDown(.s)) pos += zm.f32x4(0, 0, -dt, 0);
        if (keyboard.isKeyDown(.a)) pos += zm.f32x4(-dt, 0, 0, 0);
        if (keyboard.isKeyDown(.d)) pos += zm.f32x4(dt, 0, 0, 0);
        if (keyboard.isKeyDown(.left_shift)) pos += zm.f32x4(0, dt, 0, 0);
        if (keyboard.isKeyDown(.left_ctrl)) pos += zm.f32x4(0, -dt, 0, 0);
        rot += dt;

        const object_to_world = zm.mul(zm.rotationY(rot), zm.translationV(pos));
        var world_to_clip = zm.perspectiveFovRh(90.0 * std.math.rad_per_deg, 640.0 / 480.0, 0.1, 10);
        world_to_clip[2][3] = 1; // invert z axis, so +z becomes away from screen
        const object_to_clip = zm.mul(object_to_world, world_to_clip);
        const projection = object_to_clip;
        const UBO = extern struct {
            // offset: @Vector(3, f32),
            offset: zm.F32x4,
            projection: zm.Mat,
        };
        const ubo: UBO = .{
            .offset = .{ 0.5, -0.25, 0, 0 },
            .projection = projection,
        };

        //draw
        const draw_command_buffer = try device.acquireCommandBuffer();
        const arr: [@bitSizeOf(UBO) / 8]u8 = @bitCast(ubo);
        draw_command_buffer.pushVertexUniformData(0, &arr);
        const maybe_swapchain_texture = try draw_command_buffer.waitAndAcquireSwapchainTexture(window);
        if (maybe_swapchain_texture.texture) |swapchain_texture| {
            const render_pass = draw_command_buffer.beginRenderPass(&.{
                .{
                    .texture = swapchain_texture,
                    .load = .clear,
                    .clear_color = .{ .a = 1 },
                },
            }, null);
            defer render_pass.end();
            render_pass.bindGraphicsPipeline(pipeline);
            render_pass.bindVertexBuffers(0, &.{.{ .buffer = vertex_buffer, .offset = 0 }});
            render_pass.bindIndexBuffer(.{ .buffer = index_buffer, .offset = 0 }, .indices_16bit);
            render_pass.bindFragmentSamplers(0, &.{.{ .texture = texture, .sampler = sampler }});
            render_pass.drawIndexedPrimitives(6, 1, 0, 0, 0);
        }
        try draw_command_buffer.submit();
    }
}
//ext.zig
const std = @import("std");
const sdl = @import("sdl3");

pub fn sdlErr(
    err: ?[]const u8,
) void {
    if (err) |val| {
        std.debug.print("******* [Error! {s}] *******\n", .{val});
    } else {
        std.debug.print("******* [Unknown Error!] *******\n", .{});
    }
}

pub fn sdlLog(
    user_data: ?*anyopaque,
    category: ?sdl.log.Category,
    priority: ?sdl.log.Priority,
    message: [:0]const u8,
) void {
    _ = user_data;
    const category_str: ?[]const u8 = if (category) |val| @tagName(val) else null;
    const priority_str: [:0]const u8 = if (priority) |val| @tagName(val) else "unknown";
    if (category_str) |val| {
        std.debug.print("[{s}:{s}] {s}\n", .{ val, priority_str, message });
    } else {
        std.debug.print("[Custom_{?}:{s}] {s}\n", .{ category, priority_str, message });
    }
}

pub inline fn createShader(
    device: sdl.gpu.Device,
    comptime name: [:0]const u8,
    comptime entry_point: [:0]const u8,
    stage: sdl.gpu.ShaderStage,
    num_samplers: u32,
    num_uniform_buffers: u32,
    num_storage_buffers: u32,
    num_storage_textures: u32,
) !sdl.gpu.Shader {
    return try device.createShader(.{
        .code = @embedFile(name),
        .stage = stage,
        // .entry_point = switch (stage) {
        //     .vertex => "vert",
        //     .fragment => "frag",
        // },
        // .entry_point = "main",
        .entry_point = entry_point,
        .format = .{ .spirv = true },
        .num_samplers = num_samplers,
        .num_uniform_buffers = num_uniform_buffers,
        .num_storage_buffers = num_storage_buffers,
        .num_storage_textures = num_storage_textures,
        .props = .{ .name = name },
    });
}

pub inline fn loadTextureToSurface(path: [:0]const u8) !sdl.surface.Surface {
    const image_surface_raw = try sdl.surface.Surface.initFromBmpFile(path);
    defer image_surface_raw.deinit();
    const image_surface = try image_surface_raw.convertFormat(.packed_abgr_8_8_8_8);
    errdefer image_surface.deinit();
    return image_surface;
}

pub inline fn vertexBufferDescriptionsForVertexBufferObjects(
    comptime vertex_buffer_objects: []const type,
    slots_start: u32,
) [vertex_buffer_objects.len]sdl.gpu.VertexBufferDescription {
    var result: [vertex_buffer_objects.len]sdl.gpu.VertexBufferDescription = undefined;
    inline for (vertex_buffer_objects, slots_start..) |VBO, slot| {
        result[slot] = .{
            .pitch = @sizeOf(VBO),
            .input_rate = if (@hasDecl(VBO, "input_rate")) VBO.input_rate else .vertex,
            .slot = slot,
            .instance_step_rate = 0,
        };
    }
    return result;
}

pub inline fn attributesForVertextBufferObjects(
    comptime vertex_buffer_objects: []const type,
    buffer_slot_start: u32,
    location_start: u32,
) [attributesLen(vertex_buffer_objects)]sdl.gpu.VertexAttribute {
    var result: [attributesLen(vertex_buffer_objects)]sdl.gpu.VertexAttribute = undefined;
    var location = location_start;
    inline for (vertex_buffer_objects, buffer_slot_start..) |VBO, buffer_slot| {
        const attributes = attributesForVertextBufferObject(VBO, buffer_slot, location);
        result[location..][0..attributes.len].* = attributes;
        location += attributes.len;
    }
    return result;
}

fn attributesLen(comptime vertex_buffer_objects: []const type) usize {
    var result: usize = 0;
    inline for (vertex_buffer_objects) |VBO| {
        result += @typeInfo(VBO).@"struct".fields.len;
    }
    return result;
}

pub inline fn attributesForVertextBufferObject(
    comptime VertexBufferObject: type,
    buffer_slot: u32,
    location_start: u32,
) [@typeInfo(VertexBufferObject).@"struct".fields.len]sdl.gpu.VertexAttribute {
    const fields = @typeInfo(VertexBufferObject).@"struct".fields;
    var result: [@typeInfo(VertexBufferObject).@"struct".fields.len]sdl.gpu.VertexAttribute = undefined;
    inline for (fields, 0..) |field, i| {
        result[i] = .{
            .buffer_slot = buffer_slot,
            .location = location_start + @as(u32, i),
            .offset = @offsetOf(VertexBufferObject, field.name),
            .format = switch (field.type) {
                u32 => .u32x1,
                @Vector(1, u32) => .u32x1,
                @Vector(2, u32) => .u32x2,
                @Vector(3, u32) => .u32x3,
                @Vector(4, u32) => .u32x4,

                i32 => .i32x1,
                @Vector(1, i32) => .i32x1,
                @Vector(2, i32) => .i32x2,
                @Vector(3, i32) => .i32x3,
                @Vector(4, i32) => .i32x4,

                f32 => .f32x1,
                @Vector(1, f32) => .f32x1,
                @Vector(2, f32) => .f32x2,
                @Vector(3, f32) => .f32x3,
                @Vector(4, f32) => .f32x4,

                @Vector(2, i8) => .i8x2,
                @Vector(4, i8) => .i8x4,
                @Vector(2, u8) => .u8x2,
                @Vector(4, u8) => .u8x4,

                @Vector(2, i16) => .i16x2,
                @Vector(4, i16) => .i16x4,

                @Vector(2, u16) => .u16x2,
                @Vector(4, u16) => .u16x4,

                @Vector(2, f16) => .f16x2,
                @Vector(4, f16) => .f16x4,

                else => @compileError("Unsupported type"),
            },
        };
    }
    return result;
}

pub const KeyboardState = struct {
    state: []const bool,

    pub fn init() @This() {
        return .{ .state = sdl.keyboard.getState() };
    }

    pub inline fn isKeyDown(self: @This(), scancode: sdl.Scancode) bool {
        return self.state[@intFromEnum(scancode)];
    }
};

C compilers usually include an ‘illegal instruction’ (Control: x86 Instruction Set Reference) in places where they know that undefined behaviour happened, which is the better alternative to letting the execution run into entirely unrelated code (which is much harder to debug).

Triggering the error by editing seemingly unrelated code also sounds like UB and it seems to happen in C code which more prone to accidential UB than Zig code.

The first thing I would do is making sure that SDL has been compiled with UBSAN enabled (but AFAIK that should automtically be the case when the C code is compiled with Zig in debug mode - but at the same time UBSAN cannot catch all UB, especially when the compiler already knows about the UB by inserting an ud2 instruction.

Next thing I would try is letting the illegal instruction hit in the debugger and then trying to make sense of execution flow leading to the illegal instruction (of course this gets ‘interesting’ when the problem is only triggered in optimized code, because then you’ll mostly be in assembly code).

1 Like

This appears to stem from vulkanCommandBuffer->swapchainRequested never getting initialized before having its value read, which is UB if it’s not one of the two legal bool bit patterns (this explains why your program sometimes works and sometimes doesn’t). It has already been fixed for the next SDL3 release:

Zig compiles C code with UBSan enabled in Debug/ReleaseSafe optimization modes by default, which will explicitly crash your application when encountering UB. Assuming you’re using zig-sdl3, you can disable UBSan by passing .c_sdl_sanitize_c = false when retrieving the module in your build.zig.

4 Likes

Ah… good point, it hadn’t occured to me that illegal instruction could have been inserted by UBSAN in the first place to ‘crash’ on UB. So far I had encountered the ud2 instruction in some rare situations without UBSAN (for instance in GCC when the argument list of a printf doesn’t match the formatting string).

I ran into this same issue a while ago. Thanks for maintaining the SDL Zig repository. If you want help updating to 3.2.22, let me know and I’ll try to craft a PR.