459 lines
14 KiB
C#
459 lines
14 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|