finish(maybe...)

This commit is contained in:
2026-04-14 21:30:38 +08:00
parent 785e92ec8b
commit 2c61ef07ff
172 changed files with 202485 additions and 68 deletions
@@ -0,0 +1,8 @@
namespace VMdemo.Core
{
public enum AddressGenerationMode
{
RandomUniform = 0,
SequentialArrayLoop = 1
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d91aaaf0f71455ba5f1707f02b3f7b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+39 -2
View File
@@ -40,9 +40,21 @@ namespace VMdemo.Core
return false;
}
if (config.vaBits != 32 && config.vaBits != 48 && config.vaBits != 64)
if (config.machineBits <= 0 || config.machineBits > 64)
{
errorMessage = "vaBits 仅支持 32、48、64。";
errorMessage = "machineBits 必须在 1 到 64 之间。";
return false;
}
if (config.vaBits != 8 && config.vaBits != 16 && config.vaBits != 24)
{
errorMessage = "vaBits 仅支持 8、16、24。";
return false;
}
if (config.vaBits > config.machineBits)
{
errorMessage = $"vaBits 不能大于 machineBits,当前 vaBits={config.vaBits}, machineBits={config.machineBits}。";
return false;
}
@@ -82,6 +94,31 @@ namespace VMdemo.Core
return false;
}
if (config.arrayLengthBytes <= 0)
{
errorMessage = "arrayLengthBytes 必须是正整数。";
return false;
}
if (config.arrayElementBytes <= 0)
{
errorMessage = "arrayElementBytes 必须是正整数。";
return false;
}
if (config.arrayBaseAddress < 0)
{
errorMessage = "arrayBaseAddress 不能为负数。";
return false;
}
if (config.addressGenerationMode == AddressGenerationMode.SequentialArrayLoop &&
config.arrayLengthBytes < config.arrayElementBytes)
{
errorMessage = "SequentialArrayLoop 模式下 arrayLengthBytes 必须 >= arrayElementBytes。";
return false;
}
var pageSizeBytes = config.PageSizeBytes;
if (!TryGetLog2(pageSizeBytes, out var offsetBits))
{
+24 -4
View File
@@ -5,12 +5,32 @@ namespace VMdemo.Core
[Serializable]
public class SimulationConfig
{
public int vaBits = 32;
public int pageSizeKB = 4;
public int physicalMemoryMB = 64;
public int tlbEntries = 16;
public const int FixedMachineBits = 32;
public const int FixedVaBits = 24;
public const int FixedPageSizeKB = 4;
public const int FixedPhysicalMemoryMB = 1;
public const int FixedTlbEntries = 8;
public int machineBits = FixedMachineBits;
public int vaBits = FixedVaBits;
public int pageSizeKB = FixedPageSizeKB;
public int physicalMemoryMB = FixedPhysicalMemoryMB;
public int tlbEntries = FixedTlbEntries;
public int accessCount = 100;
public int pageFaultPenalty = 100;
public AddressGenerationMode addressGenerationMode = AddressGenerationMode.SequentialArrayLoop;
public int arrayLengthBytes = 256;
public int arrayElementBytes = 4;
public int arrayBaseAddress = 0;
public void ApplyFixedCoreSpec()
{
machineBits = FixedMachineBits;
vaBits = FixedVaBits;
pageSizeKB = FixedPageSizeKB;
physicalMemoryMB = FixedPhysicalMemoryMB;
tlbEntries = FixedTlbEntries;
}
public ulong PageSizeBytes => pageSizeKB > 0 ? (ulong)pageSizeKB * 1024UL : 0UL;
+61 -7
View File
@@ -1,4 +1,5 @@
using System;
using VMdemo.Core;
namespace VMdemo.Simulation
{
@@ -6,28 +7,74 @@ namespace VMdemo.Simulation
{
public const int DefaultSeed = 12345;
private readonly int _vaBits;
private readonly ulong _vaMask;
private readonly Random _random;
private readonly byte[] _buffer = new byte[8];
private readonly AddressGenerationMode _mode;
private readonly ulong _arrayBaseAddress;
private readonly ulong _arrayElementBytes;
private readonly ulong _arrayElementCount;
private ulong _nextArrayIndex;
public AddressGenerator(int vaBits, int? seed = null)
public AddressGenerator(
int vaBits,
int? seed = null,
AddressGenerationMode mode = AddressGenerationMode.RandomUniform,
int arrayLengthBytes = 256,
int arrayElementBytes = 4,
int arrayBaseAddress = 0)
{
if (vaBits != 32 && vaBits != 48 && vaBits != 64)
if (vaBits != 8 && vaBits != 16 && vaBits != 24)
{
throw new ArgumentOutOfRangeException(nameof(vaBits), "vaBits 仅支持 32、48、64。");
throw new ArgumentOutOfRangeException(nameof(vaBits), "vaBits 仅支持 8、16、24。");
}
_vaBits = vaBits;
_vaMask = vaBits == 64 ? ulong.MaxValue : (1UL << vaBits) - 1UL;
_vaMask = (1UL << vaBits) - 1UL;
_random = new Random(seed ?? DefaultSeed);
_mode = mode;
if (arrayLengthBytes <= 0)
{
throw new ArgumentOutOfRangeException(nameof(arrayLengthBytes), "arrayLengthBytes 必须是正整数。");
}
if (arrayElementBytes <= 0)
{
throw new ArgumentOutOfRangeException(nameof(arrayElementBytes), "arrayElementBytes 必须是正整数。");
}
if (arrayBaseAddress < 0)
{
throw new ArgumentOutOfRangeException(nameof(arrayBaseAddress), "arrayBaseAddress 不能为负数。");
}
if (mode == AddressGenerationMode.SequentialArrayLoop && arrayLengthBytes < arrayElementBytes)
{
throw new ArgumentOutOfRangeException(
nameof(arrayLengthBytes),
"SequentialArrayLoop 模式下 arrayLengthBytes 必须 >= arrayElementBytes。");
}
_arrayBaseAddress = (ulong)arrayBaseAddress;
_arrayElementBytes = (ulong)arrayElementBytes;
_arrayElementCount = Math.Max(1UL, (ulong)arrayLengthBytes / _arrayElementBytes);
var maxGenerated = _arrayBaseAddress + (_arrayElementCount - 1UL) * _arrayElementBytes;
if (maxGenerated > _vaMask)
{
throw new ArgumentOutOfRangeException(
nameof(arrayLengthBytes),
$"数组访问地址超出 vaBits 范围。maxGenerated={maxGenerated}, maxVA={_vaMask}");
}
}
public ulong MaxVirtualAddress => _vaMask;
public ulong NextVirtualAddress()
{
return NextRawUlong() & _vaMask;
return _mode == AddressGenerationMode.SequentialArrayLoop
? NextArrayLoopAddress()
: (NextRawUlong() & _vaMask);
}
private ulong NextRawUlong()
@@ -35,5 +82,12 @@ namespace VMdemo.Simulation
_random.NextBytes(_buffer);
return BitConverter.ToUInt64(_buffer, 0);
}
private ulong NextArrayLoopAddress()
{
var address = _arrayBaseAddress + _nextArrayIndex * _arrayElementBytes;
_nextArrayIndex = (_nextArrayIndex + 1UL) % _arrayElementCount;
return address;
}
}
}
@@ -17,6 +17,7 @@ namespace VMdemo.Simulation
public bool IsTlbHit { get; set; }
public bool IsPageTableHit { get; set; }
public int CurrentCost { get; set; }
public string LastDecisionReason { get; set; }
public void Reset()
{
@@ -31,6 +32,7 @@ namespace VMdemo.Simulation
IsTlbHit = false;
IsPageTableHit = false;
CurrentCost = 0;
LastDecisionReason = string.Empty;
}
}
}
+50 -9
View File
@@ -81,7 +81,7 @@ namespace VMdemo.Simulation
L1Bits = l1Bits;
L2Bits = l2Bits;
_addressGenerator = new AddressGenerator(_config.vaBits, _seed);
_addressGenerator = CreateAddressGenerator();
_tlbCache = new TlbCache(_config.tlbEntries);
_pageTable = new TwoLevelPageTable(L1Bits, L2Bits);
_physicalMemoryManager = new PhysicalMemoryManager(_derivedConfig.FrameCount);
@@ -91,6 +91,10 @@ namespace VMdemo.Simulation
public SimulationState State { get; }
public TranslationStep CurrentStep => State.CurrentStep;
public int MachineBits => _config.machineBits;
public int VaBits => _config.vaBits;
public int PageSizeKB => _config.pageSizeKB;
public int PhysicalMemoryMB => _config.physicalMemoryMB;
public int OffsetBits => _derivedConfig.OffsetBits;
public int L1Bits { get; }
public int L2Bits { get; }
@@ -109,7 +113,7 @@ namespace VMdemo.Simulation
_pendingForcedVirtualAddress = 0UL;
Stats.Reset();
_addressGenerator = new AddressGenerator(_config.vaBits, _seed);
_addressGenerator = CreateAddressGenerator();
_tlbCache = new TlbCache(_config.tlbEntries);
_pageTable = new TwoLevelPageTable(L1Bits, L2Bits);
_physicalMemoryManager = new PhysicalMemoryManager(_derivedConfig.FrameCount);
@@ -190,6 +194,7 @@ namespace VMdemo.Simulation
State.CurrentPhysicalAddress = 0UL;
State.CurrentPfn = 0UL;
State.HasCurrentPfn = false;
State.LastDecisionReason = "已生成本轮虚拟地址,准备进行地址拆分。";
_lastEviction = EvictionInfo.None;
if (_hasPendingForcedVirtualAddress)
@@ -212,6 +217,7 @@ namespace VMdemo.Simulation
State.CurrentVirtualAddress,
_config.vaBits,
_derivedConfig.OffsetBits);
State.LastDecisionReason = "已拆分 VA 为 VPN/Offset,并从 VPN 拆分出 L1/L2 索引。";
State.CurrentStep = TranslationStep.LookupTLB;
}
@@ -223,28 +229,42 @@ namespace VMdemo.Simulation
State.IsTlbHit = true;
State.CurrentPfn = pfn;
State.HasCurrentPfn = true;
State.LastDecisionReason =
$"TLB 命中:VPN={State.CurrentAddressParts.Vpn} 命中 PFN={pfn},跳过页表,直接合成物理地址。";
State.CurrentStep = TranslationStep.ComposePA;
return;
}
State.LastDecisionReason =
$"TLB 未命中:VPN={State.CurrentAddressParts.Vpn} 未找到映射,转入页表查询。";
State.CurrentStep = TranslationStep.LookupPageTable;
}
private void ExecuteLookupPageTable()
{
if (_pageTable.TryGetPresentPfn(
State.CurrentAddressParts.L1Index,
State.CurrentAddressParts.L2Index,
out var pfn))
var probe = _pageTable.Probe(State.CurrentAddressParts.L1Index, State.CurrentAddressParts.L2Index);
if (probe.Status == PageTableProbeStatus.PresentEntry)
{
State.IsPageTableHit = true;
State.CurrentPfn = pfn;
State.CurrentPfn = probe.Entry.Pfn;
State.HasCurrentPfn = true;
_tlbCache.InsertOrUpdate(State.CurrentAddressParts.Vpn, pfn);
_tlbCache.InsertOrUpdate(State.CurrentAddressParts.Vpn, probe.Entry.Pfn);
State.LastDecisionReason =
$"页表命中:L1={State.CurrentAddressParts.L1Index}, L2={State.CurrentAddressParts.L2Index}, PFN={probe.Entry.Pfn},并写回 TLB。";
State.CurrentStep = TranslationStep.ComposePA;
return;
}
State.LastDecisionReason = probe.Status switch
{
PageTableProbeStatus.MissingL1Table =>
$"页表未命中:L1={State.CurrentAddressParts.L1Index} 对应二级页表不存在,触发缺页处理。",
PageTableProbeStatus.MissingL2Entry =>
$"页表未命中:L2={State.CurrentAddressParts.L2Index} 页表项不存在,触发缺页处理。",
PageTableProbeStatus.NotPresentEntry =>
$"页表项无效:L2={State.CurrentAddressParts.L2Index} present=false,触发缺页处理。",
_ => "页表未命中,触发缺页处理。"
};
State.CurrentStep = TranslationStep.HandlePageFault;
}
@@ -263,6 +283,9 @@ namespace VMdemo.Simulation
State.CurrentPfn = result.Pfn;
State.HasCurrentPfn = true;
_lastEviction = result.Eviction;
State.LastDecisionReason = result.Eviction.HasEvicted
? $"缺页处理完成:装入 PFN={result.Pfn},并按 FIFO 淘汰 VPN={result.Eviction.EvictedVpn}。"
: $"缺页处理完成:装入 PFN={result.Pfn},当前仍有空闲物理帧。";
State.CurrentStep = TranslationStep.ComposePA;
}
@@ -275,6 +298,8 @@ namespace VMdemo.Simulation
State.CurrentPhysicalAddress =
(State.CurrentPfn << _derivedConfig.OffsetBits) | State.CurrentAddressParts.Offset;
State.LastDecisionReason =
$"物理地址合成:PA = (PFN << offsetBits) | offset = ({State.CurrentPfn} << {_derivedConfig.OffsetBits}) | {State.CurrentAddressParts.Offset}。";
State.CurrentStep = TranslationStep.Finalize;
}
@@ -291,6 +316,11 @@ namespace VMdemo.Simulation
State.IsPageTableHit,
State.IsPageFault,
State.CurrentCost);
State.LastDecisionReason = State.IsPageFault
? $"本轮完成:TLB 未命中 + 页表未命中,发生缺页,开销={State.CurrentCost}。"
: State.IsTlbHit
? $"本轮完成:TLB 命中,开销={State.CurrentCost}。"
: $"本轮完成:TLB 未命中 + 页表命中,开销={State.CurrentCost}。";
State.CurrentRound += 1;
State.CurrentStep = TranslationStep.GenerateVA;
@@ -298,10 +328,21 @@ namespace VMdemo.Simulation
private void EnsureVirtualAddressInRange(ulong virtualAddress)
{
if (_config.vaBits < 64 && virtualAddress > _addressGenerator.MaxVirtualAddress)
if (virtualAddress > _addressGenerator.MaxVirtualAddress)
{
throw new ArgumentOutOfRangeException(nameof(virtualAddress), "virtualAddress 超出 vaBits 可表示范围。");
}
}
private AddressGenerator CreateAddressGenerator()
{
return new AddressGenerator(
_config.vaBits,
_seed,
_config.addressGenerationMode,
_config.arrayLengthBytes,
_config.arrayElementBytes,
_config.arrayBaseAddress);
}
}
}
@@ -3,6 +3,28 @@ using System.Collections.Generic;
namespace VMdemo.Simulation
{
public enum PageTableProbeStatus
{
MissingL1Table = 0,
MissingL2Entry = 1,
NotPresentEntry = 2,
PresentEntry = 3
}
public readonly struct PageTableProbeResult
{
public PageTableProbeResult(PageTableProbeStatus status, bool hasEntry, PageTableEntry entry)
{
Status = status;
HasEntry = hasEntry;
Entry = entry;
}
public PageTableProbeStatus Status { get; }
public bool HasEntry { get; }
public PageTableEntry Entry { get; }
}
public class TwoLevelPageTable
{
private readonly Dictionary<ulong, Dictionary<ulong, PageTableEntry>> _table;
@@ -73,6 +95,40 @@ namespace VMdemo.Simulation
return false;
}
public PageTableProbeResult Probe(ulong l1Index, ulong l2Index)
{
ValidateIndices(l1Index, l2Index);
if (!_table.TryGetValue(l1Index, out var l2Table))
{
return new PageTableProbeResult(
PageTableProbeStatus.MissingL1Table,
hasEntry: false,
entry: PageTableEntry.NotPresent);
}
if (!l2Table.TryGetValue(l2Index, out var entry))
{
return new PageTableProbeResult(
PageTableProbeStatus.MissingL2Entry,
hasEntry: false,
entry: PageTableEntry.NotPresent);
}
if (!entry.Present)
{
return new PageTableProbeResult(
PageTableProbeStatus.NotPresentEntry,
hasEntry: true,
entry: entry);
}
return new PageTableProbeResult(
PageTableProbeStatus.PresentEntry,
hasEntry: true,
entry: entry);
}
public void SetPresent(ulong l1Index, ulong l2Index, ulong pfn, bool dirty = false)
{
ValidateIndices(l1Index, l2Index);
@@ -0,0 +1,232 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using VMdemo.Core;
using System;
namespace VMdemo.UI
{
public class SimulationConfigInputView : MonoBehaviour
{
[Header("Fixed Core Spec")] [SerializeField]
private TMP_Text fixedCoreSpecText;
[SerializeField] private TMP_InputField accessCountInput;
[SerializeField] private TMP_InputField pageFaultPenaltyInput;
[SerializeField] private TMP_Dropdown addressModeDropdown;
[SerializeField] private TMP_InputField arrayLengthBytesInput;
[SerializeField] private TMP_InputField arrayElementBytesInput;
[SerializeField] private TMP_InputField arrayBaseAddressInput;
[SerializeField] private Button saveButton;
[SerializeField] private Button cancelButton;
[SerializeField] private GameObject panelRoot;
public event Action SaveClicked;
public event Action CancelClicked;
public bool IsPanelVisible => panelRoot != null ? panelRoot.activeInHierarchy : gameObject.activeInHierarchy;
private void Awake()
{
BindButtons();
ApplyFixedCoreSpecVisual();
}
public bool TryBuildConfig(out SimulationConfig config, out string errorMessage)
{
config = new SimulationConfig
{
machineBits = SimulationConfig.FixedMachineBits,
vaBits = SimulationConfig.FixedVaBits,
pageSizeKB = SimulationConfig.FixedPageSizeKB,
physicalMemoryMB = SimulationConfig.FixedPhysicalMemoryMB,
tlbEntries = SimulationConfig.FixedTlbEntries,
accessCount = ParseInt(accessCountInput, 100),
pageFaultPenalty = ParseInt(pageFaultPenaltyInput, 100),
addressGenerationMode = ReadAddressGenerationMode(),
arrayLengthBytes = ParseInt(arrayLengthBytesInput, 256),
arrayElementBytes = ParseInt(arrayElementBytesInput, 4),
arrayBaseAddress = ParseInt(arrayBaseAddressInput, 0)
};
config.ApplyFixedCoreSpec();
return ConfigValidator.TryValidate(config, out errorMessage);
}
public void PopulateFromConfig(SimulationConfig config)
{
if (config == null)
{
return;
}
SetInputText(accessCountInput, config.accessCount);
SetInputText(pageFaultPenaltyInput, config.pageFaultPenalty);
SetAddressGenerationMode(config.addressGenerationMode);
SetInputText(arrayLengthBytesInput, config.arrayLengthBytes);
SetInputText(arrayElementBytesInput, config.arrayElementBytes);
SetInputText(arrayBaseAddressInput, config.arrayBaseAddress);
ApplyFixedCoreSpecVisual();
}
public void SetPanelVisible(bool visible)
{
if (panelRoot != null)
{
panelRoot.SetActive(visible);
if (visible)
{
ApplyFixedCoreSpecVisual();
}
return;
}
gameObject.SetActive(visible);
if (visible)
{
ApplyFixedCoreSpecVisual();
}
}
private void BindButtons()
{
if (saveButton != null)
{
saveButton.onClick.RemoveAllListeners();
saveButton.onClick.AddListener(() => SaveClicked?.Invoke());
}
if (cancelButton != null)
{
cancelButton.onClick.RemoveAllListeners();
cancelButton.onClick.AddListener(() => CancelClicked?.Invoke());
}
}
private void ApplyFixedCoreSpecVisual()
{
if (fixedCoreSpecText != null)
{
fixedCoreSpecText.text =
$"{SimulationConfig.FixedMachineBits}位机\n" +
$"VA {SimulationConfig.FixedVaBits}位\n" +
$"页大小 {SimulationConfig.FixedPageSizeKB}KB\n" +
$"物理内存 {SimulationConfig.FixedPhysicalMemoryMB}MB\n" +
$"TLB {SimulationConfig.FixedTlbEntries}条全相联";
}
}
private static void SetInputText(TMP_InputField input, int value)
{
if (input != null)
{
input.text = value.ToString();
}
}
private static void SetInteractable(Selectable selectable, bool interactable)
{
if (selectable != null)
{
selectable.interactable = interactable;
}
}
private AddressGenerationMode ReadAddressGenerationMode()
{
if (addressModeDropdown == null || addressModeDropdown.options == null ||
addressModeDropdown.options.Count == 0)
{
return AddressGenerationMode.SequentialArrayLoop;
}
// Preferred convention:
// index 0 = RandomUniform, index 1 = SequentialArrayLoop.
if (addressModeDropdown.options.Count >= 2)
{
return addressModeDropdown.value == 0
? AddressGenerationMode.RandomUniform
: AddressGenerationMode.SequentialArrayLoop;
}
var option = addressModeDropdown.options[addressModeDropdown.value].text;
if (IsRandomOption(option))
{
return AddressGenerationMode.RandomUniform;
}
if (IsSequentialOption(option))
{
return AddressGenerationMode.SequentialArrayLoop;
}
return AddressGenerationMode.SequentialArrayLoop;
}
private void SetAddressGenerationMode(AddressGenerationMode mode)
{
if (addressModeDropdown == null || addressModeDropdown.options == null ||
addressModeDropdown.options.Count == 0)
{
return;
}
if (addressModeDropdown.options.Count >= 2)
{
addressModeDropdown.value = mode == AddressGenerationMode.RandomUniform ? 0 : 1;
addressModeDropdown.RefreshShownValue();
return;
}
for (var i = 0; i < addressModeDropdown.options.Count; i++)
{
var option = addressModeDropdown.options[i].text;
if ((mode == AddressGenerationMode.RandomUniform && IsRandomOption(option)) ||
(mode == AddressGenerationMode.SequentialArrayLoop && IsSequentialOption(option)))
{
addressModeDropdown.value = i;
addressModeDropdown.RefreshShownValue();
return;
}
}
}
private static bool IsRandomOption(string optionText)
{
if (string.IsNullOrEmpty(optionText))
{
return false;
}
return optionText.Contains("Random") ||
optionText.Contains("随机") ||
optionText.Contains("rand");
}
private static bool IsSequentialOption(string optionText)
{
if (string.IsNullOrEmpty(optionText))
{
return false;
}
return optionText.Contains("Sequential") ||
optionText.Contains("ArrayLoop") ||
optionText.Contains("数组") ||
optionText.Contains("顺序") ||
optionText.Contains("for");
}
private static int ParseInt(TMP_InputField input, int fallback)
{
if (input == null)
{
return fallback;
}
return int.TryParse(input.text, out var value) ? value : fallback;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93f3af00cf194cf4bf98f5df86f86ffc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,74 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace VMdemo.UI
{
public class SimulationControlView : MonoBehaviour
{
[SerializeField] private Button stepButton;
[SerializeField] private Button playButton;
[SerializeField] private Button pauseButton;
[SerializeField] private Button resetButton;
public event Action StepClicked;
public event Action PlayClicked;
public event Action PauseClicked;
public event Action ResetClicked;
private void Awake()
{
Bind();
}
public void SetPlaying(bool isPlaying)
{
if (stepButton != null)
{
stepButton.interactable = !isPlaying;
}
if (playButton != null)
{
playButton.interactable = !isPlaying;
}
if (pauseButton != null)
{
pauseButton.interactable = isPlaying;
}
if (resetButton != null)
{
resetButton.interactable = true;
}
}
private void Bind()
{
if (stepButton != null)
{
stepButton.onClick.RemoveAllListeners();
stepButton.onClick.AddListener(() => StepClicked?.Invoke());
}
if (playButton != null)
{
playButton.onClick.RemoveAllListeners();
playButton.onClick.AddListener(() => PlayClicked?.Invoke());
}
if (pauseButton != null)
{
pauseButton.onClick.RemoveAllListeners();
pauseButton.onClick.AddListener(() => PauseClicked?.Invoke());
}
if (resetButton != null)
{
resetButton.onClick.RemoveAllListeners();
resetButton.onClick.AddListener(() => ResetClicked?.Invoke());
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 40ec73de0f6f4717a043fc64ff3c3b13
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using TMPro;
using UnityEngine;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public class SimulationDashboardView : MonoBehaviour
{
[SerializeField] private TMP_Text stateSummaryText;
[SerializeField] private TMP_Text statsText;
[SerializeField] private TMP_Text errorText;
public void ShowNotInitialized()
{
SetText(stateSummaryText, "引擎:未初始化");
SetText(statsText, "统计:N/A");
}
public void Render(TranslatorEngine engine)
{
if (engine == null)
{
ShowNotInitialized();
return;
}
SetText(
stateSummaryText,
SimulationUIFormatter.FormatStateSummary(
engine.State,
engine.MachineBits,
engine.VaBits,
engine.PageSizeKB,
engine.PhysicalMemoryMB,
engine.TlbCache.Capacity));
SetText(statsText, SimulationUIFormatter.FormatStats(engine.Stats));
}
public void SetError(string message)
{
SetText(errorText, message);
}
private static void SetText(TMP_Text target, string value)
{
if (target != null)
{
target.text = value;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9b68de53ac3e4c49a27b25bf5f91821b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using UnityEngine;
using UnityEngine.UI;
namespace VMdemo.UI
{
public class SimulationDetailDrawerView : MonoBehaviour
{
[SerializeField] private GameObject drawerRoot;
[SerializeField] private GameObject tablesTabRoot;
[SerializeField] private GameObject eventsTabRoot;
[SerializeField] private bool openOnStart;
[Header("Drawer Buttons")] [SerializeField]
private Button tablesTabButton;
[SerializeField] private Button eventsTabButton;
[SerializeField] private Button closeDrawerButton;
private bool _isOpen;
private void Awake()
{
BindButtons();
_isOpen = openOnStart;
ApplyDrawerState();
ShowTablesTab();
}
public void ToggleDrawer()
{
_isOpen = !_isOpen;
ApplyDrawerState();
}
public void OpenDrawer()
{
_isOpen = true;
ApplyDrawerState();
}
public void CloseDrawer()
{
_isOpen = false;
ApplyDrawerState();
}
public void ShowTablesTab()
{
SetActiveSafe(tablesTabRoot, true);
SetActiveSafe(eventsTabRoot, false);
}
public void ShowEventsTab()
{
SetActiveSafe(tablesTabRoot, false);
SetActiveSafe(eventsTabRoot, true);
}
private void BindButtons()
{
if (tablesTabButton != null)
{
tablesTabButton.onClick.RemoveAllListeners();
tablesTabButton.onClick.AddListener(ShowTablesTab);
}
if (eventsTabButton != null)
{
eventsTabButton.onClick.RemoveAllListeners();
eventsTabButton.onClick.AddListener(ShowEventsTab);
}
if (closeDrawerButton != null)
{
closeDrawerButton.onClick.RemoveAllListeners();
closeDrawerButton.onClick.AddListener(CloseDrawer);
}
}
private void ApplyDrawerState()
{
SetActiveSafe(drawerRoot, _isOpen);
}
private static void SetActiveSafe(GameObject target, bool active)
{
if (target != null)
{
target.SetActive(active);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 58e1bc0dfbbf46b7b64e6d6d74fd5f24
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
using TMPro;
using UnityEngine;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public class SimulationProcessHeaderView : MonoBehaviour
{
[SerializeField] private TMP_Text currentStepText;
[SerializeField] private TMP_Text currentEventText;
public void RenderStep(SimulationState state)
{
if (state == null)
{
SetText(currentStepText, "步骤:N/A");
return;
}
SetText(currentStepText, $"步骤:{TranslationStepDisplay.ToChinese(state.CurrentStep)}");
}
public void SetEvent(string message)
{
SetText(currentEventText, message);
}
private static void SetText(TMP_Text target, string text)
{
if (target != null)
{
target.text = text;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71f3b95f31a74ef98ffd50e2f6435c20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+48
View File
@@ -0,0 +1,48 @@
using TMPro;
using UnityEngine;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public class SimulationTablesView : MonoBehaviour
{
[SerializeField] private TMP_Text tlbText;
[SerializeField] private TMP_Text pageTableText;
[SerializeField] private TMP_Text fifoText;
public void ShowNotInitialized()
{
SetText(tlbText, "TLB:N/A");
SetText(pageTableText, "页表:N/A");
SetText(fifoText, "FIFO:N/A");
}
public void Render(TranslatorEngine engine)
{
if (engine == null)
{
ShowNotInitialized();
return;
}
var vpnBits = engine.VaBits - engine.OffsetBits;
var pfnBits = SimulationUIFormatter.ComputeBitsForCount(engine.PhysicalMemoryManager.FrameCount);
SetText(
tlbText,
SimulationUIFormatter.FormatTlbTable(
engine.TlbCache.GetEntriesMostRecentFirst(),
vpnBits,
pfnBits));
SetText(pageTableText, SimulationUIFormatter.FormatCurrentPageTableEntry(engine.State, engine.PageTable, engine.LastEviction));
SetText(fifoText, SimulationUIFormatter.FormatFifoQueue(engine.PhysicalMemoryManager.GetFifoVpnOrder()));
}
private static void SetText(TMP_Text target, string value)
{
if (target != null)
{
target.text = value;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 68f3ab1453614176a0f66589d29a55dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace VMdemo.UI
{
public class SimulationTimelineView : MonoBehaviour
{
[SerializeField] private RectTransform logContainer;
[SerializeField] private TMP_Text logItemPrefab;
[SerializeField] private ScrollRect logScrollRect;
[SerializeField] private int maxLogEntries = 200;
private readonly Queue<TMP_Text> _logItems = new Queue<TMP_Text>();
public void Append(string message)
{
if (logContainer == null || logItemPrefab == null)
{
return;
}
var item = Instantiate(logItemPrefab, logContainer);
item.gameObject.SetActive(true);
item.text = message;
item.ForceMeshUpdate();
var autoSize = item.GetComponent<TimelineLogItemAutoSize>();
if (autoSize != null)
{
autoSize.RefreshLayout();
}
else
{
// Fallback: adapt this text object's rect to preferred content size.
var preferred = item.GetPreferredValues(item.text, logContainer.rect.width, 0f);
var rect = item.rectTransform;
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, preferred.x);
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, preferred.y);
}
_logItems.Enqueue(item);
while (_logItems.Count > Mathf.Max(1, maxLogEntries))
{
var old = _logItems.Dequeue();
if (old != null)
{
Destroy(old.gameObject);
}
}
LayoutRebuilder.ForceRebuildLayoutImmediate(logContainer);
Canvas.ForceUpdateCanvases();
if (logScrollRect != null)
{
logScrollRect.verticalNormalizedPosition = 0f;
}
}
public void Clear()
{
while (_logItems.Count > 0)
{
var item = _logItems.Dequeue();
if (item != null)
{
Destroy(item.gameObject);
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c65bcc42d5bf440ca80a93cc5a2f1fc1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+98
View File
@@ -0,0 +1,98 @@
using TMPro;
using UnityEngine;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public class SimulationTraceView : MonoBehaviour
{
[SerializeField] private TMP_Text splitTraceText;
[SerializeField] private TMP_Text pageTableTraceText;
[SerializeField] private TMP_Text decisionReasonText;
[SerializeField] private TMP_Text pathSummaryText;
private void Awake()
{
EnsureRichTextEnabled(splitTraceText);
}
public void ShowNotInitialized()
{
SetText(splitTraceText, "地址拆分可视化:N/A");
SetText(pageTableTraceText, "多级页表查询可视化:N/A");
SetText(decisionReasonText, "分支原因:N/A");
SetText(pathSummaryText, "路径摘要:N/A");
}
public void Render(TranslatorEngine engine)
{
if (engine == null)
{
ShowNotInitialized();
return;
}
SetText(
splitTraceText,
SimulationUIFormatter.FormatAddressSplitTrace(
engine.State,
engine.VaBits,
engine.OffsetBits,
engine.L1Bits,
engine.L2Bits));
SetText(
pageTableTraceText,
SimulationUIFormatter.FormatPageTableWalkTrace(engine.State, engine.PageTable));
SetText(
decisionReasonText,
string.IsNullOrEmpty(engine.State.LastDecisionReason)
? "分支原因:暂无"
: $"分支原因:{engine.State.LastDecisionReason}");
SetText(pathSummaryText, BuildPathSummary(engine.State));
}
private static string BuildPathSummary(SimulationState state)
{
if (state == null)
{
return "路径摘要:N/A";
}
if (state.IsTlbHit)
{
return "路径摘要:TLB命中 -> 直接合成PA";
}
if (state.IsPageTableHit)
{
return "路径摘要:TLB未命中 -> 页表命中 -> 合成PA";
}
if (state.IsPageFault)
{
return "路径摘要:TLB未命中 -> 页表未命中 -> 缺页处理 -> 合成PA";
}
return "路径摘要:进行中";
}
private static void SetText(TMP_Text target, string value)
{
if (target != null)
{
target.text = value;
}
}
private static void EnsureRichTextEnabled(TMP_Text target)
{
if (target != null)
{
target.richText = true;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5f6f28922eb241649a4d14b8b6cc6bf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+458
View File
@@ -0,0 +1,458 @@
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using VMdemo.Core;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public class SimulationUIController : MonoBehaviour
{
[Header("Sub Views")]
[SerializeField] private SimulationConfigInputView configInputView;
[SerializeField] private SimulationControlView controlView;
[SerializeField] private SimulationDashboardView dashboardView;
[SerializeField] private SimulationTablesView tablesView;
[SerializeField] private SimulationTraceView traceView;
[SerializeField] private SimulationTimelineView detailTimelineView;
[SerializeField] private SimulationProcessHeaderView processHeaderView;
[SerializeField] private StepFlowAnimator flowAnimator;
[SerializeField] private SimulationDetailDrawerView detailDrawerView;
[Header("Playback")]
[SerializeField] private float playIntervalSeconds = 0.3f;
private TranslatorEngine _engine;
private Coroutine _playRoutine;
private SimulationConfig _activeConfig;
private void Awake()
{
BindControlEvents();
controlView?.SetPlaying(false);
InitializeEngineIfNeeded();
RefreshUI(accessCompleted: false);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
HandleStepHotkey();
}
}
private void OnDisable()
{
StopPlayRoutine();
}
public void OpenConfigPanel()
{
if (configInputView == null)
{
return;
}
configInputView.PopulateFromConfig(_activeConfig ?? new SimulationConfig());
configInputView.SetPanelVisible(true);
dashboardView?.SetError(string.Empty);
}
public void ToggleDetailDrawer()
{
detailDrawerView?.ToggleDrawer();
}
public void OpenDetailDrawer()
{
detailDrawerView?.OpenDrawer();
}
public void CloseDetailDrawer()
{
detailDrawerView?.CloseDrawer();
}
public void ShowTablesTab()
{
detailDrawerView?.ShowTablesTab();
detailDrawerView?.OpenDrawer();
}
public void ShowEventsTab()
{
detailDrawerView?.ShowEventsTab();
detailDrawerView?.OpenDrawer();
}
public void CloseConfigPanel()
{
configInputView?.SetPanelVisible(false);
}
public void SaveConfigAndClosePanel()
{
if (configInputView == null)
{
dashboardView?.SetError("缺少 SimulationConfigInputView 引用。");
return;
}
if (!configInputView.TryBuildConfig(out var config, out var error))
{
dashboardView?.SetError(error);
return;
}
dashboardView?.SetError(string.Empty);
if (!IsSameConfig(_activeConfig, config))
{
ApplyConfig(config, appendInitLog: true, clearTimeline: true);
}
else
{
AppendEvent("配置未变更", "配置未变更,保留当前引擎。");
}
configInputView.SetPanelVisible(false);
}
public void CancelConfigAndClosePanel()
{
if (configInputView == null)
{
return;
}
configInputView.PopulateFromConfig(_activeConfig ?? new SimulationConfig());
configInputView.SetPanelVisible(false);
dashboardView?.SetError(string.Empty);
AppendEvent("已取消配置", "已取消配置面板修改,保留当前引擎。");
}
private void BindControlEvents()
{
if (controlView == null)
{
return;
}
controlView.StepClicked += HandleStepClicked;
controlView.PlayClicked += HandlePlayClicked;
controlView.PauseClicked += HandlePauseClicked;
controlView.ResetClicked += HandleResetClicked;
}
private void InitializeEngineIfNeeded()
{
if (_engine != null)
{
return;
}
string initialError = string.Empty;
if (configInputView != null)
{
if (configInputView.TryBuildConfig(out var viewConfig, out var viewError))
{
dashboardView?.SetError(string.Empty);
ApplyConfig(viewConfig, appendInitLog: true, clearTimeline: true);
return;
}
initialError = viewError;
}
var fallback = new SimulationConfig();
if (configInputView != null)
{
configInputView.PopulateFromConfig(fallback);
}
ApplyConfig(fallback, appendInitLog: true, clearTimeline: true);
if (!string.IsNullOrEmpty(initialError))
{
dashboardView?.SetError(initialError);
}
}
private void ApplyConfig(SimulationConfig config, bool appendInitLog, bool clearTimeline)
{
if (config == null)
{
config = new SimulationConfig();
}
config.ApplyFixedCoreSpec();
StopPlayRoutine();
controlView?.SetPlaying(false);
_activeConfig = CloneConfig(config);
_engine = new TranslatorEngine(_activeConfig);
if (clearTimeline)
{
detailTimelineView?.Clear();
}
if (appendInitLog)
{
AppendEvent("模拟已就绪", "已按新配置初始化模拟。");
}
RefreshUI(accessCompleted: false);
}
private IEnumerator PlayRoutine()
{
while (_engine != null)
{
if (_activeConfig != null &&
_engine.Stats.TotalAccess >= _activeConfig.accessCount &&
_engine.State.CurrentStep == TranslationStep.GenerateVA)
{
AppendEvent("连续播放结束", "连续播放达到访问次数上限。");
break;
}
var accessCompleted = _engine.StepOnce();
RefreshUI(accessCompleted);
if (accessCompleted)
{
var state = _engine.State;
AppendEvent(
BuildAccessCompactMessage(state),
$"访问#{state.CurrentRound} 虚拟地址VA={SimulationUIFormatter.FormatAddressHex(state.CurrentVirtualAddress, _engine.VaBits)} -> " +
$"物理地址PA={SimulationUIFormatter.FormatAddressHex(state.CurrentPhysicalAddress, _engine.VaBits)} " +
$"TLB命中={state.IsTlbHit} 页表命中={state.IsPageTableHit} 缺页={state.IsPageFault} 开销={state.CurrentCost}");
}
yield return new WaitForSeconds(playIntervalSeconds);
}
_playRoutine = null;
controlView?.SetPlaying(false);
}
private void RefreshUI(bool accessCompleted)
{
if (_engine == null)
{
dashboardView?.ShowNotInitialized();
tablesView?.ShowNotInitialized();
traceView?.ShowNotInitialized();
flowAnimator?.ResetVisual();
processHeaderView?.RenderStep(null);
return;
}
dashboardView?.Render(_engine);
tablesView?.Render(_engine);
traceView?.Render(_engine);
flowAnimator?.AnimateStep(_engine.State.CurrentStep, _engine.State.IsPageFault, accessCompleted);
processHeaderView?.RenderStep(_engine.State);
}
private void AppendStepLog(bool accessCompleted)
{
if (_engine == null)
{
return;
}
var state = _engine.State;
if (accessCompleted)
{
AppendEvent(
$"访问#{state.CurrentRound} 完成({BuildResultFlags(state)})",
$"访问#{state.CurrentRound} 完成 | " +
$"TLB命中={state.IsTlbHit} 页表命中={state.IsPageTableHit} 缺页={state.IsPageFault} 开销={state.CurrentCost}\n" +
$"分支原因:{state.LastDecisionReason}");
return;
}
var stepZh = TranslationStepDisplay.ToChinese(state.CurrentStep);
AppendEvent($"步骤:{stepZh}", $"步骤 -> {stepZh}\n分支原因:{state.LastDecisionReason}");
}
private void AppendEvent(string compact, string detail)
{
detailTimelineView?.Append(detail);
processHeaderView?.SetEvent(compact);
}
private void StopPlayRoutine()
{
if (_playRoutine == null)
{
return;
}
StopCoroutine(_playRoutine);
_playRoutine = null;
}
private void HandleStepClicked()
{
InitializeEngineIfNeeded();
if (_engine == null)
{
return;
}
var accessCompleted = _engine.StepOnce();
RefreshUI(accessCompleted);
AppendStepLog(accessCompleted);
}
private void HandlePlayClicked()
{
InitializeEngineIfNeeded();
if (_engine == null || _playRoutine != null)
{
return;
}
_playRoutine = StartCoroutine(PlayRoutine());
controlView?.SetPlaying(true);
AppendEvent("开始连续播放", "已开始连续播放。");
}
private void HandleStepHotkey()
{
if (IsStepHotkeyBlocked())
{
return;
}
HandleStepClicked();
}
private void HandlePauseClicked()
{
StopPlayRoutine();
controlView?.SetPlaying(false);
AppendEvent("暂停播放", "已暂停连续播放。");
}
private void HandleResetClicked()
{
StopPlayRoutine();
if (_engine != null)
{
_engine.Reset();
}
detailTimelineView?.Clear();
RefreshUI(accessCompleted: false);
controlView?.SetPlaying(false);
AppendEvent("已重置", "模拟已重置。");
}
private bool IsStepHotkeyBlocked()
{
if (_playRoutine != null)
{
return true;
}
if (configInputView != null && configInputView.IsPanelVisible)
{
return true;
}
return IsTypingInInputField();
}
private static bool IsTypingInInputField()
{
var eventSystem = EventSystem.current;
if (eventSystem == null)
{
return false;
}
var selected = eventSystem.currentSelectedGameObject;
if (selected == null)
{
return false;
}
return selected.GetComponent<TMP_InputField>() != null ||
selected.GetComponent<InputField>() != null;
}
private static string BuildAccessCompactMessage(SimulationState state)
{
var flagZh = state.IsPageFault ? "缺页" : (state.IsTlbHit ? "TLB命中" : (state.IsPageTableHit ? "页表命中" : "未命中"));
return $"访问#{state.CurrentRound} {flagZh}";
}
private static string BuildResultFlags(SimulationState state)
{
if (state.IsPageFault)
{
return "缺页";
}
if (state.IsTlbHit)
{
return "TLB命中";
}
if (state.IsPageTableHit)
{
return "页表命中";
}
return "未知";
}
private static SimulationConfig CloneConfig(SimulationConfig source)
{
if (source == null)
{
return new SimulationConfig();
}
return new SimulationConfig
{
machineBits = source.machineBits,
vaBits = source.vaBits,
pageSizeKB = source.pageSizeKB,
physicalMemoryMB = source.physicalMemoryMB,
tlbEntries = source.tlbEntries,
accessCount = source.accessCount,
pageFaultPenalty = source.pageFaultPenalty,
addressGenerationMode = source.addressGenerationMode,
arrayLengthBytes = source.arrayLengthBytes,
arrayElementBytes = source.arrayElementBytes,
arrayBaseAddress = source.arrayBaseAddress
};
}
private static bool IsSameConfig(SimulationConfig a, SimulationConfig b)
{
if (a == null || b == null)
{
return false;
}
return a.machineBits == b.machineBits &&
a.vaBits == b.vaBits &&
a.pageSizeKB == b.pageSizeKB &&
a.physicalMemoryMB == b.physicalMemoryMB &&
a.tlbEntries == b.tlbEntries &&
a.accessCount == b.accessCount &&
a.pageFaultPenalty == b.pageFaultPenalty &&
a.addressGenerationMode == b.addressGenerationMode &&
a.arrayLengthBytes == b.arrayLengthBytes &&
a.arrayElementBytes == b.arrayElementBytes &&
a.arrayBaseAddress == b.arrayBaseAddress;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac9c8cdc89fb6c14bbd38774897a7e26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+324
View File
@@ -0,0 +1,324 @@
using System.Collections.Generic;
using System.Text;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public static class SimulationUIFormatter
{
private const string ColorVpn = "#4FC3F7";
private const string ColorL1 = "#FFB74D";
private const string ColorL2 = "#81C784";
private const string ColorOffset = "#B0BEC5";
public static string FormatStateSummary(
SimulationState state,
int machineBits,
int vaBits,
int pageSizeKB,
int physicalMemoryMB,
int tlbEntries)
{
var va = FormatAddressHex(state.CurrentVirtualAddress, vaBits);
var pa = FormatAddressHex(state.CurrentPhysicalAddress, vaBits);
var stepText = TranslationStepDisplay.ToChinese(state.CurrentStep);
return
$"系统规格:{machineBits}位机\n" +
$"虚拟地址有效位:{vaBits}位\n" +
$"页大小:{pageSizeKB}KB\n" +
$"物理内存:{physicalMemoryMB}MB\n" +
$"TLB:{tlbEntries}条全相联\n";
}
public static string FormatStats(StatsCollector stats)
{
return
$"总访问={stats.TotalAccess}\n" +
$"TLB命中率={stats.TlbHitRate:P2}\n" +
$"页表命中率={stats.PageTableHitRate:P2}\n" +
$"缺页次数={stats.PageFault}\n" +
$"平均开销={stats.AvgCost:F2}";
}
public static string FormatTlbTable(
IReadOnlyList<TlbEntry> entries,
int vpnBits,
int pfnBits)
{
if (entries == null || entries.Count == 0)
{
return "TLB:空";
}
var sb = new StringBuilder();
sb.AppendLine("TLB(最近使用 -> 最久未用)");
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
sb.Append("#").Append(i + 1)
.Append("VPN=").Append(FormatBitWidthHex(entry.Vpn, vpnBits))
.Append(" -> PFN=").Append(FormatBitWidthHex(entry.Pfn, pfnBits))
.AppendLine();
}
return sb.ToString().TrimEnd();
}
public static string FormatCurrentPageTableEntry(
SimulationState state,
TwoLevelPageTable pageTable,
EvictionInfo eviction)
{
if (state == null || pageTable == null)
{
return "页表:N/A";
}
var sb = new StringBuilder();
sb.Append("当前页表项:");
if (pageTable.TryGetEntry(state.CurrentAddressParts.L1Index, state.CurrentAddressParts.L2Index,
out var entry))
{
sb.Append("存在=").Append(entry.Present)
.Append(" 物理帧号PFN=").Append(FormatHex(entry.Pfn))
.Append(" 脏位=").Append(entry.Dirty);
}
else
{
sb.Append("(缺失)");
}
sb.AppendLine();
if (eviction.HasEvicted)
{
sb.Append("最近淘汰:虚拟页号VPN=").Append(FormatHex(eviction.EvictedVpn))
.Append(" 物理帧号PFN=").Append(FormatHex(eviction.EvictedPfn));
}
else
{
sb.Append("最近淘汰:无");
}
return sb.ToString();
}
public static string FormatFifoQueue(IReadOnlyList<ulong> fifoOrder)
{
if (fifoOrder == null || fifoOrder.Count == 0)
{
return "FIFO:空";
}
var sb = new StringBuilder("FIFO(最早进入 -> 最新进入):");
for (var i = 0; i < fifoOrder.Count; i++)
{
if (i > 0)
{
sb.Append(" -> ");
}
sb.Append(FormatHex(fifoOrder[i]));
}
return sb.ToString();
}
public static string FormatAddressHex(ulong value, int vaBits)
{
return FormatBitWidthHex(value, vaBits);
}
private static string FormatHex(ulong value)
{
return $"0x{value:X}";
}
public static string FormatBitWidthHex(ulong value, int bits)
{
var digits = bits switch
{
8 => 2,
16 => 4,
24 => 6,
32 => 8,
48 => 12,
64 => 16,
_ => bits > 0 ? (bits + 3) / 4 : 1
};
return $"0x{value.ToString($"X{digits}")}";
}
public static int ComputeBitsForCount(ulong count)
{
if (count <= 1UL)
{
return 1;
}
var maxValue = count - 1UL;
var bits = 0;
while (maxValue > 0UL)
{
bits++;
maxValue >>= 1;
}
return bits;
}
public static string FormatAddressSplitTrace(
SimulationState state,
int vaBits,
int offsetBits,
int l1Bits,
int l2Bits)
{
if (state == null)
{
return "地址拆分可视化:N/A";
}
var vpnBits = vaBits - offsetBits;
var vaHex = FormatAddressHex(state.CurrentVirtualAddress, vaBits);
var vaBin = FormatBinary(state.CurrentVirtualAddress, vaBits);
var vaBinColored = FormatVaBinaryWithFieldColors(state.CurrentVirtualAddress, vaBits, l1Bits, l2Bits, offsetBits);
var vpnBin = FormatBinary(state.CurrentAddressParts.Vpn, vpnBits);
var l1Bin = FormatBinary(state.CurrentAddressParts.L1Index, l1Bits);
var l2Bin = FormatBinary(state.CurrentAddressParts.L2Index, l2Bits);
var offBin = FormatBinary(state.CurrentAddressParts.Offset, offsetBits);
var sb = new StringBuilder();
sb.AppendLine("地址拆分可视化");
sb.AppendLine($"{vaHex})");
sb.AppendLine($"{vaBinColored}");
sb.AppendLine(
$"{WrapColor("VPN", ColorVpn)} = {WrapColor("L1+L2", ColorVpn)}," +
$"{WrapColor("L1", ColorL1)},{WrapColor("L2", ColorL2)},{WrapColor("Offset", ColorOffset)}");
sb.AppendLine($"VPN({vpnBits}) = L1({l1Bits}) + L2({l2Bits}) | Offset({offsetBits})");
sb.AppendLine($"VPN:{FormatBitWidthHex(state.CurrentAddressParts.Vpn, vpnBits)} ({WrapColor(vpnBin, ColorVpn)})");
sb.AppendLine($"L1 索引:{FormatBitWidthHex(state.CurrentAddressParts.L1Index, l1Bits)} ({WrapColor(l1Bin, ColorL1)})");
sb.AppendLine($"L2 索引:{FormatBitWidthHex(state.CurrentAddressParts.L2Index, l2Bits)} ({WrapColor(l2Bin, ColorL2)})");
sb.Append($"Offset:{FormatBitWidthHex(state.CurrentAddressParts.Offset, offsetBits)} ({WrapColor(offBin, ColorOffset)})");
return sb.ToString();
}
public static string FormatPageTableWalkTrace(SimulationState state, TwoLevelPageTable pageTable)
{
if (state == null || pageTable == null)
{
return "页表访问路径:N/A";
}
var probe = pageTable.Probe(state.CurrentAddressParts.L1Index, state.CurrentAddressParts.L2Index);
var sb = new StringBuilder();
sb.AppendLine("多级页表查询可视化");
sb.AppendLine(
$"1) 先按 VPN 拆分:L1={state.CurrentAddressParts.L1Index}, L2={state.CurrentAddressParts.L2Index}");
switch (probe.Status)
{
case PageTableProbeStatus.MissingL1Table:
sb.Append("2) L1 未命中:对应二级页表不存在。");
break;
case PageTableProbeStatus.MissingL2Entry:
sb.Append("2) L1 命中,L2 未命中:页表项不存在。");
break;
case PageTableProbeStatus.NotPresentEntry:
sb.Append($"2) L1/L2 命中,但 present=false(PFN={FormatHex(probe.Entry.Pfn)})。");
break;
case PageTableProbeStatus.PresentEntry:
sb.Append($"2) L1/L2 命中,present=true,得到 PFN={FormatHex(probe.Entry.Pfn)}。");
break;
}
return sb.ToString();
}
private static string FormatBinary(ulong value, int bits)
{
if (bits <= 0)
{
return "0";
}
var sb = new StringBuilder(bits + (bits / 4));
for (var i = bits - 1; i >= 0; i--)
{
var bit = (value >> i) & 1UL;
sb.Append(bit == 1UL ? '1' : '0');
if (i > 0 && i % 4 == 0)
{
sb.Append('_');
}
}
return sb.ToString();
}
private static string FormatVaBinaryWithFieldColors(
ulong value,
int vaBits,
int l1Bits,
int l2Bits,
int offsetBits)
{
if (vaBits <= 0)
{
return "0";
}
var sb = new StringBuilder(vaBits * 10);
string currentColor = null;
for (var i = vaBits - 1; i >= 0; i--)
{
var bitColor = ResolveBitColor(i, l1Bits, l2Bits, offsetBits);
if (currentColor != bitColor)
{
if (currentColor != null)
{
sb.Append("</color>");
}
sb.Append("<color=").Append(bitColor).Append(">");
currentColor = bitColor;
}
var bit = (value >> i) & 1UL;
sb.Append(bit == 1UL ? '1' : '0');
if (i > 0 && i % 4 == 0)
{
sb.Append('_');
}
}
if (currentColor != null)
{
sb.Append("</color>");
}
return sb.ToString();
}
private static string ResolveBitColor(int bitIndex, int l1Bits, int l2Bits, int offsetBits)
{
var l1Start = offsetBits + l2Bits;
if (bitIndex >= l1Start)
{
return ColorL1;
}
if (bitIndex >= offsetBits)
{
return ColorL2;
}
return ColorOffset;
}
private static string WrapColor(string text, string colorHex)
{
return $"<color={colorHex}>{text}</color>";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d62c9fa502173a542a04294863667636
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+180
View File
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using VMdemo.Simulation;
namespace VMdemo.UI
{
public class StepFlowAnimator : MonoBehaviour
{
[Serializable]
public class StepNodeView
{
public TranslationStep step;
public RectTransform target;
public CanvasGroup canvasGroup;
public Image background;
}
[Header("Step Nodes")] [SerializeField]
private List<StepNodeView> stepNodes = new List<StepNodeView>();
[Header("Colors")] [SerializeField] private Color idleColor = new Color(0.18f, 0.22f, 0.28f, 1f);
[SerializeField] private Color activeColor = new Color(0.19f, 0.62f, 0.98f, 1f);
[SerializeField] private Color faultColor = new Color(0.97f, 0.31f, 0.27f, 1f);
[Header("Animation")] [SerializeField] private float highlightDuration = 0.2f;
[SerializeField] private float normalScale = 1f;
[SerializeField] private float activeScale = 1.08f;
[SerializeField] private float pulseScale = 1.04f;
[SerializeField] private RectTransform accessPulseTarget;
private readonly Dictionary<TranslationStep, StepNodeView> _nodeMap =
new Dictionary<TranslationStep, StepNodeView>();
private TranslationStep? _lastStep;
private void Awake()
{
BuildNodeMap();
ResetVisual();
}
public void ResetVisual()
{
DOTween.Kill(this);
_lastStep = null;
foreach (var node in stepNodes)
{
if (node == null)
{
continue;
}
if (node.target != null)
{
node.target.localScale = Vector3.one * normalScale;
}
if (node.canvasGroup != null)
{
node.canvasGroup.alpha = 0.72f;
}
if (node.background != null)
{
node.background.color = idleColor;
}
}
if (accessPulseTarget != null)
{
accessPulseTarget.localScale = Vector3.one;
}
}
public void AnimateStep(TranslationStep currentStep, bool isPageFault, bool accessCompleted)
{
BuildNodeMap();
if (_lastStep.HasValue && _nodeMap.TryGetValue(_lastStep.Value, out var lastNode))
{
AnimateToIdle(lastNode);
}
if (_nodeMap.TryGetValue(currentStep, out var currentNode))
{
AnimateToActive(currentNode, isPageFault && currentStep == TranslationStep.HandlePageFault);
}
if (accessCompleted)
{
AnimateAccessCompletedPulse();
}
_lastStep = currentStep;
}
private void BuildNodeMap()
{
_nodeMap.Clear();
foreach (var node in stepNodes)
{
if (node == null)
{
continue;
}
if (!_nodeMap.ContainsKey(node.step))
{
_nodeMap.Add(node.step, node);
}
}
}
private void AnimateToIdle(StepNodeView node)
{
if (node.target != null)
{
node.target.DOKill();
node.target.DOScale(normalScale, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this);
}
if (node.canvasGroup != null)
{
node.canvasGroup.DOKill();
node.canvasGroup.DOFade(0.72f, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this);
}
if (node.background != null)
{
node.background.DOKill();
node.background.DOColor(idleColor, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this);
}
}
private void AnimateToActive(StepNodeView node, bool emphasizeFault)
{
if (node.target != null)
{
node.target.DOKill();
node.target.DOScale(activeScale, highlightDuration).SetEase(Ease.OutBack).SetTarget(this);
}
if (node.canvasGroup != null)
{
node.canvasGroup.DOKill();
node.canvasGroup.DOFade(1f, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this);
}
if (node.background != null)
{
node.background.DOKill();
var targetColor = emphasizeFault ? faultColor : activeColor;
node.background.DOColor(targetColor, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this);
}
if (emphasizeFault && node.target != null)
{
node.target.DOPunchScale(new Vector3(0.07f, 0.07f, 0f), 0.28f, 6, 0.75f).SetTarget(this);
}
}
private void AnimateAccessCompletedPulse()
{
if (accessPulseTarget == null)
{
return;
}
accessPulseTarget.DOKill();
accessPulseTarget
.DOScale(pulseScale, 0.12f)
.SetEase(Ease.OutQuad)
.SetLoops(2, LoopType.Yoyo)
.SetTarget(this);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f36cd8ac00908594fb49c431d072c1bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace VMdemo.UI
{
[RequireComponent(typeof(RectTransform))]
public class TimelineLogItemAutoSize : MonoBehaviour
{
[SerializeField] private TMP_Text textComponent;
[SerializeField] private float horizontalPadding = 20f;
[SerializeField] private float verticalPadding = 12f;
[SerializeField] private bool clampToParentWidth = true;
[SerializeField] private float minWidth = 80f;
[SerializeField] private float minHeight = 30f;
public void RefreshLayout()
{
var text = textComponent != null ? textComponent : GetComponent<TMP_Text>();
var root = (RectTransform)transform;
if (text == null)
{
return;
}
float maxWidth = float.PositiveInfinity;
if (clampToParentWidth && root.parent is RectTransform parentRect)
{
maxWidth = Mathf.Max(1f, parentRect.rect.width - horizontalPadding);
}
var preferred = text.GetPreferredValues(text.text, maxWidth, 0f);
var width = Mathf.Max(minWidth, preferred.x + horizontalPadding);
var height = Mathf.Max(minHeight, preferred.y + verticalPadding);
if (clampToParentWidth && root.parent is RectTransform p)
{
width = Mathf.Min(width, Mathf.Max(minWidth, p.rect.width));
}
root.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
root.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
var layoutElement = GetComponent<LayoutElement>();
if (layoutElement != null)
{
layoutElement.preferredWidth = width;
layoutElement.preferredHeight = height;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d594815f47f84f758ecf3e198de8d8de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using VMdemo.Simulation;
namespace VMdemo.UI
{
public static class TranslationStepDisplay
{
public static string ToChinese(TranslationStep step)
{
return step switch
{
TranslationStep.GenerateVA => "生成虚拟地址",
TranslationStep.SplitVA => "拆分虚拟地址",
TranslationStep.LookupTLB => "查询 TLB",
TranslationStep.LookupPageTable => "查询页表",
TranslationStep.HandlePageFault => "处理缺页",
TranslationStep.ComposePA => "合成物理地址",
TranslationStep.Finalize => "完成收尾",
_ => step.ToString()
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 61f58553676e41d68ef5818d93ba09dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+16 -2
View File
@@ -1,3 +1,17 @@
{
"name": "VMdemo"
}
"name": "VMdemo",
"rootNamespace": "",
"references": [
"GUID:6055be8ebefd69e48b49212b09b47b2f",
"GUID:b1a18c052caf77a44a8bbb8a5e4acd11"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}