Initial Commit
This commit is contained in:
commit
5b3b81e4dc
|
@ -0,0 +1,4 @@
|
|||
/.vscode
|
||||
/bin
|
||||
/zig-cache
|
||||
/zig-out
|
|
@ -0,0 +1,34 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.build.Builder) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard release options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
||||
const mode = b.standardReleaseOptions();
|
||||
|
||||
const exe = b.addExecutable("zba", "src/main.zig");
|
||||
exe.setTarget(target);
|
||||
exe.setBuildMode(mode);
|
||||
exe.install();
|
||||
|
||||
const run_cmd = exe.run();
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const exe_tests = b.addTest("src/main.zig");
|
||||
exe_tests.setTarget(target);
|
||||
exe_tests.setBuildMode(mode);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&exe_tests.step);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
const std = @import("std");
|
||||
const GamePak = @import("pak.zig").GamePak;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Bus = struct {
|
||||
pak: GamePak,
|
||||
|
||||
pub fn withPak(alloc: Allocator, path: []const u8) !@This() {
|
||||
return @This(){
|
||||
.pak = try GamePak.fromPath(alloc, path),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn readWord(self: *const @This(), addr: u32) u32 {
|
||||
return self.pak.readWord(addr);
|
||||
}
|
||||
|
||||
pub fn writeWord(_: *@This(), _: u32, _: u32) void {
|
||||
std.debug.panic("TODO: Implement Bus#writeWord", .{});
|
||||
}
|
||||
|
||||
pub fn readHalfWord(self: *const @This(), addr: u32) u16 {
|
||||
return self.pak.readHalfWord(addr);
|
||||
}
|
||||
|
||||
pub fn writeHalfWord(self: *@This(), addr: u32, halfword: u16) void {
|
||||
|
||||
// TODO: Actually implement the memory mmap
|
||||
if (addr >= self.pak.buf.len) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pak.writeHalfWord(addr, halfword);
|
||||
}
|
||||
|
||||
pub fn readByte(self: *const @This(), addr: u32) u8 {
|
||||
return self.pak.readByte(addr);
|
||||
}
|
||||
|
||||
pub fn writeByte(_: *@This(), _: u32, _: u8) void {
|
||||
std.debug.panic("TODO: Implement Bus#writeByte", .{});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
const std = @import("std");
|
||||
const Bus = @import("bus.zig").Bus;
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
|
||||
const comptimeDataProcessing = @import("cpu/data_processing.zig").comptimeDataProcessing;
|
||||
const comptimeSingleDataTransfer = @import("cpu/single_data_transfer.zig").comptimeSingleDataTransfer;
|
||||
const comptimeHalfSignedDataTransfer = @import("cpu/half_signed_data_transfer.zig").comptimeHalfSignedDataTransfer;
|
||||
|
||||
pub const InstrFn = fn (*ARM7TDMI, *Bus, u32) void;
|
||||
const ARM_LUT: [0x1000]InstrFn = populate();
|
||||
|
||||
pub const ARM7TDMI = struct {
|
||||
r: [16]u32,
|
||||
sch: *Scheduler,
|
||||
bus: *Bus,
|
||||
cpsr: CPSR,
|
||||
|
||||
pub fn new(scheduler: *Scheduler, bus: *Bus) @This() {
|
||||
const cpsr: u32 = 0x0000_00DF;
|
||||
return .{
|
||||
.r = [_]u32{0x00} ** 16,
|
||||
.sch = scheduler,
|
||||
.bus = bus,
|
||||
.cpsr = @bitCast(CPSR, cpsr),
|
||||
};
|
||||
}
|
||||
|
||||
pub inline fn step(self: *@This()) u64 {
|
||||
const opcode = self.fetch();
|
||||
// Debug
|
||||
std.debug.print("R15: 0x{X:}\n", .{ opcode });
|
||||
|
||||
ARM_LUT[armIdx(opcode)](self, self.bus, opcode);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn fetch(self: *@This()) u32 {
|
||||
const word = self.bus.readWord(self.r[15]);
|
||||
self.r[15] += 4;
|
||||
return word;
|
||||
}
|
||||
|
||||
fn fakePC(self: *const @This()) u32 {
|
||||
return self.r[15] + 4;
|
||||
}
|
||||
};
|
||||
|
||||
fn armIdx(opcode: u32) u12 {
|
||||
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 8 & 0xF);
|
||||
}
|
||||
|
||||
fn populate() [0x1000]InstrFn {
|
||||
return comptime {
|
||||
@setEvalBranchQuota(0x5000);
|
||||
var lut = [_]InstrFn{undefined_instr} ** 0x1000;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < lut.len) : (i += 1) {
|
||||
if (i >> 10 & 0x3 == 0b00) {
|
||||
const I = i >> 9 & 0x01 == 0x01;
|
||||
const S = i >> 4 & 0x01 == 0x01;
|
||||
const instrKind = i >> 5 & 0x0F;
|
||||
|
||||
lut[i] = comptimeDataProcessing(I, S, instrKind);
|
||||
}
|
||||
|
||||
if (i >> 9 & 0x7 == 0b000 and i >> 6 & 0x01 == 0x00 and i & 0xF == 0x0) {
|
||||
// Halfword and Signed Data Transfer with register offset
|
||||
const P = i >> 8 & 0x01 == 0x01;
|
||||
const U = i >> 7 & 0x01 == 0x01;
|
||||
const I = true;
|
||||
const W = i >> 5 & 0x01 == 0x01;
|
||||
const L = i >> 4 & 0x01 == 0x01;
|
||||
|
||||
lut[i] = comptimeHalfSignedDataTransfer(P, U, I, W, L);
|
||||
}
|
||||
|
||||
if (i >> 9 & 0x7 == 0b000 and i >> 6 & 0x01 == 0x01) {
|
||||
// Halfword and Signed Data Tranfer with immediate offset
|
||||
const P = i >> 8 & 0x01 == 0x01;
|
||||
const U = i >> 7 & 0x01 == 0x01;
|
||||
const I = false;
|
||||
const W = i >> 5 & 0x01 == 0x01;
|
||||
const L = i >> 4 & 0x01 == 0x01;
|
||||
|
||||
lut[i] = comptimeHalfSignedDataTransfer(P, U, I, W, L);
|
||||
}
|
||||
|
||||
if (i >> 10 & 0x3 == 0b01 and i & 0x01 == 0x00) {
|
||||
const I = i >> 9 & 0x01 == 0x01;
|
||||
const P = i >> 8 & 0x01 == 0x01;
|
||||
const U = i >> 7 & 0x01 == 0x01;
|
||||
const B = i >> 6 & 0x01 == 0x01;
|
||||
const W = i >> 5 & 0x01 == 0x01;
|
||||
const L = i >> 4 & 0x01 == 0x01;
|
||||
|
||||
lut[i] = comptimeSingleDataTransfer(I, P, U, B, W, L);
|
||||
}
|
||||
|
||||
if (i >> 9 & 0x7 == 0b101) {
|
||||
const L = i >> 8 & 0x01 == 0x01;
|
||||
lut[i] = comptimeBranch(L);
|
||||
}
|
||||
}
|
||||
|
||||
return lut;
|
||||
};
|
||||
}
|
||||
|
||||
const CPSR = packed struct {
|
||||
n: bool, // Negative / Less Than
|
||||
z: bool, // Zero
|
||||
c: bool, // Carry / Borrow / Extend
|
||||
v: bool, // Overflow
|
||||
_: u20,
|
||||
i: bool, // IRQ Disable
|
||||
f: bool, // FIQ Diable
|
||||
t: bool, // State
|
||||
m: Mode, // Mode
|
||||
};
|
||||
|
||||
const Mode = enum(u5) {
|
||||
User = 0b10000,
|
||||
Fiq = 0b10001,
|
||||
Irq = 0b10010,
|
||||
Supervisor = 0b10011,
|
||||
Abort = 0b10111,
|
||||
Undefined = 0b11011,
|
||||
System = 0b11111,
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
fn undefined_instr(_: *ARM7TDMI, _: *Bus, opcode: u32) void {
|
||||
const id = armIdx(opcode);
|
||||
std.debug.panic("[0x{X:}] 0x{X:} is an illegal opcode", .{ id, opcode });
|
||||
}
|
||||
|
||||
fn comptimeBranch(comptime L: bool) InstrFn {
|
||||
return struct {
|
||||
fn branch(cpu: *ARM7TDMI, _: *Bus, opcode: u32) void {
|
||||
if (L) {
|
||||
cpu.r[14] = cpu.r[15] - 4;
|
||||
}
|
||||
|
||||
const offset = @bitCast(i32, (opcode << 2) << 8) >> 8;
|
||||
cpu.r[15] = cpu.fakePC() + @bitCast(u32, offset);
|
||||
}
|
||||
}.branch;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
const std = @import("std");
|
||||
const cpu_mod = @import("../cpu.zig");
|
||||
|
||||
const Bus = @import("../bus.zig").Bus;
|
||||
const ARM7TDMI = cpu_mod.ARM7TDMI;
|
||||
const InstrFn = cpu_mod.InstrFn;
|
||||
|
||||
pub fn comptimeDataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
||||
return struct {
|
||||
fn dataProcessing(cpu: *ARM7TDMI, _: *Bus, opcode: u32) void {
|
||||
const rd = opcode >> 12 & 0xF;
|
||||
const op1 = opcode >> 16 & 0xF;
|
||||
|
||||
var op2: u32 = undefined;
|
||||
if (I) {
|
||||
op2 = std.math.rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) << 1);
|
||||
} else {
|
||||
op2 = reg_op2(cpu, opcode);
|
||||
}
|
||||
|
||||
switch (instrKind) {
|
||||
0x4 => {
|
||||
cpu.r[rd] = cpu.r[op1] + op2;
|
||||
|
||||
if (S) std.debug.panic("TODO: implement ADD condition codes", .{});
|
||||
},
|
||||
0xD => {
|
||||
cpu.r[rd] = op2;
|
||||
|
||||
if (S) std.debug.panic("TODO: implement MOV condition codes", .{});
|
||||
},
|
||||
else => std.debug.panic("TODO: implement data processing type {}", .{instrKind}),
|
||||
}
|
||||
}
|
||||
}.dataProcessing;
|
||||
}
|
||||
|
||||
fn reg_op2(cpu: *const ARM7TDMI, opcode: u32) u32 {
|
||||
var amount: u32 = undefined;
|
||||
if (opcode >> 4 & 0x01 == 0x01) {
|
||||
amount = cpu.r[opcode >> 8 & 0xF] & 0xFF;
|
||||
} else {
|
||||
amount = opcode >> 7 & 0x1F;
|
||||
}
|
||||
|
||||
const rm = opcode & 0xF;
|
||||
const r_val = cpu.r[rm];
|
||||
|
||||
return switch (opcode >> 5 & 0x03) {
|
||||
0b00 => r_val << @truncate(u5, amount),
|
||||
0b01 => r_val >> @truncate(u5, amount),
|
||||
0b10 => @bitCast(u32, @bitCast(i32, r_val) >> @truncate(u5, amount)),
|
||||
0b11 => std.math.rotr(u32, r_val, amount),
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
const std = @import("std");
|
||||
const cpu_mod = @import("../cpu.zig");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
const Bus = @import("../bus.zig").Bus;
|
||||
const ARM7TDMI = cpu_mod.ARM7TDMI;
|
||||
const InstrFn = cpu_mod.InstrFn;
|
||||
|
||||
pub fn comptimeHalfSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||
return struct {
|
||||
fn halfSignedDataTransfer(cpu: *ARM7TDMI, bus: *Bus, opcode: u32) void {
|
||||
const rn = opcode >> 16 & 0xF;
|
||||
const rd = opcode >> 12 & 0xF;
|
||||
const rm = opcode & 0xF;
|
||||
const imm_offset_high = opcode >> 8 & 0xF;
|
||||
|
||||
const base = cpu.r[rn];
|
||||
|
||||
var offset: u32 = undefined;
|
||||
if (I) {
|
||||
offset = imm_offset_high << 4 | rm;
|
||||
} else {
|
||||
offset = cpu.r[rm];
|
||||
}
|
||||
|
||||
const modified_base = if (U) base + offset else base - offset;
|
||||
var address = if (P) modified_base else base;
|
||||
|
||||
if (L) {
|
||||
switch(@truncate(u2, opcode >> 5)) {
|
||||
0b00 => {
|
||||
// SWP
|
||||
std.debug.panic("TODO: Implement SWP", .{});
|
||||
},
|
||||
0b01 => {
|
||||
// LDRH
|
||||
const halfword = bus.readHalfWord(address);
|
||||
cpu.r[rd] = @as(u32, halfword);
|
||||
},
|
||||
0b10 => {
|
||||
// LDRSB
|
||||
const byte = bus.readByte(address);
|
||||
cpu.r[rd] = util.u32_sign_extend(@as(u32, byte), 8);
|
||||
},
|
||||
0b11 => {
|
||||
// LDRSH
|
||||
const halfword = bus.readHalfWord(address);
|
||||
cpu.r[rd] = util.u32_sign_extend(@as(u32, halfword), 16);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (opcode >> 5 & 0x01 == 0x01) {
|
||||
// STRH
|
||||
const src = @truncate(u16, cpu.r[rd]);
|
||||
|
||||
bus.writeHalfWord(address + 2, src);
|
||||
bus.writeHalfWord(address, src);
|
||||
} else {
|
||||
std.debug.panic("TODO Figure out if this is also SWP", .{});
|
||||
}
|
||||
}
|
||||
|
||||
address = modified_base;
|
||||
if (W and P) cpu.r[rn] = address;
|
||||
}
|
||||
}.halfSignedDataTransfer;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
const std = @import("std");
|
||||
const util = @import("../util.zig");
|
||||
const mod_cpu = @import("../cpu.zig");
|
||||
|
||||
const ARM7TDMI = mod_cpu.ARM7TDMI;
|
||||
const InstrFn = mod_cpu.InstrFn;
|
||||
const Bus = @import("../bus.zig").Bus;
|
||||
|
||||
pub fn comptimeSingleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||
return struct {
|
||||
fn singleDataTransfer(cpu: *ARM7TDMI, bus: *Bus, opcode: u32) void {
|
||||
const rn = opcode >> 16 & 0xF;
|
||||
const rd = opcode >> 12 & 0xF;
|
||||
|
||||
const base = cpu.r[rn];
|
||||
const offset = if (I) opcode & 0xFFF else registerOffset(cpu, opcode);
|
||||
|
||||
const modified_base = if (U) base + offset else base - offset;
|
||||
var address = if (P) modified_base else base;
|
||||
|
||||
if (L) {
|
||||
if (B) {
|
||||
// LDRB
|
||||
cpu.r[rd] = bus.readByte(address);
|
||||
} else {
|
||||
// LDR
|
||||
std.debug.panic("Implement LDR", .{});
|
||||
}
|
||||
} else {
|
||||
if (B) {
|
||||
// STRB
|
||||
const src = @truncate(u8, cpu.r[rd]);
|
||||
|
||||
bus.writeByte(address + 3, src);
|
||||
bus.writeByte(address + 2, src);
|
||||
bus.writeByte(address + 1, src);
|
||||
bus.writeByte(address, src);
|
||||
} else {
|
||||
// STR
|
||||
std.debug.panic("Implement STR", .{});
|
||||
}
|
||||
}
|
||||
|
||||
address = modified_base;
|
||||
if (W and P) cpu.r[rn] = address;
|
||||
|
||||
// TODO: W-bit forces non-privledged mode for the transfer
|
||||
}
|
||||
}.singleDataTransfer;
|
||||
}
|
||||
|
||||
fn registerOffset(cpu: *ARM7TDMI, opcode: u32) u32 {
|
||||
const amount = opcode >> 7 & 0x1F;
|
||||
const rm = opcode & 0xF;
|
||||
const r_val = cpu.r[rm];
|
||||
|
||||
return switch (opcode >> 5 & 0x03) {
|
||||
0b00 => r_val << @truncate(u5, amount),
|
||||
0b01 => r_val >> @truncate(u5, amount),
|
||||
0b10 => @bitCast(u32, @bitCast(i32, r_val) >> @truncate(u5, amount)),
|
||||
0b11 => std.math.rotr(u32, r_val, amount),
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
const _ = @import("std");
|
||||
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
|
||||
const Bus = @import("bus.zig").Bus;
|
||||
|
||||
const CYCLES_PER_FRAME: u64 = 10_000; // TODO: What is this?
|
||||
|
||||
pub fn runFrame(sch: *Scheduler, cpu: *ARM7TDMI, bus: *Bus) void {
|
||||
const frame_end = sch.tick + CYCLES_PER_FRAME;
|
||||
|
||||
while (sch.tick < frame_end) {
|
||||
while (sch.tick < sch.nextTimestamp()) {
|
||||
sch.tick += cpu.step();
|
||||
}
|
||||
|
||||
sch.handleEvent(cpu, bus);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const Bus = @import("bus.zig").Bus;
|
||||
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
|
||||
|
||||
const emu = @import("emu.zig");
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const alloc = gpa.allocator();
|
||||
// defer gpa.deinit();
|
||||
|
||||
var bus = try Bus.withPak(alloc, "./bin/demo/beeg/beeg.gba");
|
||||
var scheduler = Scheduler.new(alloc);
|
||||
var cpu = ARM7TDMI.new(&scheduler, &bus);
|
||||
|
||||
while (true) {
|
||||
emu.runFrame(&scheduler, &cpu, &bus);
|
||||
}
|
||||
}
|
||||
|
||||
test "basic test" {
|
||||
try std.testing.expectEqual(10, 3 + 7);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const GamePak = struct {
|
||||
buf: []u8,
|
||||
|
||||
pub fn fromPath(alloc: Allocator, path: []const u8) !@This() {
|
||||
const file = try std.fs.cwd().openFile(path, .{ .read = true });
|
||||
defer file.close();
|
||||
|
||||
const len = try file.getEndPos();
|
||||
|
||||
return @This(){
|
||||
.buf = try file.readToEndAlloc(alloc, len),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn readWord(self: *const @This(), addr: u32) u32 {
|
||||
return (@as(u32, self.buf[addr + 3]) << 24) | (@as(u32, self.buf[addr + 2]) << 16) | (@as(u32, self.buf[addr + 1]) << 8) | (@as(u32, self.buf[addr]));
|
||||
}
|
||||
|
||||
pub fn readHalfWord(self: *const @This(), addr: u32) u16 {
|
||||
return (@as(u16, self.buf[addr + 1]) << 8) | @as(u16, self.buf[addr]);
|
||||
}
|
||||
|
||||
pub fn writeHalfWord(self: *@This(), addr: u32, halfword: u16) void {
|
||||
self.buf[addr + 1] = @truncate(u8, halfword >> 8);
|
||||
self.buf[addr] = @truncate(u8, halfword);
|
||||
}
|
||||
|
||||
pub fn readByte(self: *const @This(), addr: u32) u8 {
|
||||
return self.buf[addr];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
const std = @import("std");
|
||||
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
|
||||
const Bus = @import("bus.zig").Bus;
|
||||
|
||||
const Order = std.math.Order;
|
||||
const PriorityQueue = std.PriorityQueue;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Scheduler = struct {
|
||||
tick: u64,
|
||||
queue: PriorityQueue(Event, void, lessThan),
|
||||
|
||||
pub fn new(alloc: Allocator) @This() {
|
||||
var scheduler = Scheduler{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(alloc, {}) };
|
||||
|
||||
scheduler.queue.add(.{
|
||||
.kind = EventKind.HeatDeath,
|
||||
.tick = std.math.maxInt(u64),
|
||||
}) catch unreachable;
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
pub fn handleEvent(self: *@This(), _: *ARM7TDMI, _: *Bus) void {
|
||||
const should_handle = if (self.queue.peek()) |e| self.tick >= e.tick else false;
|
||||
|
||||
if (should_handle) {
|
||||
const event = self.queue.remove();
|
||||
|
||||
switch (event.kind) {
|
||||
.HeatDeath => {
|
||||
std.debug.panic("Somehow, a u64 overflowed", .{});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub inline fn nextTimestamp(self: *@This()) u64 {
|
||||
if (self.queue.peek()) |e| {
|
||||
return e.tick;
|
||||
} else unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Event = struct {
|
||||
kind: EventKind,
|
||||
tick: u64,
|
||||
};
|
||||
|
||||
fn lessThan(context: void, a: Event, b: Event) Order {
|
||||
_ = context;
|
||||
return std.math.order(a.tick, b.tick);
|
||||
}
|
||||
|
||||
pub const EventKind = enum {
|
||||
HeatDeath,
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
const std = @import("std");
|
||||
|
||||
|
||||
pub fn u32_sign_extend(value: u32, bitSize: anytype) u32 {
|
||||
const amount: u5 = 32 - bitSize;
|
||||
return @bitCast(u32, @bitCast(i32, value << amount) >> amount);
|
||||
}
|
Loading…
Reference in New Issue