From 182520a58558896ecb16bc4f9d3d32a84f7d9f29 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Mon, 13 Jul 2026 21:53:06 +0200 Subject: [PATCH] feat: vblank IRQ and double-buffered page flip for Intel GPU Add tear-free scanout to the Intel GPU driver: a second kernel-allocated scanout buffer, DSPASURF flips latched at vblank, and a vblank interrupt delivered over MSI (Gen 11+ master/display/pipe IRQ chain) with a monotonic vblank counter and WaitVblank(). Expose it as SYS_FBFLIP (150): index selects the front buffer, -1 queries support, flags bit0 waits for the flip to latch. fb_map() now maps buffer 1 right after buffer 0 when flipping is available, and gui::Framebuffer draws to the off-screen buffer and flips with vsync, falling back to the direct copy when unsupported. Scanout is restored to buffer 0 when the flip-owning process exits and on panic, so the terminal and panic box never land on the invisible buffer. Co-Authored-By: Claude Fable 5 --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Graphics.hpp | 34 ++ kernel/src/Api/Syscall.cpp | 2 + kernel/src/Api/Syscall.hpp | 3 + kernel/src/Common/Panic.cpp | 6 + kernel/src/Drivers/Graphics/IntelGPU.cpp | 393 ++++++++++++++++++ kernel/src/Drivers/Graphics/IntelGPU.hpp | 64 +++ kernel/src/Sched/Scheduler.cpp | 6 + programs/include/Api/Syscall.hpp | 3 + programs/include/gui/framebuffer.hpp | 29 +- programs/include/libc/montauk.h | 1 + programs/include/montauk/syscall.h | 9 + .../obj/src/libprintersapplet.o | Bin 19720 -> 19776 bytes .../obj/src/libtimezonesapplet.o | Bin 19856 -> 19912 bytes 14 files changed, 549 insertions(+), 3 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 34ee96d..43b7931 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 4 +#define MONTAUK_BUILD_NUMBER 6 diff --git a/kernel/src/Api/Graphics.hpp b/kernel/src/Api/Graphics.hpp index cb6704c..7d012b9 100644 --- a/kernel/src/Api/Graphics.hpp +++ b/kernel/src/Api/Graphics.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "Syscall.hpp" @@ -59,9 +60,42 @@ namespace montauk::abi { } } + // When page flipping is available, map the second scanout buffer + // immediately after the first so a double-buffered client can reach + // both. Single-buffer clients never touch the extra pages. + if (Drivers::Graphics::IntelGPU::FlipSupported()) { + uint64_t buf1Phys = Drivers::Graphics::IntelGPU::GetBufferPhys(1); + for (uint64_t i = 0; i < numPages; i++) { + if (!Memory::VMM::Paging::MapUserInWC( + proc->pml4Phys, + buf1Phys + i * 0x1000, + userVa + (numPages + i) * 0x1000 + )) { + return 0; + } + } + } + return userVa; } + // SYS_FBFLIP: point scanout at buffer `index` (0 or 1). The hardware + // latches the new surface at vblank, so the flip is tear-free. + // index == -1 queries support without side effects. + static int64_t Sys_FbFlip(uint64_t index, uint64_t flags) { + namespace GPU = Drivers::Graphics::IntelGPU; + + if ((int64_t)index == -1) { + return GPU::FlipSupported() ? 1 : 0; + } + if (!GPU::FlipSupported()) return -1; + + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc != nullptr) GPU::SetFlipOwner(proc->pid); + + return GPU::Flip((int)index, (flags & 1) != 0); + } + static uint64_t Sys_TermSize() { // If the process is redirected to a GUI terminal, return those dimensions auto* proc = Sched::GetCurrentProcessPtr(); diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index a731c2f..0b54a95 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -148,6 +148,8 @@ namespace montauk::abi { return 0; case SYS_FBMAP: return (int64_t)Sys_FbMap(); + case SYS_FBFLIP: + return Sys_FbFlip(frame->arg1, frame->arg2); case SYS_TERMSIZE: return (int64_t)Sys_TermSize(); case SYS_GETARGS: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 703e93b..670ae28 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -279,6 +279,9 @@ namespace montauk::abi { /* Power.hpp -- CPU power/thermal status */ static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported + /* Graphics.hpp -- framebuffer page flip (double-buffered scanout) */ + static constexpr uint64_t SYS_FBFLIP = 150; // (index, flags) -> new front index; index=-1 queries support (1/0); flags bit0 = wait vsync + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz diff --git a/kernel/src/Common/Panic.cpp b/kernel/src/Common/Panic.cpp index 2d80852..bee58a4 100644 --- a/kernel/src/Common/Panic.cpp +++ b/kernel/src/Common/Panic.cpp @@ -5,6 +5,7 @@ #include "Panic.hpp" #include "../CppLib/BoxUI.hpp" +#include "../Drivers/Graphics/IntelGPU.hpp" static constexpr int BoxWidth = 72; @@ -65,6 +66,11 @@ static void PrintRegisters(System::PanicFrame* frame) { } void Panic(const char *meditationString, System::PanicFrame* frame) { + // Bring scanout back to buffer 0 first: if a page-flipped client was + // showing the other buffer, the panic box would land on an invisible + // surface. Safe no-op when the GPU driver is inactive. + Drivers::Graphics::IntelGPU::PanicRestoreScanout(); + // Header kerr << BOXUI_ANSI_RED_BG << BOXUI_ANSI_WHITE_FG << BOXUI_ANSI_BOLD << "\n"; PrintHorizontalEdge(BOXUI_TL, BOXUI_TR); diff --git a/kernel/src/Drivers/Graphics/IntelGPU.cpp b/kernel/src/Drivers/Graphics/IntelGPU.cpp index a0da217..f07ef1d 100644 --- a/kernel/src/Drivers/Graphics/IntelGPU.cpp +++ b/kernel/src/Drivers/Graphics/IntelGPU.cpp @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include using namespace Kt; @@ -45,6 +48,19 @@ namespace Drivers::Graphics::IntelGPU { static uint64_t g_fbSize = 0; // Total framebuffer size in bytes static uint64_t g_fbGttOffset = 0; // GTT offset where FB starts (in bytes) + // Page flip state + static volatile void* g_ggtt = nullptr; // GGTT at the gen-correct BAR0 offset + static uint64_t g_ggttEntries = 0; + static uint64_t g_fbGttOffsetA = 0; // GGTT offset of buffer 0 (firmware FB) + static uint32_t* g_buf1Virt = nullptr; // buffer 1 kernel VA (HHDM, WC) + static uint64_t g_buf1Phys = 0; + static uint64_t g_buf1GttOffset = 0; // GGTT offset of buffer 1 (in bytes) + static bool g_flipSupported = false; + static int g_frontBuffer = 0; + static int g_flipOwnerPid = -1; + static volatile uint64_t g_vblankCount = 0; + static bool g_vblankIrqReady = false; + // ========================================================================= // Register access helpers // ========================================================================= @@ -481,6 +497,284 @@ namespace Drivers::Graphics::IntelGPU { << ", stride=" << base::dec << g_fbPitch; } + // ========================================================================= + // Page flipping: GGTT at the generation-correct offset + // ========================================================================= + // + // The GTT window lives in the upper half of GTTMMADR (BAR0): + // Gen 6-7: 4 MB BAR, GTT at +2 MB, 32-bit entries + // Gen 8+: 16 MB BAR, GGTT at +8 MB, 64-bit entries + // The legacy init path above writes at +2 MB for all generations; on + // Gen 8+ those writes land in register space, and scanout keeps working + // only because the firmware's real GGTT entries (stored in stolen RAM, + // which survives S3 in self-refresh) are never actually touched. Page + // flipping needs entries the display engine really reads, so it uses the + // correct window and refuses to run unless the firmware's own scanout + // PTEs are visible there (valid bits set). On any failure page flipping + // stays off and the driver behaves exactly as before. + + static void MicroDelay(int us) { + // Simple busy-wait; us is approximate + for (volatile int i = 0; i < us * 100; i++) { + asm volatile("pause"); + } + } + + static uint64_t ReadGgttPte(uint64_t index) { + if (g_gpuGen >= 8) return ((volatile uint64_t*)g_ggtt)[index]; + return ((volatile uint32_t*)g_ggtt)[index]; + } + + static void WriteGgttPte(uint64_t index, uint64_t physAddr) { + if (g_gpuGen >= 8) { + ((volatile uint64_t*)g_ggtt)[index] = MakeGttPte64(physAddr); + } else { + ((volatile uint32_t*)g_ggtt)[index] = MakeGttPte32(physAddr); + } + } + + static bool MapCorrectGgtt() { + if (g_gpuGen < 8) { + // The legacy mapping at BAR0 + 2 MB is already the real GTT + g_ggtt = g_gttBase; + g_ggttEntries = g_gttEntryCount; + return g_ggtt != nullptr; + } + + // Gen 8+: GGC.GGMS moved to bits 7:6 (0 = none, 1/2/3 = 2/4/8 MB) + uint16_t gmchCtl = Pci::LegacyRead16(g_gpuInfo.pciBus, g_gpuInfo.pciDevice, + g_gpuInfo.pciFunction, (uint8_t)PCI_REG_GMCH_CTL); + uint8_t ggms = (gmchCtl >> 6) & 0x3; + if (ggms == 0) { + KernelLogStream(WARNING, "IntelGPU") << "GGC reports no GGTT (GGC=" + << base::hex << (uint64_t)gmchCtl << "), page flip unavailable"; + return false; + } + uint64_t ggttBytes = (1ull << ggms) * 1024 * 1024; + uint64_t ggttPhys = g_gpuInfo.mmioPhys + 8 * 1024 * 1024; + + for (uint64_t off = 0; off < ggttBytes; off += 0x1000) { + Memory::VMM::g_paging->MapMMIO(ggttPhys + off, Memory::HHDM(ggttPhys + off)); + } + + g_ggtt = (volatile void*)Memory::HHDM(ggttPhys); + g_ggttEntries = ggttBytes / sizeof(uint64_t); + + KernelLogStream(INFO, "IntelGPU") << "GGTT window at BAR0+8MB (phys " + << base::hex << ggttPhys << "), " << base::dec << (ggttBytes / 1024) + << " KB, " << g_ggttEntries << " entries"; + return true; + } + + static bool VerifyFirmwareScanoutPtes() { + // The active scanout surface must be backed by valid PTEs in the + // window we mapped; if not, our idea of where the GGTT lives is wrong + // and writing to it could corrupt arbitrary state. + g_fbGttOffsetA = ReadReg(DSPASURF) & ~0xFFFull; + uint64_t firstIdx = g_fbGttOffsetA >> 12; + uint64_t lastIdx = (g_fbGttOffsetA + g_fbSize - 1) >> 12; + + if (lastIdx >= g_ggttEntries) { + KernelLogStream(WARNING, "IntelGPU") << "Scanout range exceeds GGTT (" + << base::dec << lastIdx << " >= " << g_ggttEntries << ")"; + return false; + } + + uint64_t checks[3] = { firstIdx, (firstIdx + lastIdx) / 2, lastIdx }; + for (int i = 0; i < 3; i++) { + uint64_t pte = ReadGgttPte(checks[i]); + if (!(pte & 1)) { + KernelLogStream(WARNING, "IntelGPU") << "Scanout PTE " << base::dec + << checks[i] << " invalid (" << base::hex << pte + << ") - GGTT window not where expected, page flip unavailable"; + return false; + } + } + + KernelLogStream(OK, "IntelGPU") << "Firmware scanout PTEs verified (surface at GGTT+" + << base::hex << g_fbGttOffsetA << ", PTE[" << base::dec << firstIdx + << "]=" << base::hex << ReadGgttPte(firstIdx) << ")"; + return true; + } + + static bool AllocateBackBuffer() { + uint64_t pages = (g_fbSize + 0xFFF) >> 12; + void* virt = Memory::g_pfa->ReallocConsecutive(nullptr, pages); + if (!virt) { + KernelLogStream(WARNING, "IntelGPU") << "Failed to allocate " << base::dec + << pages << " contiguous pages for the second scanout buffer"; + return false; + } + g_buf1Phys = Memory::SubHHDM(virt); + g_buf1Virt = (uint32_t*)virt; + + // Display scanout does not snoop the CPU cache: remap the buffer + // write-combining before touching it, same as the firmware FB. + for (uint64_t i = 0; i < pages; i++) { + Memory::VMM::g_paging->MapWC(g_buf1Phys + i * 0x1000, + Memory::HHDM(g_buf1Phys + i * 0x1000)); + } + Memory::VMM::FlushTLB(); + + memset(virt, 0, pages * 0x1000); + return true; + } + + static bool FindAndMapGgttRange() { + uint64_t pages = (g_fbSize + 0xFFF) >> 12; + constexpr uint64_t guard = 16; + uint64_t need = pages + 2 * guard; + + // Firmware does not leave unused GGTT entries invalid: it points the + // ENTIRE table at a scratch page, so free entries are valid but all + // hold one identical PTE value (HW-confirmed on Raptor Lake, where a + // scan for invalid entries found none in 1M entries). Sample the tail + // of the table to learn the scratch value; a non-uniform tail means + // the layout is not what we expect, so bail. + uint64_t scratchPte = ReadGgttPte(g_ggttEntries - 1); + for (uint64_t probe = 2; probe <= 32; probe++) { + if (ReadGgttPte(g_ggttEntries - probe) != scratchPte) { + KernelLogStream(WARNING, "IntelGPU") << "GGTT tail not uniform (entry -" + << base::dec << probe << " != " << base::hex << scratchPte + << "), page flip unavailable"; + return false; + } + } + + // Search the upper half for a run of scratch-backed (or invalid) + // entries. Never overlap the active scanout range. + uint64_t avoidFirst = g_fbGttOffsetA >> 12; + uint64_t avoidLast = (g_fbGttOffsetA + g_fbSize - 1) >> 12; + + uint64_t runStart = 0, run = 0; + bool found = false; + for (uint64_t idx = g_ggttEntries / 2; idx < g_ggttEntries; idx++) { + if (idx >= avoidFirst && idx <= avoidLast) { run = 0; continue; } + uint64_t pte = ReadGgttPte(idx); + if ((pte & 1) && pte != scratchPte) { run = 0; continue; } + if (run == 0) runStart = idx; + if (++run >= need) { found = true; break; } + } + if (!found) { + KernelLogStream(WARNING, "IntelGPU") << "No free GGTT run of " << base::dec + << need << " entries in upper half, page flip unavailable"; + return false; + } + + uint64_t base = runStart + guard; + for (uint64_t i = 0; i < pages; i++) { + WriteGgttPte(base + i, g_buf1Phys + i * 0x1000); + } + + // Read back first and last entry to confirm the writes stuck + uint64_t expectFirst = (g_gpuGen >= 8) ? MakeGttPte64(g_buf1Phys) + : MakeGttPte32(g_buf1Phys); + uint64_t lastPhys = g_buf1Phys + (pages - 1) * 0x1000; + uint64_t expectLast = (g_gpuGen >= 8) ? MakeGttPte64(lastPhys) + : MakeGttPte32(lastPhys); + if (ReadGgttPte(base) != expectFirst || ReadGgttPte(base + pages - 1) != expectLast) { + KernelLogStream(WARNING, "IntelGPU") << "GGTT read-back mismatch at entry " + << base::dec << base << ", page flip unavailable"; + return false; + } + + g_buf1GttOffset = base << 12; + KernelLogStream(OK, "IntelGPU") << "Buffer 1 mapped at GGTT+" << base::hex + << g_buf1GttOffset << " (" << base::dec << pages << " pages, phys " + << base::hex << g_buf1Phys << ")"; + return true; + } + + // ========================================================================= + // Page flipping: vblank interrupt (Gen 11+) + // ========================================================================= + + static void HandleInterrupt(uint8_t /*irq*/) { + // Gen 11+ flow: park the master enable, ack the pipe IIR, re-enable + uint32_t master = ReadReg(GFX_MSTR_INTR); + if (!(master & GFX_MSTR_INTR_DISPLAY)) { + WriteReg(GFX_MSTR_INTR, GFX_MSTR_INTR_ENABLE); + return; + } + WriteReg(GFX_MSTR_INTR, 0); + + uint32_t iir = ReadReg(DE_PIPE_A_IIR); + if (iir) WriteReg(DE_PIPE_A_IIR, iir); // write-1-to-clear + + WriteReg(GFX_MSTR_INTR, GFX_MSTR_INTR_ENABLE); + + if (iir & DE_PIPE_VBLANK) { + g_vblankCount = g_vblankCount + 1; + Sched::WakeObjectWaiters((void*)&g_vblankCount); + } + } + + static bool SetupMsi() { + uint8_t bus = g_gpuInfo.pciBus; + uint8_t dev = g_gpuInfo.pciDevice; + uint8_t func = g_gpuInfo.pciFunction; + + uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI); + if (cap == 0) { + KernelLogStream(INFO, "IntelGPU") << "MSI capability not found"; + return false; + } + + uint16_t msgCtrl = Pci::LegacyRead16(bus, dev, func, cap + 2); + bool is64bit = (msgCtrl & (1 << 7)) != 0; + + Pci::LegacyWrite32(bus, dev, func, cap + 4, MSI_ADDR_BASE); + if (is64bit) { + Pci::LegacyWrite32(bus, dev, func, cap + 8, 0); + Pci::LegacyWrite16(bus, dev, func, cap + 12, MSI_VECTOR); + } else { + Pci::LegacyWrite16(bus, dev, func, cap + 8, MSI_VECTOR); + } + + msgCtrl &= ~(0x70); // one message + msgCtrl |= (1 << 0); // MSI enable + Pci::LegacyWrite16(bus, dev, func, cap + 2, msgCtrl); + + uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND); + pciCmd |= Pci::PCI_CMD_INTX_DISABLE; + Pci::LegacyWrite16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND, pciCmd); + + Hal::RegisterIrqHandler(MSI_IRQ, HandleInterrupt); + + KernelLogStream(OK, "IntelGPU") << "MSI enabled: vector " << base::dec + << (uint64_t)MSI_VECTOR << " (IRQ slot " << (uint64_t)MSI_IRQ << ")" + << (is64bit ? " [64-bit]" : " [32-bit]"); + return true; + } + + static void EnableVblankIrq() { + WriteReg(DE_PIPE_A_IIR, 0xFFFFFFFFu); // clear stale events + WriteReg(DE_PIPE_A_IMR, ~DE_PIPE_VBLANK); // unmask vblank only + WriteReg(DE_PIPE_A_IER, DE_PIPE_VBLANK); + WriteReg(DISPLAY_INT_CTL, DISPLAY_IRQ_ENABLE); + WriteReg(GFX_MSTR_INTR, GFX_MSTR_INTR_ENABLE); + (void)ReadReg(GFX_MSTR_INTR); + } + + static void SetupPageFlip() { + if (!MapCorrectGgtt()) return; + if (!VerifyFirmwareScanoutPtes()) return; + if (!AllocateBackBuffer()) return; + if (!FindAndMapGgttRange()) return; + + // Vblank interrupts: Gen 11+ register layout only. Older generations + // still flip, with DSPASURFLIVE polling standing in for the IRQ. + if (g_gpuGen >= 11 && SetupMsi()) { + EnableVblankIrq(); + g_vblankIrqReady = true; + } + + g_flipSupported = true; + g_frontBuffer = 0; + KernelLogStream(OK, "IntelGPU") << "Page flip ready: 2 buffers, vblank IRQ " + << (g_vblankIrqReady ? "on" : "off (SURFLIVE polling)"); + } + // ========================================================================= // Public API // ========================================================================= @@ -570,6 +864,8 @@ namespace Drivers::Graphics::IntelGPU { ProgramDisplayPlane(); g_initialized = true; + SetupPageFlip(); + uint64_t fwWidth = ::Graphics::Framebuffer::GetWidth(); uint64_t fwHeight = ::Graphics::Framebuffer::GetHeight(); uint64_t fwPitch = ::Graphics::Framebuffer::GetPitch(); @@ -680,6 +976,78 @@ namespace Drivers::Graphics::IntelGPU { return g_fbPitch; } + // ========================================================================= + // Page flip public API + // ========================================================================= + + bool FlipSupported() { + return g_flipSupported; + } + + uint64_t GetBufferPhys(int index) { + if (index == 0) return g_fbPhysBase; + if (index == 1 && g_flipSupported) return g_buf1Phys; + return 0; + } + + int GetFrontBuffer() { + return g_frontBuffer; + } + + uint64_t GetVblankCount() { + return g_vblankCount; + } + + bool WaitVblank(uint64_t timeoutMs) { + if (!g_vblankIrqReady) return false; + uint64_t start = g_vblankCount; + // The vblank IRQ fires every frame, so a wakeup lost to the race + // between reading the counter and blocking only costs one frame; + // the timeout is a backstop for a stalled pipe. + Sched::BlockOnObject((void*)&g_vblankCount, timeoutMs ? timeoutMs : 50); + return g_vblankCount != start; + } + + int64_t Flip(int index, bool waitVsync) { + if (!g_flipSupported || index < 0 || index > 1) return -1; + + uint64_t gttOff = (index == 0) ? g_fbGttOffsetA : g_buf1GttOffset; + WriteReg(DSPASURF, (uint32_t)gttOff); + (void)ReadReg(DSPASURF); + g_frontBuffer = index; + + if (waitVsync) { + if (g_vblankIrqReady) { + WaitVblank(50); + } + // Confirm the latch: DSPASURFLIVE tracks the surface actually + // being scanned out. With a working IRQ this succeeds on the + // first read; without one it is the vsync wait itself. + for (int i = 0; i < 3000; i++) { + if ((ReadReg(DSPASURFLIVE) & ~0xFFFu) == (uint32_t)gttOff) break; + MicroDelay(10); + } + } + return index; + } + + void SetFlipOwner(int pid) { + g_flipOwnerPid = pid; + } + + void OnProcessExit(int pid) { + if (!g_flipSupported || pid != g_flipOwnerPid) return; + g_flipOwnerPid = -1; + if (g_frontBuffer != 0) Flip(0, false); + } + + void PanicRestoreScanout() { + if (!g_flipSupported || !g_mmioBase || g_frontBuffer == 0) return; + WriteReg(DSPASURF, (uint32_t)g_fbGttOffsetA); + (void)ReadReg(DSPASURF); + g_frontBuffer = 0; + } + void Reinitialize() { if (!g_initialized || !g_mmioBase) return; @@ -760,6 +1128,31 @@ namespace Drivers::Graphics::IntelGPU { // 5. Reprogram display plane to point at our GTT-mapped framebuffer ProgramDisplayPlane(); + // 6. Restore page-flip state: rewrite buffer 1's GGTT entries, bring + // MSI + vblank interrupts back (the PCI config reset cleared MSI), + // and re-show whichever buffer was front when we suspended. + if (g_flipSupported) { + uint64_t pages = (g_fbSize + 0xFFF) >> 12; + uint64_t baseIdx = g_buf1GttOffset >> 12; + for (uint64_t i = 0; i < pages; i++) { + WriteGgttPte(baseIdx + i, g_buf1Phys + i * 0x1000); + } + (void)ReadGgttPte(baseIdx + pages - 1); + + if (g_vblankIrqReady) { + SetupMsi(); + EnableVblankIrq(); + } + + if (g_frontBuffer != 0) { + WriteReg(DSPASURF, (uint32_t)g_buf1GttOffset); + (void)ReadReg(DSPASURF); + } + + KernelLogStream(DEBUG, "IntelGPU") << "Page flip state restored (front=" + << base::dec << (uint64_t)g_frontBuffer << ")"; + } + KernelLogStream(OK, "IntelGPU") << "Display restored after S3 resume"; } diff --git a/kernel/src/Drivers/Graphics/IntelGPU.hpp b/kernel/src/Drivers/Graphics/IntelGPU.hpp index 4ae0fbe..1a21738 100644 --- a/kernel/src/Drivers/Graphics/IntelGPU.hpp +++ b/kernel/src/Drivers/Graphics/IntelGPU.hpp @@ -226,6 +226,34 @@ namespace Drivers::Graphics::IntelGPU { static constexpr uint32_t CURABASE = 0x70084; static constexpr uint32_t CURAPOS = 0x70088; + // --- Live surface address (read-only, reflects the surface the display + // engine is currently scanning out; DSPASURF writes latch at vblank) --- + static constexpr uint32_t DSPASURFLIVE = 0x701AC; + + // --- Display engine interrupts (Gen 8+ pipe block, Gen 11+ master) --- + // Per-pipe interrupt registers, pipe A instance. Vblank is bit 0. + static constexpr uint32_t DE_PIPE_A_ISR = 0x44400; + static constexpr uint32_t DE_PIPE_A_IMR = 0x44404; + static constexpr uint32_t DE_PIPE_A_IIR = 0x44408; + static constexpr uint32_t DE_PIPE_A_IER = 0x4440C; + static constexpr uint32_t DE_PIPE_VBLANK = (1u << 0); + + // Gen 11+ display interrupt master control (0x44200 was the Gen 8-10 + // master IRQ register; on Gen 11+ it controls the display half only) + static constexpr uint32_t DISPLAY_INT_CTL = 0x44200; + static constexpr uint32_t DISPLAY_IRQ_ENABLE = (1u << 31); + static constexpr uint32_t DISPLAY_IRQ_PIPE_A = (1u << 16); + + // Gen 11+ top-level graphics master interrupt + static constexpr uint32_t GFX_MSTR_INTR = 0x190010; + static constexpr uint32_t GFX_MSTR_INTR_ENABLE = (1u << 31); + static constexpr uint32_t GFX_MSTR_INTR_DISPLAY = (1u << 16); + + // --- MSI (vblank interrupt delivery) --- + static constexpr uint8_t MSI_IRQ = 28; // IRQ slot 28 = vector 60 + static constexpr uint32_t MSI_VECTOR = 60; + static constexpr uint32_t MSI_ADDR_BASE = 0xFEE00000; + // --- Output connectors --- static constexpr uint32_t ADPA = 0x61100; // Analog Display Port (VGA/CRT) static constexpr uint32_t DVOB = 0x61140; // DVO-B @@ -434,4 +462,40 @@ namespace Drivers::Graphics::IntelGPU { uint64_t GetHeight(); uint64_t GetPitch(); + // ========================================================================= + // Page flipping (double-buffered scanout) + // ========================================================================= + + // True when a second scanout buffer exists and the display plane can be + // flipped between the two. Requires a validated GGTT mapping. + bool FlipSupported(); + + // Physical base of scanout buffer 0 (firmware FB) or 1 (kernel-allocated) + uint64_t GetBufferPhys(int index); + + // Index of the buffer currently programmed for scanout + int GetFrontBuffer(); + + // Point the display plane at the given buffer. The hardware latches the + // new surface address at the next vblank (tear-free). When waitVsync is + // set, blocks until the flip has been latched. Returns the new front + // buffer index, or -1 if flipping is unavailable. + int64_t Flip(int index, bool waitVsync); + + // Block until the next vblank (Gen 11+ with working IRQ only). + // Returns false on timeout or when no vblank interrupt is available. + bool WaitVblank(uint64_t timeoutMs); + + // Monotonic vblank counter (0 when no vblank IRQ) + uint64_t GetVblankCount(); + + // Track which process performs flips so scanout can be restored to + // buffer 0 when it exits (called from the flip syscall / scheduler). + void SetFlipOwner(int pid); + void OnProcessExit(int pid); + + // Panic-safe: force scanout back to buffer 0 so panic output is visible. + // No locks, no allocation, no blocking. + void PanicRestoreScanout(); + }; diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index d8d3361..6a110b1 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include // Assembly: context switch with CR3 and FPU state parameters @@ -1110,6 +1111,11 @@ namespace Sched { // process was not the owner). Drivers::USB::Bluetooth::A2dp::ReleaseOutput(exitingPid); + // Restore scanout to buffer 0 if the exiting process owned page + // flips, so the next fullscreen client and the kernel terminal are + // never stranded on the invisible buffer (no-op for non-owners). + Drivers::Graphics::IntelGPU::OnProcessExit(exitingPid); + // Release process-scoped IPC handles/mappings before tearing down the address space. Ipc::CleanupProcessSlot(slot, exitingPid, proc.pml4Phys); montauk::abi::CleanupHeapForSlot(slot, proc.pml4Phys); diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index c6f31b6..69eebb6 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -203,6 +203,9 @@ namespace montauk::abi { // CPU power/thermal status static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported + // Framebuffer page flip (double-buffered scanout) + static constexpr uint64_t SYS_FBFLIP = 150; // (index, flags) -> new front index; index=-1 queries support (1/0); flags bit0 = wait vsync + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz diff --git a/programs/include/gui/framebuffer.hpp b/programs/include/gui/framebuffer.hpp index 303d657..68bd7bc 100644 --- a/programs/include/gui/framebuffer.hpp +++ b/programs/include/gui/framebuffer.hpp @@ -14,10 +14,12 @@ namespace gui { class Framebuffer { uint32_t* hw_fb; + uint32_t* hw_fb2; // second scanout buffer (page flip), nullptr if unsupported uint32_t* back_buf; int fb_width; int fb_height; int fb_pitch; // in bytes + int hw_next; // scanout buffer the next flip() will present (page flip only) static inline void fill_pixels(uint32_t* dst, int count, uint32_t pixel) { if (!dst || count <= 0) return; @@ -60,7 +62,8 @@ class Framebuffer { } public: - Framebuffer() : hw_fb(nullptr), back_buf(nullptr), fb_width(0), fb_height(0), fb_pitch(0) { + Framebuffer() : hw_fb(nullptr), hw_fb2(nullptr), back_buf(nullptr), + fb_width(0), fb_height(0), fb_pitch(0), hw_next(1) { montauk::abi::FbInfo info; montauk::fb_info(&info); @@ -70,6 +73,13 @@ public: hw_fb = (uint32_t*)montauk::fb_map(); back_buf = (uint32_t*)montauk::alloc((uint64_t)fb_height * fb_pitch); + + // Hardware page flipping: fb_map() maps the second scanout buffer + // directly after the first when the kernel supports flipping. + if (hw_fb && montauk::fb_flip(-1, 0) == 1) { + uint64_t pages = ((uint64_t)fb_height * fb_pitch + 0xFFF) / 0x1000; + hw_fb2 = (uint32_t*)((uint8_t*)hw_fb + pages * 0x1000); + } } int width() const { return fb_width; } @@ -250,13 +260,28 @@ public: inline void flip() { if (!hw_fb || !back_buf) return; + // With hardware page flipping, copy into the scanout buffer that is + // currently OFF screen, then ask the display engine to present it at + // the next vblank (tear-free). Without it, fall back to copying + // straight into the visible framebuffer. + uint32_t* dst_fb = hw_fb; + if (hw_fb2) dst_fb = (hw_next == 1) ? hw_fb2 : hw_fb; + // Copy back buffer to hardware framebuffer, row by row (pitch may differ) uint64_t row_bytes = (uint64_t)fb_width * sizeof(uint32_t); for (int y = 0; y < fb_height; y++) { uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch); - uint32_t* dst = (uint32_t*)((uint8_t*)hw_fb + y * fb_pitch); + uint32_t* dst = (uint32_t*)((uint8_t*)dst_fb + y * fb_pitch); montauk::memcpy(dst, src, row_bytes); } + + if (hw_fb2) { + // Wait for the latch (vsync): the buffer we just left becomes + // safe to draw into only once the new one is actually scanning. + if (montauk::fb_flip(hw_next, 1) == hw_next) { + hw_next ^= 1; + } + } } }; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 29a51fd..ace118d 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -173,6 +173,7 @@ extern "C" { #define MTK_SYS_SDR_SETPARAM 147 #define MTK_SYS_SDR_GETPARAM 148 #define MTK_SYS_POWERINFO 149 +#define MTK_SYS_FBFLIP 150 /* @SYSCALLS-END */ #define MTK_SOCK_TCP 1 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index b12b610..dac2896 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -327,6 +327,15 @@ namespace montauk { inline void fb_info(montauk::abi::FbInfo* info) { syscall1(montauk::abi::SYS_FBINFO, (uint64_t)info); } inline void* fb_map() { return (void*)syscall0(montauk::abi::SYS_FBMAP); } + // Page flip between the two scanout buffers. fb_map() maps buffer 1 + // directly after buffer 0 (at +page_align(height*pitch)). index selects + // the buffer to show; the hardware latches it at vblank (tear-free). + // flags bit0 = block until the flip has been latched (vsync). + // fb_flip(-1, 0) returns 1 when page flipping is available, 0 when not. + inline int64_t fb_flip(int64_t index, uint64_t flags) { + return syscall2(montauk::abi::SYS_FBFLIP, (uint64_t)index, flags); + } + // Arguments inline int getargs(char* buf, uint64_t maxLen) { return (int)syscall2(montauk::abi::SYS_GETARGS, (uint64_t)buf, maxLen); diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index 00fa9831603768c23ac90ed94dedf96c625d4acf..234bbee994f6e115c61b2bfaa9678a594f20362e 100644 GIT binary patch delta 3973 zcmZ8keQXrR72kJfb02Gr4+qBU+9oF0a5iRr_SxqT@L@`PocW5I2-Q&)bI_=Trlzi1 zij$Np394+Olv*z`1f?Mt`bUWhs(USjD1taui5jFNCWH@BxxJ{RDG6y!S{le#XJ+5w z%NyxtfAjmj_h$CJnccmi1MtcL`1vCE$w=@xd^nQ%YO&9dt+r&n?A_jcHSl2s%1bmK zwsuP*{|8FU^El!|@DccNqpP>v^e!_`f@I5;;xWw&e#4lV*tEs)+_5ix-WBj9=Z#8b z)TQ*ZrBQX!_-&0r<8vCjG=4{8x5no+&eQmU#`zjw)Od!*mn#w~W`*iJSLP+v)yQ+^ zcux)`;Y{*RtvQoi)z}IAzQ$f1>;sL9G`_AUaRPm$u@mP%8ar|RtFg1#8yY(?|EIBD zY|{8x51iHB)YzjZxTUc(@h2MlH2h zP-?1_JEPxW!YrvdCM)Dg%9`oUxKo95ExbcrR?45}iQ8TJkU8HC-^J5~o69yyFW9EK zM^!g0I}`k6pGad9m(OZX#%3ADU2R* z!E*2Z9MYT09lSsTUH1hld{sI}SA_qD?5Y)lITc%TM6cpxgJ=AA%Y4kB*Z&a%^6(mANqXTfTXlQNNa2K{!dfM|_&3N%rtIT}Jk_pW> z;xddpSfOHvKRCGykdR^c_pa0(8d;7XpbghZs zlm2b#1O9Yj)gHIiU!YcK7-8pP$aNVr<*ypY7TNGk%q$XCm$OfdEzD*J13>`~V=x%A z+{#h5XPwLKICF3~XhRd82-;SkE`nkFnb~wA)ClVmeq@Kp{ z#R6OEisG-*`X`xv9$8*x`~>Cy2IKeQNS%Ofc(~5CiYR8aK0DJZ%-~d=uy#{~4yMk; z$|b@YBHs;6mHCuv8)c@SspGgys&$m@pEC6c9FyvUkPls%H1OFuBlv56y=^SL&@FWHrgtdj# zbxhrkoect#*xO)RwY0}}GMm|$k~UL#LfZV2GIo^N96@Ll)+pubG*d@0)F|L8#vAuq zzo7`#w4&@fdvU5!;B!shKnGLSVP#l=iLK!n+{9gBVQtOUoLSz7V__Sj)5it zzsF#cZGA0ULuP*t2byg7HKv+`)ko`^OV{kWeufiL?WYJ!m^y{f+y|qWXtrTJjx-Bc zi-((I@FY%2ybOJjK9}E%TO(bt5XU0I+DECWpt5JDc>psJ8*X8sMZn)N*kZ#Z_R9V) z9F~0no@j}|I6|upJs4^g)@u}JlEwK2CZxKN*c(ipha;^bxsrSf>9}XtkiEH5#$Tq5 zQO)?}6up7*-%tT%>tOu*&Jo&VbTi>Ia@fH50(#zTVtj<^*w6SBoza~*&?c-sOnsed zH;O5#KF`#AQjg+9n~1-})RiGA0L+!%4%G7^l%Y%t_k#y1BXUp(B z#Vlm}A1O*H)r>z%Wouyk0L|Xv@X6b+yCW2+n>ie$*W3oizeRyIGydOHw(l|i zWwQVAmw5R_^a165&zA)GHBWc2pYdmCe30>nXoBO6ucG)T86Q^TGWmO?Q0>?0m)3UL zVDckmnXpEfdYQsJgPl@MGxZDFgeP%8s;}m#%8l;GIHP^f<*2IaS@o|tA$|XrqblEp z@~7H0gbra%F;#s(t9DC8j=SmmQTeIJ^OYpqtQ=lxJ(`stkc?kXd>M|&x$b0YAE`kc zmuekTe?)2nPD!<$sl%j3u`(*GwK=MKs=r5j?d#YR70E%SuBRQkoAEXk{tt}bN6}tq z{1G}z6O3O%b-(EF_>X9y^DoS19o6>=sxYxS>YH2TaK+m3<<$$I9y6<>a1krl ZL}3UMvTwoEnyBBq(y`nQ_`=;4{|7E;)TICb delta 3876 zcmZ`*32c-_6rQ)G-R^E_yFi&v<#0JFTU!2Z*|xOBT`I?RTZA+qrbMg@iV_J}f=8pa z;>lsu`V3H_gknrY0=B6EC5F-{Bu&t;fJo6`>H%uvfoLsw&dmQ-mP!1Ro&Dzf-h1=r zy_tXa$PU=J174Z}HEsTb@LAiy?2+kAK2viS$}uH9RZY^d9LDBqJ}91+MCy7a4s+V# zYPbw8yvxxr)}4N{=P*citWumFPoJBy;j!hbm~+&o?lTUrvo$ZQB!?PKXbc+1HFjux zQsWejPiZ_%HsvAyt+ z#xBiY(b%o=RgFCw|Dkb)#(!ySC-k?*4v*USYS23XJ?RA7N>94v1lM_T+;7URq_xUE zrD2;V$CI{2y4gc_muEr>G~wyY6+9-bV2v6cm4h4HWB!nKZd{aA)f&uVY@%8XL{Q6+AX`+vsqj{|JnIzKrLh@uAO>~PVsqm9`CFzQA4ay;3 zA;=kbdy=S+ucBXd&6Qgpu}N-9Ek9D(J~Ba-!-hcx*$7pneNxX@vySDf3T4CsY`P8b z`Rt9irn0DXaHg`rNC!W73zzp~G)u{>^rcjJDm+SaWigL0T_JZmtMn*3S57=ZZRV^> zRc15A7|&|a9rNDf>W=3vb9Wa{-k2g*WT> zJdOcBhqV~>N5qiJ7IzvpZrtOypaA>*mRPJS&)DaNO&R(MxcHa~_oty=#CoZINjc=G zn#m`T=fk+6fI}9x7ewGS9FRBzuM{+jSIGX5VIRe&sS#16wkRL_USEs0593oU7>-cL zVGWAH2rR>x#P48pp(T#!)6H0mz8BZL9>Ly1?kb{6ZpG>%jzfjnVlvfazhR$+K55Ze zlcDELK2E+%3{}=rsuopcjiDaE4yjI{N7w9K4S+A zwG0CmhZc-lmiU1pTrg}lVVh;aDD1T&B8_sGHq5S5;_Q5gzT!qWjvI5+e6<)hPVAL77qDO2v{S|Q8#cSqH=TQ4pEA2OAsI##;$bn%)FakefhqP==^xd$GVmxTUT?nNX zT*g2thqo|VYKcgqkA{68wn@tl>@DSDG37Rzz7x4Ug_oqdf+9>a)C=eP4$L?*pL5;>fp zLy%|ir4%LK;OnVjB^apWB4nt)kXnl|sV*_pLTbfwY^&t0>kRccy=+GCX8B>`k0*gKwtNoHY2e^viJG;J+sO@2}&l7^|U- z2Cows+eQmJsG5mhB@)cUfhr!&Gt}3qllca}l9n$q_}z4xLRdYMi&{fHN?uE_X(n%7 zZm3eXWgf0cH$+2D`Ryv^XZQ~W(Pf2~(}v_Plf;HUY0nfCirP1OtIemV>f z60cfBA+%wOVSj?+Y)Z0EWt6@0*oAF3bFnwcO?hPNV2r(uy;6PmI%ksPS1(?Y>PLpJ zpA6#Y3vqGIP^(EjkM$uQz4$+oe@^mJ$xa}zKa+TsXFc%@yX0E(U!H!HqCPQu%7){B zR7V?XGpTv#4RbL*NmVP(Aax2>OSLdbRgs?~wGx}cJUW+D)eO1w)dq z9_fKjY^aIIU%if+a@WvJcGMais40gY6wxsFu|bZruw9Nl*e}Oi6t!W;KO=EzZAkuv zwbzE_ABKKOovp^ax{zyajvZw?#_Gz!iS2b^$Iv7^R~Le86!XKd3mfEkAGXUe7yIXj PT_J-7XysBLL zVYv&9!e1&_;Bnxu`KR0mce^%KS>Bb_A&zd9A|A^+=3{Jjd}|-`T(mEJ*5&sktA~*c zyA+?Qh^U*^?`qApKB~1#>-V&FYyCH^3$#9_b)nYBwVtK*iTQDrbG~Xkr>c|c&I+74 zKEf^<&LkPloJnT1b`t+fYp+iBvDT$ppV5;zi9XfZ$@7`kPM-g2?G$@fYbWRbwARHY z+2^|BRQrY29zDT1t(}R#)VfIfUuo^r`fIH%t-sN_c$R)bt~*?JxYKdz&UUV!!69j0 zVW}l|dcTK3t9;Q*xI>;K?wM}b#VVcavWwKM1Am^q=bQBF)&e)b8BUelP`MRH@$IU) zLp8gxvAD1DOANMP%bd1kYz||WV2^PbRyue2x94@-%H=<-@ZOeZdhgBa_#epY=(Fe= zsPaCVXL?8SJNv1l_x)0peqlO~E5mmdn;NC~y!qSmpjUZ#@h5$k<9f{9hYzKuFH`1i z3)C7pGOQ$-7U2=x(!AAXYesSplz7mY20!$}TTU_xo=w@?Ev*s@~4ko$IiC7mm1jKTOsNUIUq0TmDqPr;J^z%Ek+;WF`3Pgiuh(Axn9T?Qm}A=}w3T zW74gH@%pWEJ5T4qM9}7qkj1ta0*i!<7CRvy8IkWp57IM~r${XnUbz}h!ywXosKS6j zD`0$4Og^i(nX%gqD!^aY%b$iso#0o)?z$LnhDp?~K&H-?<+OW?^t*9k#sLT|j>(@< z&dmmW4N{A3egq~K3wf{pnlZM=h-ro_(mN=_s6m%PpkBxddZL2{eH?m_CTQ%GK@Y>E7AS&OT(&8-d$GR?^u020aNOq){4s*r2;1Zrl8GNZEov3}dLP z;e9*C{Sa6x_+O!AX|Fs+=XYCyGk@-6T?*q%WAbKt-p37k8vG%fZ-ywgVMv68?4&#& z7?A@og7gDAP3H`{2_`YaUw}8*yb3}MLf%jB!D`y$Ts?gdM|ulA>1Kl_A%z+0U;<<2 zQieT7Oct^j^E(JE6Y_xbm1hTym|^HaI!x708FU5)mp#B=0DoA>O}TSInR0cGKosc} zf4j{gkiQ|iz@YsQST1-LT9)_nVR&r0EjQ2#-(y6shw0@pc|W-?8gwN@n|fV7 zFN`*I%FE~|%%x?_m9h_bv*6c*Y_{b&O0e39`4HmGHa`g|Y%hTcY>Oe=9ODzv(qhXV z+S(~&)SqCmMaZX!&KPtHjJJqnH9eS%HZE6H2Q6`l;V+>IbDyGI{0W++#)w}B z+^85!8Sy75&y#uag^cz?4M;<3rI3g6hACL85MGC20_l>eW&&piy)3d{t);`DHMUb5$BNPV{1!MjABe0MUBz zM}%z1qiV)xqHPdG8p)$--d_=24~dBA`Y}gp`&oz@XM*Tze8x12%a)%lJ@?z;eVS>th#j;KMtAB*Z3ze7W>xat%dz- zI{6vMu4yXTSnH(S0->%Z^!mCY{5lxJ_65jbdj^8t5k3Tc*fzizw(md&+eaX{Hp16H z-`Ysg=sYLKGcdlkiC+#GByA8}7vUSBZ(YPy+5!95wex2ogVqiRu8$P0>TrhK2cfmq F{|EY#&>jE) delta 3958 zcmZ9O32anV6o&7e(#}>|hQ-IN6r3VUOYwEew53!T3(DBC*{F~bwN6pu7PN=~+{U6N z7Kl)e5!{&?6A@9y8c|}{Tq21YTHJyOWU2yUR4Ryq3!Z!5c`}}t&h3By@7#0udGqnJ zavlDSm+Lx9zmng8l+%a!1U?gn&&4vwm#Zs2m@kWm8$YTZMI(2qh6B6~Tq93PKRoPd zE)MuE3+|NY7Hi@S2KNPo7?^0iO?ZcPbf5Nwyvd@NMjl=4H(DCqW3d&NKS!kqMn{q%OSQj!NqPVt~`f_onLfaRfC&r<0=wIEjxL?K8=KFnW;D zznD&(L??`P67?JHK6J;uCtibx74d?g^IU$Tm1tu+U0ilw7Qf`>_bfz=%IFE&%rw)+M8bLX6hENfp)FeL~k~RdTc| zFD&xjl4W}D&Z>kS$f}s5=2=nfeJsoLK9OD7Kozsa7whz6Hf3>b_)lXoRx3Gd$_ z=v~lW8drZ%+ZPxOK&I4|We_T}<+V^>W~u6YC*&B5{0!QV9@U-Vb;^+*e8Mmrc?3vEajQQKZd zw?hi)85;RoZ&kGUS_=XEp4OKh17kxMM~CB;;-m=%yf>LJ?5=!4LSw1H;b z%BT%{kiJdyK1NHRAL+vbo!wo}=mH4amI_hZR~bD3O-QS$?OsM7f_B@MKSA2IWC!d+ zJrvH^ahV786_)$}nk!n=Cv=8yAK>)wo-PGF6>-%{^L>WVK8QqYITsRGmO^L5QgxK4 zk3}wqJxITzeL2hM9O%ak??brKmV=?G(o!qwE=;3+cE@ugv?INPX1ajUrI5x97eNMN zyp&-Ti#YL*#ME6W-+K{fM(e^TW7*dng%9kM$wbUH2z`%85PhOjjNx@y^qlr=#T3A=&!QnO;B59tCKW>y)5!oXvdg1q^m5s5B62ra8uM5 z3eYKW$8s%%sx9@1uHsX|=t8Kkw&W3Lu5OX5;oWLmT|uk6l0{AfSrb>T`zE86kf>?# zob$roni=?o>U1`kme3u_8VFCeb@&%08C)O|-<7 z%>U83kgZ}W1HV#(M&>)eFNB!K{BqiV_ddDSLz)>3^3^S96jf5iM1v^2et!oChM`W?~Z&{Jn6PcyoKa(ZZ0+}XK(7cjq= zZbcFEU!rXr%lt*ud!^$i|9gdYQKG4=!He{|Y-Ik2lxQCFPtfb8mHBCkzw3WK{)EcV z&OGoxAjDZ(iB-(sP4!OZZ=?=3Grx%PZ)1L`u8+Xat@>9CKDw^u9dsD(h4#zvAThe1 z(sV!?=_W?c(IMOd8KiGyQ9T>l6Y-8|yC;ik;?hr{eyXKiO4t*#l8YF5S|&6Dj+faR|@WsS;TFAT@j3zx2?^xc~qF