diff --git a/kernel/package-scanout.py b/kernel/package-scanout.py index d1f2ef2..058d41b 100644 --- a/kernel/package-scanout.py +++ b/kernel/package-scanout.py @@ -37,7 +37,7 @@ def info(field): raise ValueError('unexpected vermagic') kernel_id, module_id = build_id(kernel_build / 'vmlinux'), build_id(module) profile = '\n'.join(['ZAPAROO-SCANOUT-PROFILE-1', release, kernel_id, - module_id, digest(module), revision, 'zaparoo-scanout-v1-1080p', '']) + module_id, digest(module), revision, 'zaparoo-scanout-v2-native', '']) provenance = { 'schema': 1, 'kernel_revision': revision, 'kernel_build_id': kernel_id, 'module_build_id': module_id, diff --git a/kernel/scanout-slots/README.md b/kernel/scanout-slots/README.md index a3de1af..fea0896 100644 --- a/kernel/scanout-slots/README.md +++ b/kernel/scanout-slots/README.md @@ -3,9 +3,9 @@ This GPL-3.0-or-later component derives from Nigel Breslaw's MagiK scanout-slot module and the Zaparoo demo's 1080p extension. Keep its source and attribution with this Menu fork. It is a separate kernel artifact, not linked into the -frontend. Source imports retain their original license; the Linux module -loader's license classification is separate and must not be changed to gain -access to GPL-only kernel exports. +frontend. The module declares `MODULE_LICENSE("GPL")`: the native vertical-sync +wait maps and requests an interrupt, and the kernel exports those helpers to +GPL modules only. ## Compatibility policy @@ -101,22 +101,37 @@ with Rust 1.95.0 installed and a separate `BUILD_ROOT`. That retains the older profile. Never suppress modpost errors or substitute `modules_prepare` for the full build that creates genuine symbol exports. -ABI v1 uses `/dev/zaparoo-scanout`, ioctl `_IOR('Z', 1, layout)` and a 64-byte +ABI v2 uses `/dev/zaparoo-scanout`, ioctl `_IOR('Z', 1, layout)` and a 64-byte layout. Slots start at `0x23000000` and `0x23400000`, outside the complete `MiSTer_fb` DT aperture (`0x22000000`, 8 MiB). Each has 4,147,200 usable bytes and a 4,149,248-byte mapping. Slot-one mmap selector is 8,294,400, **not its physical address**. Only exact shared read/write mappings are accepted; executable mappings and fork inheritance are disabled. +v2 adds Menu's native video window (`0x3A000000`, 3 MiB, the DDR contract in +`rtl/native_video_reader.sv`) as two more exact-length mappings whose selectors +and sizes the layout reports: the 4 KiB control page, uncached, and the +3,137,536 bytes of frame slots after it, write-combined. The control words stay +uncached so a publish can never wait in a write-combining buffer behind the +pixels it announces. The window is reserved by its first mapping, not at open, +so an HDMI-only client never claims it. + +`_IOR('Z', 2, __u32)` blocks until the native raster's next vertical sync and +returns a running count, or fails with `ETIMEDOUT` after 50 ms. The source is +`sys_top`'s `video_sync` pulse on `f2h_irq[1]` (GIC SPI 41), beside the HDMI +interrupt `MiSTer_fb` owns on SPI 40. The stock device tree has no node for it, +so the module maps it on `MiSTer_fb`'s interrupt controller itself. The +interrupt is requested by the first wait and freed with the last file +reference; idle residency holds no interrupt. + 6.18 compatibility decisions: - `registered_fb` is no longer exported. Validate the pinned root-level DT aperture instead of linking to that private symbol. - `no_llseek` is gone; use a NULL file-operation entry. -- Published-VMA setters require GPL-only locking helpers. The pinned kernel - invokes `.mmap` on a newly allocated VMA before insertion into the tree, so - initialize its flags with `vm_flags_init`. Recheck this ordering for a new - kernel; do not change the loader license marker to bypass modpost. +- The pinned kernel invokes `.mmap` on a newly allocated VMA before insertion + into the tree, so initialize its flags with `vm_flags_init`. Recheck this + ordering for a new kernel. Successful compilation is **not hardware qualification**. Before installing or distributing the artifact, verify the matched Main/frontend/Menu stack on @@ -133,7 +148,7 @@ disk: the running kernel may still precede an update awaiting reboot. `kernel/.build/ci/zaparoo-scanout.zip` (`package-scanout.py` remains the prototype packager). Each profile is installed under `zaparoo/modules///`. The profile contains -the module build ID, SHA-256, kernel revision, and Zaparoo's v1 1080p contract +the module build ID, SHA-256, kernel revision, and Zaparoo's v2 native contract identifier. The ZIP also carries kernel config/symbol-table checksums and matching module sources with their existing attribution. Do not strip or rewrite the module after packaging. This is integrity/provenance, not a signature. diff --git a/kernel/scanout-slots/zaparoo_scanout.c b/kernel/scanout-slots/zaparoo_scanout.c index eac3033..5b4c51c 100644 --- a/kernel/scanout-slots/zaparoo_scanout.c +++ b/kernel/scanout-slots/zaparoo_scanout.c @@ -2,21 +2,29 @@ // Copyright (C) 2026 Nigel Breslaw // Zaparoo fork: namespaced device, 6.18 platform validation, lifetime ownership. -/* Bounded write-combined RGB565 slots. No FPGA commands, DMA, or IRQ ownership. - * Reservations belong to the open file and all its VMAs, not module residency. +/* Bounded write-combined scanout slots: two RGB565 HDMI slots and Menu's native + * video window, plus a wait on the native raster's vertical sync. No FPGA + * commands or DMA. Reservations and the sync interrupt belong to the open file + * and all its VMAs, not module residency. */ #include #include #include +#include #include +#include +#include +#include #include #include #include #include #include +#include #include #include #include +#include #include "zaparoo_scanout_platform.h" #include "zaparoo_scanout_uapi.h" @@ -25,6 +33,10 @@ static DEFINE_MUTEX(owner_lock); static bool owned; static struct resource *slot_resources[ZAPAROO_SCANOUT_SLOT_COUNT]; +static struct resource *native_resource; +static int native_irq; +static u32 native_sync_count; +static DECLARE_WAIT_QUEUE_HEAD(native_sync_wait); static const unsigned long slot_addresses[] = { ZAPAROO_SCANOUT_SLOT0_PHYS, ZAPAROO_SCANOUT_SLOT1_PHYS }; @@ -36,17 +48,20 @@ static const struct zaparoo_scanout_layout layout = { .max_stride_bytes = ZAPAROO_SCANOUT_MAX_STRIDE, .slot_capacity_bytes = ZAPAROO_SCANOUT_CAPACITY, .map_bytes = ZAPAROO_SCANOUT_MAP_BYTES, - .flags = ZAPAROO_SCANOUT_WRITE_COMBINE | ZAPAROO_SCANOUT_EXCLUSIVE_OWNER, + .flags = ZAPAROO_SCANOUT_WRITE_COMBINE | ZAPAROO_SCANOUT_EXCLUSIVE_OWNER | + ZAPAROO_SCANOUT_NATIVE_WINDOW | ZAPAROO_SCANOUT_NATIVE_VBLANK, .slots = { { ZAPAROO_SCANOUT_SLOT0_PHYS, 0 }, { ZAPAROO_SCANOUT_SLOT1_PHYS, ZAPAROO_SCANOUT_SLOT1_SELECTOR }, }, + .native_control_offset_bytes = ZAPAROO_SCANOUT_NATIVE_CONTROL_SELECTOR, + .native_control_bytes = ZAPAROO_SCANOUT_NATIVE_CONTROL_BYTES, + .native_pixels_offset_bytes = ZAPAROO_SCANOUT_NATIVE_PIXELS_SELECTOR, + .native_pixels_bytes = ZAPAROO_SCANOUT_NATIVE_PIXELS_BYTES, }; /* 6.18 no longer exports registered_fb. Check the exact root-level DT window * instead: slots are above its complete aperture, independent of live fb mode. - * Raw property reads deliberately use non-GPL-only exports. Do not mislabel - * the GPL-3.0 source's loader classification to access GPL-only helpers. */ static int validate_platform(void) { @@ -75,6 +90,58 @@ static int validate_platform(void) return ret; } +static irqreturn_t native_sync_irq(int irq, void *data) +{ + WRITE_ONCE(native_sync_count, native_sync_count + 1); + wake_up_interruptible(&native_sync_wait); + return IRQ_HANDLED; +} + +/* The stock device tree describes only MiSTer_fb's HDMI interrupt, so map + * the neighbouring FPGA line on that node's interrupt controller directly. + * Caller holds owner_lock. + */ +static int native_sync_start(void) +{ + struct irq_fwspec spec = { + .param_count = 3, + .param = { 0, ZAPAROO_SCANOUT_NATIVE_SYNC_SPI, IRQ_TYPE_EDGE_RISING }, + }; + struct device_node *node, *controller; + int irq, ret; + + if (native_irq) + return 0; + node = of_find_compatible_node(NULL, NULL, "MiSTer_fb"); + if (!node) + return -ENODEV; + controller = of_irq_find_parent(node); + of_node_put(node); + if (!controller) + return -ENODEV; + spec.fwnode = of_fwnode_handle(controller); + irq = irq_create_fwspec_mapping(&spec); + of_node_put(controller); + if (!irq) + return -ENXIO; + ret = request_irq(irq, native_sync_irq, 0, DEVICE_NAME, &native_sync_wait); + if (ret) { + irq_dispose_mapping(irq); + return ret; + } + native_irq = irq; + return 0; +} + +static void native_sync_stop(void) +{ + if (!native_irq) + return; + free_irq(native_irq, &native_sync_wait); + irq_dispose_mapping(native_irq); + native_irq = 0; +} + static void release_slots(void) { unsigned int i; @@ -84,6 +151,28 @@ static void release_slots(void) slot_resources[i] = NULL; } } + if (native_resource) { + release_mem_region(ZAPAROO_SCANOUT_NATIVE_PHYS, ZAPAROO_SCANOUT_NATIVE_BYTES); + native_resource = NULL; + } +} + +/* HDMI-only clients never touch the native window, so it is reserved by its + * first mapping instead of at open. + */ +static int reserve_native(void) +{ + int ret = 0; + + mutex_lock(&owner_lock); + if (!native_resource) { + native_resource = request_mem_region_exclusive(ZAPAROO_SCANOUT_NATIVE_PHYS, + ZAPAROO_SCANOUT_NATIVE_BYTES, DEVICE_NAME); + if (!native_resource) + ret = -EBUSY; + } + mutex_unlock(&owner_lock); + return ret; } static int scanout_open(struct inode *inode, struct file *file) @@ -122,6 +211,7 @@ static int scanout_release(struct inode *inode, struct file *file) * after close(fd). The final release may run in a different task. */ mutex_lock(&owner_lock); + native_sync_stop(); release_slots(); owned = false; mutex_unlock(&owner_lock); @@ -130,36 +220,87 @@ static int scanout_release(struct inode *inode, struct file *file) static int scanout_mmap(struct file *file, struct vm_area_struct *vma) { - unsigned long phys; - if (vma->vm_end - vma->vm_start != ZAPAROO_SCANOUT_MAP_BYTES || - !(vma->vm_flags & VM_SHARED) || !(vma->vm_flags & VM_READ) || + unsigned long phys, bytes = ZAPAROO_SCANOUT_MAP_BYTES; + bool native = true, write_combine = true; + int ret; + + if (!(vma->vm_flags & VM_SHARED) || !(vma->vm_flags & VM_READ) || !(vma->vm_flags & VM_WRITE) || (vma->vm_flags & VM_EXEC)) return -EINVAL; - if (!vma->vm_pgoff) + if (!vma->vm_pgoff) { phys = ZAPAROO_SCANOUT_SLOT0_PHYS; - else if (vma->vm_pgoff == ZAPAROO_SCANOUT_SLOT1_SELECTOR / PAGE_SIZE) + native = false; + } else if (vma->vm_pgoff == ZAPAROO_SCANOUT_SLOT1_SELECTOR / PAGE_SIZE) { phys = ZAPAROO_SCANOUT_SLOT1_PHYS; - else + native = false; + } else if (vma->vm_pgoff == ZAPAROO_SCANOUT_NATIVE_CONTROL_SELECTOR / PAGE_SIZE) { + /* The reader latches these words; they must never sit in a + * write-combining buffer behind the pixels they publish. + */ + phys = ZAPAROO_SCANOUT_NATIVE_PHYS; + bytes = ZAPAROO_SCANOUT_NATIVE_CONTROL_BYTES; + write_combine = false; + } else if (vma->vm_pgoff == ZAPAROO_SCANOUT_NATIVE_PIXELS_SELECTOR / PAGE_SIZE) { + phys = ZAPAROO_SCANOUT_NATIVE_PHYS + ZAPAROO_SCANOUT_NATIVE_CONTROL_BYTES; + bytes = ZAPAROO_SCANOUT_NATIVE_PIXELS_BYTES; + } else { return -EINVAL; + } + if (vma->vm_end - vma->vm_start != bytes) + return -EINVAL; + if (native) { + ret = reserve_native(); + if (ret) + return ret; + } - vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot); + vma->vm_page_prot = write_combine ? pgprot_writecombine(vma->vm_page_prot) : + pgprot_noncached(vma->vm_page_prot); /* The pinned kernel's __mmap_new_vma invokes this callback before * inserting the new VMA into its tree. Initialize flags here; changing * a published VMA would instead require the per-VMA locking helpers. */ vm_flags_init(vma, (vma->vm_flags & ~(VM_EXEC | VM_MAYEXEC)) | VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP | VM_DONTCOPY); - if (remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT, - ZAPAROO_SCANOUT_MAP_BYTES, vma->vm_page_prot)) + if (remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT, bytes, + vma->vm_page_prot)) return -EAGAIN; return 0; } +static long wait_native_vblank(unsigned long argument) +{ + u32 seen, count; + long left; + int ret; + + mutex_lock(&owner_lock); + ret = native_sync_start(); + mutex_unlock(&owner_lock); + if (ret) + return ret; + seen = READ_ONCE(native_sync_count); + left = wait_event_interruptible_timeout(native_sync_wait, + READ_ONCE(native_sync_count) != seen, + msecs_to_jiffies(ZAPAROO_SCANOUT_NATIVE_SYNC_TIMEOUT_MSEC)); + if (left < 0) + return left; + if (!left) + return -ETIMEDOUT; + count = READ_ONCE(native_sync_count); + return put_user(count, (u32 __user *)argument) ? -EFAULT : 0; +} + static long scanout_ioctl(struct file *file, unsigned int command, unsigned long argument) { - if (command != ZAPAROO_SCANOUT_GET_LAYOUT) + switch (command) { + case ZAPAROO_SCANOUT_GET_LAYOUT: + return copy_to_user((void __user *)argument, &layout, sizeof(layout)) ? -EFAULT : 0; + case ZAPAROO_SCANOUT_WAIT_NATIVE_VBLANK: + return wait_native_vblank(argument); + default: return -ENOTTY; - return copy_to_user((void __user *)argument, &layout, sizeof(layout)) ? -EFAULT : 0; + } } static const struct file_operations scanout_fops = { @@ -188,6 +329,14 @@ static int __init scanout_init(void) BUILD_BUG_ON(ZAPAROO_SCANOUT_CAPACITY > ZAPAROO_SCANOUT_MAP_BYTES); BUILD_BUG_ON(ZAPAROO_SCANOUT_FB_DT_BASE + ZAPAROO_SCANOUT_FB_DT_BYTES > ZAPAROO_SCANOUT_SLOT0_PHYS); BUILD_BUG_ON(ZAPAROO_SCANOUT_SLOT0_PHYS + ZAPAROO_SCANOUT_MAP_BYTES > ZAPAROO_SCANOUT_SLOT1_PHYS); + BUILD_BUG_ON(ZAPAROO_SCANOUT_SLOT1_PHYS + ZAPAROO_SCANOUT_MAP_BYTES > ZAPAROO_SCANOUT_NATIVE_PHYS); + BUILD_BUG_ON(ZAPAROO_SCANOUT_NATIVE_PHYS & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_NATIVE_CONTROL_BYTES & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_NATIVE_PIXELS_BYTES & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_NATIVE_CONTROL_SELECTOR & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_NATIVE_PIXELS_SELECTOR & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_NATIVE_CONTROL_BYTES + ZAPAROO_SCANOUT_NATIVE_PIXELS_BYTES != + ZAPAROO_SCANOUT_NATIVE_BYTES); if (validate_platform()) { pr_err("zaparoo_scanout: unsupported kernel/device-tree platform\n"); return -ENODEV; @@ -203,7 +352,6 @@ module_init(scanout_init); module_exit(scanout_exit); MODULE_DESCRIPTION("Zaparoo exclusive write-combined scanout slots"); MODULE_AUTHOR("Nigel Breslaw; Zaparoo Project contributors"); -/* Linux's loader classification is not the source license. */ -MODULE_LICENSE("Proprietary"); +MODULE_LICENSE("GPL"); MODULE_INFO(source_license, "GPL-3.0-or-later"); MODULE_INFO(kernel_revision, ZAPAROO_SCANOUT_KERNEL_REVISION); diff --git a/kernel/scanout-slots/zaparoo_scanout_platform.h b/kernel/scanout-slots/zaparoo_scanout_platform.h index b53ddb4..b3d16b1 100644 --- a/kernel/scanout-slots/zaparoo_scanout_platform.h +++ b/kernel/scanout-slots/zaparoo_scanout_platform.h @@ -18,5 +18,19 @@ #define ZAPAROO_SCANOUT_CAPACITY 4147200UL #define ZAPAROO_SCANOUT_MAP_BYTES 4149248UL #define ZAPAROO_SCANOUT_SLOT1_SELECTOR 8294400UL +/* Menu's native video reader window: control words in the first page, both + * frame slots after it (rtl/native_video_reader.sv). + */ +#define ZAPAROO_SCANOUT_NATIVE_PHYS 0x3A000000UL +#define ZAPAROO_SCANOUT_NATIVE_BYTES 0x00300000UL +#define ZAPAROO_SCANOUT_NATIVE_CONTROL_BYTES 0x00001000UL +#define ZAPAROO_SCANOUT_NATIVE_PIXELS_BYTES 0x002FF000UL +#define ZAPAROO_SCANOUT_NATIVE_CONTROL_SELECTOR 16588800UL +#define ZAPAROO_SCANOUT_NATIVE_PIXELS_SELECTOR 24883200UL +/* sys_top's f2h_irq[1], the core raster's video_sync pulse: GIC SPI 41, + * beside MiSTer_fb's SPI 40 for HDMI. + */ +#define ZAPAROO_SCANOUT_NATIVE_SYNC_SPI 41U +#define ZAPAROO_SCANOUT_NATIVE_SYNC_TIMEOUT_MSEC 50U #endif diff --git a/kernel/scanout-slots/zaparoo_scanout_uapi.h b/kernel/scanout-slots/zaparoo_scanout_uapi.h index 9f004c0..4d32e19 100644 --- a/kernel/scanout-slots/zaparoo_scanout_uapi.h +++ b/kernel/scanout-slots/zaparoo_scanout_uapi.h @@ -8,10 +8,12 @@ #include #include -#define ZAPAROO_SCANOUT_ABI_VERSION 1U +#define ZAPAROO_SCANOUT_ABI_VERSION 2U #define ZAPAROO_SCANOUT_SLOT_COUNT 2U #define ZAPAROO_SCANOUT_WRITE_COMBINE 0x00000001U #define ZAPAROO_SCANOUT_EXCLUSIVE_OWNER 0x00000002U +#define ZAPAROO_SCANOUT_NATIVE_WINDOW 0x00000004U +#define ZAPAROO_SCANOUT_NATIVE_VBLANK 0x00000008U struct zaparoo_scanout_slot { __u32 physical_address; @@ -28,9 +30,19 @@ struct zaparoo_scanout_layout { __u32 map_bytes; __u32 flags; struct zaparoo_scanout_slot slots[ZAPAROO_SCANOUT_SLOT_COUNT]; - __u32 reserved[4]; + /* Native video DDR window: one uncached control page, then the + * write-combined frame slots. Each is its own exact-length mapping. + */ + __u32 native_control_offset_bytes; + __u32 native_control_bytes; + __u32 native_pixels_offset_bytes; + __u32 native_pixels_bytes; }; /* Distinct ioctl namespace: a MagiK device must not pass this handshake. */ #define ZAPAROO_SCANOUT_GET_LAYOUT _IOR('Z', 0x01, struct zaparoo_scanout_layout) +/* Blocks until the native raster's next vertical sync and returns its running + * count. Fails with ETIMEDOUT when no sync arrives within 50 ms. + */ +#define ZAPAROO_SCANOUT_WAIT_NATIVE_VBLANK _IOR('Z', 0x02, __u32) #endif diff --git a/kernel/stock-scanout.py b/kernel/stock-scanout.py index 9ccf580..bf84aab 100644 --- a/kernel/stock-scanout.py +++ b/kernel/stock-scanout.py @@ -175,7 +175,7 @@ def package(manifest_path, official, kernel_build, module_source, output): prefix = f'modules/{release}/{manifest["stock_kernel_build_id"]}/' profile = '\n'.join(['ZAPAROO-SCANOUT-PROFILE-1', release, manifest['stock_kernel_build_id'], module_id, sha(module), manifest['kernel_revision'], - 'zaparoo-scanout-v1-1080p', '']) + 'zaparoo-scanout-v2-native', '']) output.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as archive: archive.writestr(prefix + 'profile', profile)