fat: multi-sector block transfers (B5a)

The FAT engine read and wrote one sector per device command — one SCSI
READ(10)/WRITE(10) over USB Bulk-Only Transport per 512 bytes, so every
file read, log write, and cluster fill paid a full USB round trip per
sector. The lower stack (runtime.block, the block protocol, usb-storage's
read10/write10 + count*block_size data stage) already carried a
multi-sector count; only the engine's BlockDevice interface and IpcBlock
were single-sector.

Widen BlockDevice to move a run of `count` contiguous sectors per call
(readBlock/writeBlock kept as count=1 wrappers, so metadata call sites —
FAT sectors, directory entries — are untouched). readFile and writeFile
now coalesce the aligned full-sector middle of a transfer into one command
(capped at the cluster boundary and the 4 KiB bounce = 8 sectors), reading
straight into / writing straight from the caller's buffer with no staging
copy. Full-sector writes skip the read-modify-write entirely, since they
overwrite the whole sector. Partial head/tail sectors keep the per-sector
RMW path.

IpcBlock passes count through to the block driver and sizes its bounce from
engine.max_transfer_sectors (already 4 KiB — no new allocation). A new
spc=8 engine test proves runs coalesce (a 21-sector overwrite drops from
>=21 writes to <=5) and that reads/writes round-trip byte-identical,
including an unaligned offset spanning a cluster boundary.

Full QEMU suite 92/92.
This commit is contained in:
Daniel Samson
2026-07-22 00:11:51 +01:00
parent ded555961c
commit 6e261ccda1
2 changed files with 162 additions and 27 deletions
+14 -10
View File
@@ -22,20 +22,24 @@ const mount_point = "/mnt/usb";
// buffer the driver reads/writes by physical address.
const IpcBlock = struct {
device: runtime.block.Device,
bounce: dma.Region,
bounce: dma.Region, // engine.max_transfer_sectors * 512 bytes
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
fn readBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool {
const self: *IpcBlock = @ptrCast(@alignCast(context));
if (!self.device.read(lba, 1, self.bounce.physical)) return false;
if (count == 0 or count > engine.max_transfer_sectors) return false;
const len = count * 512;
if (!self.device.read(lba, count, self.bounce.physical)) return false;
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
@memcpy(buffer[0..512], source[0..512]);
@memcpy(buffer[0..len], source[0..len]);
return true;
}
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
fn writeBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool {
const self: *IpcBlock = @ptrCast(@alignCast(context));
if (count == 0 or count > engine.max_transfer_sectors) return false;
const len = count * 512;
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
@memcpy(destination[0..512], buffer[0..512]);
if (!self.device.write(lba, 1, self.bounce.physical)) return false;
@memcpy(destination[0..len], buffer[0..len]);
if (!self.device.write(lba, count, self.bounce.physical)) return false;
device_dirty = true; // a block reached the device; a close will flush it
return true;
}
@@ -108,15 +112,15 @@ fn tryBringUp() void {
_ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
return;
};
const bounce = dma.alloc(4096, dma.coherent) orelse return;
const bounce = dma.alloc(engine.max_transfer_sectors * 512, dma.coherent) orelse return;
ipc_block = .{ .device = device, .bounce = bounce };
const block_device = engine.BlockDevice{
.context = &ipc_block,
.block_size = geometry.block_size,
.block_count = geometry.block_count,
.readBlockFn = IpcBlock.readBlock,
.writeBlockFn = IpcBlock.writeBlock,
.readBlocksFn = IpcBlock.readBlocks,
.writeBlocksFn = IpcBlock.writeBlocks,
};
filesystem = engine.FileSystem.mount(block_device) orelse {
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");