// TsMemory — TSConfig 256-page × 16KB paged memory with page protection
// Enhanced: write protection, dirty tracking, violation logging
using System;
using System.Collections.Generic;
namespace InflateDebugger
{
public class TsMemory
{
public const int PageSize = 0x4000; // 16KB
public const int TotalPages = 256; // 4 MB
// Physical memory: 256 pages × 16KB
private readonly byte[][] _pages;
// Window → physical page mapping
// Win0 = 0x0000–0x3FFF, Win1 = 0x4000–0x7FFF,
// Win2 = 0x8000–0xBFFF, Win3 = 0xC000–0xFFFF
private readonly byte[] _winPage = new byte[4];
// TSConfig page ports
public const ushort PORT_W0 = 0x10AF;
public const ushort PORT_W1 = 0x11AF;
public const ushort PORT_W2 = 0x12AF;
public const ushort PORT_W3 = 0x13AF;
// ── Page protection ──────────────────────────────────────────
/// Set of physical page numbers that are write-protected
private readonly HashSet _protectedPages = new();
/// Pages that have been written to (dirty tracking)
private readonly HashSet _dirtyPages = new();
/// Protection violation log
public List Violations { get; } = new();
/// Max violations to log before stopping (0 = unlimited)
public int MaxViolations { get; set; } = 100;
/// If true, throw on protection violation
public bool ThrowOnViolation { get; set; } = false;
/// Callback invoked on every write. Args: z80addr, page, offset, value
public Action OnWrite { get; set; }
/// Callback invoked on protection violation
public Action OnViolation { get; set; }
public TsMemory()
{
_pages = new byte[TotalPages][];
for (int i = 0; i < TotalPages; i++)
_pages[i] = new byte[PageSize];
}
// ── Page protection API ──────────────────────────────────────
public void ProtectPage(int page) => _protectedPages.Add(page);
public void UnprotectPage(int page) => _protectedPages.Remove(page);
public void ProtectRange(int startPage, int count)
{
for (int i = 0; i < count; i++) _protectedPages.Add(startPage + i);
}
public void UnprotectRange(int startPage, int count)
{
for (int i = 0; i < count; i++) _protectedPages.Remove(startPage + i);
}
public bool IsProtected(int page) => _protectedPages.Contains(page);
public void ClearProtection() => _protectedPages.Clear();
public IReadOnlyCollection DirtyPages => _dirtyPages;
public void ClearDirtyTracking() => _dirtyPages.Clear();
// ── Window management ────────────────────────────────────────
/// Set window mapping (0–3) to physical page
public void SetWindow(int win, byte page)
{
_winPage[win] = page;
}
public byte GetWindow(int win) => _winPage[win];
// ── Memory access ────────────────────────────────────────────
/// Read byte at 16-bit Z80 address through page windows
public byte Read(ushort addr)
{
int win = addr >> 14; // 0–3
int offset = addr & 0x3FFF;
return _pages[_winPage[win]][offset];
}
/// Write byte at 16-bit Z80 address (with protection check)
public void Write(ushort addr, byte val)
{
int win = addr >> 14;
int offset = addr & 0x3FFF;
byte page = _winPage[win];
_dirtyPages.Add(page);
OnWrite?.Invoke(addr, page, offset, val);
if (_protectedPages.Contains(page))
{
var v = new ProtectionViolation
{
Z80Addr = addr,
PhysPage = page,
Offset = offset,
Value = val,
OldValue = _pages[page][offset],
Window = win
};
if (Violations.Count < MaxViolations || MaxViolations == 0)
Violations.Add(v);
OnViolation?.Invoke(v);
if (ThrowOnViolation)
throw new ProtectionViolationException(v);
}
_pages[page][offset] = val;
}
/// Direct write bypassing protection (for initial data loading)
public void WriteUnprotected(ushort addr, byte val)
{
int win = addr >> 14;
int offset = addr & 0x3FFF;
_pages[_winPage[win]][offset] = val;
}
/// Direct access to a physical page
public byte[] GetPage(int page) => _pages[page];
/// Load data into physical pages starting at startPage
public int LoadToPages(byte[] data, int startPage)
{
int pagesNeeded = (data.Length + PageSize - 1) / PageSize;
for (int p = 0; p < pagesNeeded; p++)
{
int srcOff = p * PageSize;
int len = Math.Min(PageSize, data.Length - srcOff);
Array.Copy(data, srcOff, _pages[startPage + p], 0, len);
}
return pagesNeeded;
}
/// Handle OUT to TSConfig page port
public bool HandlePort(ushort port, byte value)
{
switch (port)
{
case PORT_W0: SetWindow(0, value); return true;
case PORT_W1: SetWindow(1, value); return true;
case PORT_W2: SetWindow(2, value); return true;
case PORT_W3: SetWindow(3, value); return true;
default: return false;
}
}
/// Read all output pages (destination) into a flat array
public byte[] ReadPages(int startPage, int count)
{
byte[] result = new byte[count * PageSize];
for (int p = 0; p < count; p++)
Array.Copy(_pages[startPage + p], 0, result, p * PageSize, PageSize);
return result;
}
/// Update WC system variable area (0x6000–0x600E)
public void UpdateSysVars()
{
byte sysPage = _winPage[1];
_pages[sysPage][0x2000] = _winPage[0]; // 0x6000 = PG0
_pages[sysPage][0x2001] = _winPage[1]; // 0x6001 = PG4
_pages[sysPage][0x2002] = _winPage[2]; // 0x6002 = PG8
_pages[sysPage][0x2003] = _winPage[3]; // 0x6003 = PGC
}
/// Dump 16 bytes around an address for debugging
public string HexDump(ushort addr, int before = 8, int after = 8)
{
int start = Math.Max(0, addr - before);
int end = Math.Min(0xFFFF, addr + after);
var sb = new System.Text.StringBuilder();
for (int a = start; a <= end; a++)
{
if (a == addr) sb.Append('[');
sb.AppendFormat("{0:X2}", Read((ushort)a));
if (a == addr) sb.Append(']');
else sb.Append(' ');
}
return sb.ToString();
}
}
// ── Protection violation types ───────────────────────────────────
public struct ProtectionViolation
{
public ushort Z80Addr;
public byte PhysPage;
public int Offset;
public byte Value;
public byte OldValue;
public int Window;
public override string ToString() =>
$"PROTECT VIOLATION: [{Z80Addr:X4}] pg={PhysPage:X2} off={Offset:X4} " +
$"old={OldValue:X2} new={Value:X2} win={Window}";
}
public class ProtectionViolationException : Exception
{
public ProtectionViolation Violation { get; }
public ProtectionViolationException(ProtectionViolation v)
: base(v.ToString()) { Violation = v; }
}
}