diff --git a/Assets/Scripts/Simulation/SimulationState.cs b/Assets/Scripts/Simulation/SimulationState.cs index 7d0eb22..5cd62b6 100644 --- a/Assets/Scripts/Simulation/SimulationState.cs +++ b/Assets/Scripts/Simulation/SimulationState.cs @@ -7,8 +7,12 @@ namespace VMdemo.Simulation public class SimulationState { public int CurrentRound { get; set; } + public TranslationStep CurrentStep { get; set; } public ulong CurrentVirtualAddress { get; set; } public AddressParts CurrentAddressParts { get; set; } + public ulong CurrentPhysicalAddress { get; set; } + public ulong CurrentPfn { get; set; } + public bool HasCurrentPfn { get; set; } public bool IsPageFault { get; set; } public bool IsTlbHit { get; set; } public bool IsPageTableHit { get; set; } @@ -17,8 +21,12 @@ namespace VMdemo.Simulation public void Reset() { CurrentRound = 0; + CurrentStep = TranslationStep.GenerateVA; CurrentVirtualAddress = 0UL; CurrentAddressParts = AddressParts.Empty; + CurrentPhysicalAddress = 0UL; + CurrentPfn = 0UL; + HasCurrentPfn = false; IsPageFault = false; IsTlbHit = false; IsPageTableHit = false; diff --git a/Assets/Scripts/Simulation/TranslatorEngine.cs b/Assets/Scripts/Simulation/TranslatorEngine.cs new file mode 100644 index 0000000..10c0133 --- /dev/null +++ b/Assets/Scripts/Simulation/TranslatorEngine.cs @@ -0,0 +1,293 @@ +using System; +using VMdemo.Core; + +namespace VMdemo.Simulation +{ + public enum TranslationStep + { + GenerateVA = 0, + SplitVA = 1, + LookupTLB = 2, + LookupPageTable = 3, + HandlePageFault = 4, + ComposePA = 5, + Finalize = 6 + } + + public readonly struct AccessExecutionResult + { + public AccessExecutionResult( + int round, + ulong virtualAddress, + AddressParts addressParts, + ulong pfn, + ulong physicalAddress, + bool tlbHit, + bool pageTableHit, + bool pageFault, + EvictionInfo eviction) + { + Round = round; + VirtualAddress = virtualAddress; + AddressParts = addressParts; + Pfn = pfn; + PhysicalAddress = physicalAddress; + TlbHit = tlbHit; + PageTableHit = pageTableHit; + PageFault = pageFault; + Eviction = eviction; + } + + public int Round { get; } + public ulong VirtualAddress { get; } + public AddressParts AddressParts { get; } + public ulong Pfn { get; } + public ulong PhysicalAddress { get; } + public bool TlbHit { get; } + public bool PageTableHit { get; } + public bool PageFault { get; } + public EvictionInfo Eviction { get; } + } + + public class TranslatorEngine + { + private readonly SimulationConfig _config; + private readonly ConfigValidator.DerivedConfig _derivedConfig; + private readonly int? _seed; + + private AddressGenerator _addressGenerator; + private TlbCache _tlbCache; + private TwoLevelPageTable _pageTable; + private PhysicalMemoryManager _physicalMemoryManager; + + private bool _hasPendingForcedVirtualAddress; + private ulong _pendingForcedVirtualAddress; + private EvictionInfo _lastEviction; + + public TranslatorEngine(SimulationConfig config, int? seed = null) + { + if (!ConfigValidator.TryValidateAndBuildDerived(config, out var derived, out var errorMessage)) + { + throw new ArgumentException(errorMessage, nameof(config)); + } + + _config = config; + _derivedConfig = derived; + _seed = seed; + State = new SimulationState(); + + AddressTranslatorUtils.GetPageTableBitLayout(derived.VpnBits, out var l1Bits, out var l2Bits); + L1Bits = l1Bits; + L2Bits = l2Bits; + + _addressGenerator = new AddressGenerator(_config.vaBits, _seed); + _tlbCache = new TlbCache(_config.tlbEntries); + _pageTable = new TwoLevelPageTable(L1Bits, L2Bits); + _physicalMemoryManager = new PhysicalMemoryManager(_derivedConfig.FrameCount); + + Reset(); + } + + public SimulationState State { get; } + public TranslationStep CurrentStep => State.CurrentStep; + public int OffsetBits => _derivedConfig.OffsetBits; + public int L1Bits { get; } + public int L2Bits { get; } + public ulong MaxVirtualAddress => _addressGenerator.MaxVirtualAddress; + public TlbCache TlbCache => _tlbCache; + public TwoLevelPageTable PageTable => _pageTable; + public PhysicalMemoryManager PhysicalMemoryManager => _physicalMemoryManager; + public EvictionInfo LastEviction => _lastEviction; + + public void Reset() + { + State.Reset(); + _lastEviction = EvictionInfo.None; + _hasPendingForcedVirtualAddress = false; + _pendingForcedVirtualAddress = 0UL; + + _addressGenerator = new AddressGenerator(_config.vaBits, _seed); + _tlbCache = new TlbCache(_config.tlbEntries); + _pageTable = new TwoLevelPageTable(L1Bits, L2Bits); + _physicalMemoryManager = new PhysicalMemoryManager(_derivedConfig.FrameCount); + } + + public void SetNextVirtualAddress(ulong virtualAddress) + { + EnsureVirtualAddressInRange(virtualAddress); + _pendingForcedVirtualAddress = virtualAddress; + _hasPendingForcedVirtualAddress = true; + } + + public bool StepOnce() + { + switch (State.CurrentStep) + { + case TranslationStep.GenerateVA: + ExecuteGenerateVa(); + return false; + case TranslationStep.SplitVA: + ExecuteSplitVa(); + return false; + case TranslationStep.LookupTLB: + ExecuteLookupTlb(); + return false; + case TranslationStep.LookupPageTable: + ExecuteLookupPageTable(); + return false; + case TranslationStep.HandlePageFault: + ExecuteHandlePageFault(); + return false; + case TranslationStep.ComposePA: + ExecuteComposePa(); + return false; + case TranslationStep.Finalize: + ExecuteFinalize(); + return true; + default: + throw new InvalidOperationException($"未知翻译步骤:{State.CurrentStep}"); + } + } + + public AccessExecutionResult RunOneAccess(ulong? forcedVirtualAddress = null) + { + if (State.CurrentStep != TranslationStep.GenerateVA) + { + throw new InvalidOperationException("RunOneAccess 只能在 GenerateVA 起始步骤调用。"); + } + + if (forcedVirtualAddress.HasValue) + { + SetNextVirtualAddress(forcedVirtualAddress.Value); + } + + while (!StepOnce()) + { + } + + return new AccessExecutionResult( + State.CurrentRound, + State.CurrentVirtualAddress, + State.CurrentAddressParts, + State.CurrentPfn, + State.CurrentPhysicalAddress, + State.IsTlbHit, + State.IsPageTableHit, + State.IsPageFault, + _lastEviction); + } + + private void ExecuteGenerateVa() + { + State.IsTlbHit = false; + State.IsPageTableHit = false; + State.IsPageFault = false; + State.CurrentCost = 0; + State.CurrentAddressParts = AddressParts.Empty; + State.CurrentPhysicalAddress = 0UL; + State.CurrentPfn = 0UL; + State.HasCurrentPfn = false; + _lastEviction = EvictionInfo.None; + + if (_hasPendingForcedVirtualAddress) + { + State.CurrentVirtualAddress = _pendingForcedVirtualAddress; + _hasPendingForcedVirtualAddress = false; + _pendingForcedVirtualAddress = 0UL; + } + else + { + State.CurrentVirtualAddress = _addressGenerator.NextVirtualAddress(); + } + + State.CurrentStep = TranslationStep.SplitVA; + } + + private void ExecuteSplitVa() + { + State.CurrentAddressParts = AddressTranslatorUtils.SplitVirtualAddress( + State.CurrentVirtualAddress, + _config.vaBits, + _derivedConfig.OffsetBits); + + State.CurrentStep = TranslationStep.LookupTLB; + } + + private void ExecuteLookupTlb() + { + if (_tlbCache.TryLookup(State.CurrentAddressParts.Vpn, out var pfn)) + { + State.IsTlbHit = true; + State.CurrentPfn = pfn; + State.HasCurrentPfn = true; + State.CurrentStep = TranslationStep.ComposePA; + return; + } + + State.CurrentStep = TranslationStep.LookupPageTable; + } + + private void ExecuteLookupPageTable() + { + if (_pageTable.TryGetPresentPfn( + State.CurrentAddressParts.L1Index, + State.CurrentAddressParts.L2Index, + out var pfn)) + { + State.IsPageTableHit = true; + State.CurrentPfn = pfn; + State.HasCurrentPfn = true; + _tlbCache.InsertOrUpdate(State.CurrentAddressParts.Vpn, pfn); + State.CurrentStep = TranslationStep.ComposePA; + return; + } + + State.CurrentStep = TranslationStep.HandlePageFault; + } + + private void ExecuteHandlePageFault() + { + var result = MemoryAccessResolver.Resolve( + State.CurrentAddressParts.Vpn, + State.CurrentAddressParts.L1Index, + State.CurrentAddressParts.L2Index, + _pageTable, + _physicalMemoryManager, + _tlbCache); + + State.IsPageTableHit = result.PageTableHit; + State.IsPageFault = result.PageFault; + State.CurrentPfn = result.Pfn; + State.HasCurrentPfn = true; + _lastEviction = result.Eviction; + State.CurrentStep = TranslationStep.ComposePA; + } + + private void ExecuteComposePa() + { + if (!State.HasCurrentPfn) + { + throw new InvalidOperationException("ComposePA 阶段缺少 PFN,无法合成物理地址。"); + } + + State.CurrentPhysicalAddress = + (State.CurrentPfn << _derivedConfig.OffsetBits) | State.CurrentAddressParts.Offset; + + State.CurrentStep = TranslationStep.Finalize; + } + + private void ExecuteFinalize() + { + State.CurrentRound += 1; + State.CurrentStep = TranslationStep.GenerateVA; + } + + private void EnsureVirtualAddressInRange(ulong virtualAddress) + { + if (_config.vaBits < 64 && virtualAddress > _addressGenerator.MaxVirtualAddress) + { + throw new ArgumentOutOfRangeException(nameof(virtualAddress), "virtualAddress 超出 vaBits 可表示范围。"); + } + } + } +} diff --git a/Assets/Scripts/Simulation/TranslatorEngine.cs.meta b/Assets/Scripts/Simulation/TranslatorEngine.cs.meta new file mode 100644 index 0000000..48c1066 --- /dev/null +++ b/Assets/Scripts/Simulation/TranslatorEngine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 573321a5f8ef63248bc3d9baa95c450d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/Step1CoreModelTests.cs b/Assets/Tests/EditMode/Step1CoreModelTests.cs index 93caa89..662cdec 100644 --- a/Assets/Tests/EditMode/Step1CoreModelTests.cs +++ b/Assets/Tests/EditMode/Step1CoreModelTests.cs @@ -123,11 +123,15 @@ namespace VMdemo.Tests.EditMode state.Reset(); Assert.That(state.CurrentRound, Is.EqualTo(0)); + Assert.That(state.CurrentStep, Is.EqualTo(TranslationStep.GenerateVA)); Assert.That(state.CurrentVirtualAddress, Is.EqualTo(0UL)); Assert.That(state.CurrentAddressParts.Vpn, Is.EqualTo(0UL)); Assert.That(state.CurrentAddressParts.Offset, Is.EqualTo(0UL)); Assert.That(state.CurrentAddressParts.L1Index, Is.EqualTo(0UL)); Assert.That(state.CurrentAddressParts.L2Index, Is.EqualTo(0UL)); + Assert.That(state.CurrentPhysicalAddress, Is.EqualTo(0UL)); + Assert.That(state.CurrentPfn, Is.EqualTo(0UL)); + Assert.IsFalse(state.HasCurrentPfn); Assert.IsFalse(state.IsPageFault); Assert.IsFalse(state.IsTlbHit); Assert.IsFalse(state.IsPageTableHit); diff --git a/Assets/Tests/EditMode/Step5TranslatorEngineTests.cs b/Assets/Tests/EditMode/Step5TranslatorEngineTests.cs new file mode 100644 index 0000000..37133d5 --- /dev/null +++ b/Assets/Tests/EditMode/Step5TranslatorEngineTests.cs @@ -0,0 +1,192 @@ +using System.Collections.Generic; +using NUnit.Framework; +using VMdemo.Core; +using VMdemo.Simulation; + +namespace VMdemo.Tests.EditMode +{ + public class Step5TranslatorEngineTests + { + [Test] + public void StepOnce_FirstAccess_VisitsAllStagesInOrder() + { + var engine = CreateEngine(frameCountMb: 1, tlbEntries: 4, seed: 77); + engine.SetNextVirtualAddress(0x0000_1234UL); + + var visited = new List(); + bool completed; + do + { + visited.Add(engine.CurrentStep); + completed = engine.StepOnce(); + } while (!completed); + + var expected = new[] + { + TranslationStep.GenerateVA, + TranslationStep.SplitVA, + TranslationStep.LookupTLB, + TranslationStep.LookupPageTable, + TranslationStep.HandlePageFault, + TranslationStep.ComposePA, + TranslationStep.Finalize + }; + + CollectionAssert.AreEqual(expected, visited); + Assert.That(engine.State.CurrentRound, Is.EqualTo(1)); + Assert.That(engine.CurrentStep, Is.EqualTo(TranslationStep.GenerateVA)); + } + + [Test] + public void StepOnce_TlbHitPath_SkipsPageTableAndFaultStages() + { + var engine = CreateEngine(frameCountMb: 1, tlbEntries: 4, seed: 88); + const ulong va = 0x0000_4567UL; + + engine.RunOneAccess(va); // warmup: make resident + populate tlb + + engine.SetNextVirtualAddress(va); + var visited = new List(); + bool completed; + do + { + visited.Add(engine.CurrentStep); + completed = engine.StepOnce(); + } while (!completed); + + var expected = new[] + { + TranslationStep.GenerateVA, + TranslationStep.SplitVA, + TranslationStep.LookupTLB, + TranslationStep.ComposePA, + TranslationStep.Finalize + }; + + CollectionAssert.AreEqual(expected, visited); + Assert.IsTrue(engine.State.IsTlbHit); + Assert.IsFalse(engine.State.IsPageFault); + } + + [Test] + public void StepOnce_PageTableHitPath_WritesBackToTlb() + { + var engine = CreateEngine(frameCountMb: 1, tlbEntries: 2, seed: 99); + const ulong va = 0x0000_89ABUL; + var parts = AddressTranslatorUtils.SplitVirtualAddress(va, 32, engine.OffsetBits); + + engine.PageTable.SetPresent(parts.L1Index, parts.L2Index, pfn: 3UL); + engine.SetNextVirtualAddress(va); + + var visited = new List(); + bool completed; + do + { + visited.Add(engine.CurrentStep); + completed = engine.StepOnce(); + } while (!completed); + + var expected = new[] + { + TranslationStep.GenerateVA, + TranslationStep.SplitVA, + TranslationStep.LookupTLB, + TranslationStep.LookupPageTable, + TranslationStep.ComposePA, + TranslationStep.Finalize + }; + + CollectionAssert.AreEqual(expected, visited); + Assert.IsFalse(engine.State.IsTlbHit); + Assert.IsTrue(engine.State.IsPageTableHit); + Assert.IsFalse(engine.State.IsPageFault); + Assert.IsTrue(engine.TlbCache.TryLookup(parts.Vpn, out var cachedPfn)); + Assert.That(cachedPfn, Is.EqualTo(3UL)); + } + + [Test] + public void RunOneAccess_WhenMemoryFull_ProducesEvictionInfoAndSyncsState() + { + var config = new SimulationConfig + { + vaBits = 32, + pageSizeKB = 1024, // 1MB per page + physicalMemoryMB = 1, // 1 frame total + tlbEntries = 4, + accessCount = 10, + pageFaultPenalty = 100 + }; + var engine = new TranslatorEngine(config, seed: 11); + + var first = engine.RunOneAccess(0x0000_0123UL); + var second = engine.RunOneAccess(0x0010_0123UL); // different VPN + + Assert.IsTrue(first.PageFault); + Assert.IsTrue(second.PageFault); + Assert.IsTrue(second.Eviction.HasEvicted); + Assert.That(second.Eviction.EvictedVpn, Is.EqualTo(first.AddressParts.Vpn)); + Assert.IsFalse(engine.TlbCache.TryLookup(first.AddressParts.Vpn, out _)); + Assert.That(engine.LastEviction.EvictedVpn, Is.EqualTo(first.AddressParts.Vpn)); + } + + [Test] + public void RunOneAccess_ForcedVa_MatchesStepByStepOutcome() + { + const ulong va = 0x00AB_CDEFUL; + + var engineA = CreateEngine(frameCountMb: 1, tlbEntries: 4, seed: 123); + var result = engineA.RunOneAccess(va); + + var engineB = CreateEngine(frameCountMb: 1, tlbEntries: 4, seed: 123); + engineB.SetNextVirtualAddress(va); + while (!engineB.StepOnce()) + { + } + + Assert.That(engineB.State.CurrentVirtualAddress, Is.EqualTo(result.VirtualAddress)); + Assert.That(engineB.State.CurrentAddressParts.Vpn, Is.EqualTo(result.AddressParts.Vpn)); + Assert.That(engineB.State.CurrentPfn, Is.EqualTo(result.Pfn)); + Assert.That(engineB.State.CurrentPhysicalAddress, Is.EqualTo(result.PhysicalAddress)); + Assert.That(engineB.State.IsPageFault, Is.EqualTo(result.PageFault)); + Assert.That(engineB.State.IsPageTableHit, Is.EqualTo(result.PageTableHit)); + Assert.That(engineB.State.IsTlbHit, Is.EqualTo(result.TlbHit)); + Assert.That(engineB.State.CurrentRound, Is.EqualTo(result.Round)); + } + + [Test] + public void Reset_ClearsStateAndRuntimeCaches() + { + var engine = CreateEngine(frameCountMb: 1, tlbEntries: 4, seed: 222); + engine.RunOneAccess(0x0000_1000UL); + + Assert.That(engine.State.CurrentRound, Is.EqualTo(1)); + Assert.That(engine.TlbCache.Count, Is.GreaterThan(0)); + Assert.That(engine.PhysicalMemoryManager.ResidentCount, Is.GreaterThan(0)); + + engine.Reset(); + + Assert.That(engine.State.CurrentRound, Is.EqualTo(0)); + Assert.That(engine.State.CurrentStep, Is.EqualTo(TranslationStep.GenerateVA)); + Assert.That(engine.State.CurrentVirtualAddress, Is.EqualTo(0UL)); + Assert.That(engine.State.HasCurrentPfn, Is.False); + Assert.That(engine.TlbCache.Count, Is.EqualTo(0)); + Assert.That(engine.PhysicalMemoryManager.ResidentCount, Is.EqualTo(0)); + Assert.That(engine.LastEviction.HasEvicted, Is.False); + } + + private static TranslatorEngine CreateEngine(int frameCountMb, int tlbEntries, int seed) + { + var config = new SimulationConfig + { + vaBits = 32, + pageSizeKB = 4, + physicalMemoryMB = frameCountMb, + tlbEntries = tlbEntries, + accessCount = 10, + pageFaultPenalty = 100 + }; + + return new TranslatorEngine(config, seed); + } + } +} diff --git a/Assets/Tests/EditMode/Step5TranslatorEngineTests.cs.meta b/Assets/Tests/EditMode/Step5TranslatorEngineTests.cs.meta new file mode 100644 index 0000000..b4abb43 --- /dev/null +++ b/Assets/Tests/EditMode/Step5TranslatorEngineTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5fc6aae2a95a0224bb777ad21e64526c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/doc/MVP-TODO.md b/doc/MVP-TODO.md index a9e26b9..ab0fa9d 100644 --- a/doc/MVP-TODO.md +++ b/doc/MVP-TODO.md @@ -123,21 +123,21 @@ ### TODO -- [ ] 新建 `TranslatorEngine.cs` -- [ ] 定义步骤枚举: -- [ ] `GenerateVA` -- [ ] `SplitVA` -- [ ] `LookupTLB` -- [ ] `LookupPageTable` -- [ ] `HandlePageFault` -- [ ] `ComposePA` -- [ ] `Finalize` -- [ ] 提供 `StepOnce()` 与 `RunOneAccess()` 两种执行接口 +- [x] 新建 `TranslatorEngine.cs` +- [x] 定义步骤枚举: +- [x] `GenerateVA` +- [x] `SplitVA` +- [x] `LookupTLB` +- [x] `LookupPageTable` +- [x] `HandlePageFault` +- [x] `ComposePA` +- [x] `Finalize` +- [x] 提供 `StepOnce()` 与 `RunOneAccess()` 两种执行接口 ### 完成标准 -- [ ] 单步执行能暂停在每个阶段并暴露当前状态 -- [ ] 一次完整访问流程结果与预期一致 +- [x] 单步执行能暂停在每个阶段并暴露当前状态 +- [x] 一次完整访问流程结果与预期一致 ## 7. Step 6 - 统计模块