Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ public partial class App : Application
public bool IsHostAppReady => _hostApp != null;

/// <summary>
/// Optional async runner invoked from <see cref="ViewModels.MainViewModel.SetDefaultSystemSelectionAsync"/>
/// when an automated startup has been requested (e.g. via URL query parameters in the Browser host).
/// Running automation from there guarantees the Avalonia view tree has been loaded and rendered
/// at least once, so subsequent <c>InvalidateVisual</c> calls actually trigger paint.
/// Optional async runner invoked from <see cref="ViewModels.MainViewModel.InitializeAsync"/>
/// when an automated startup has been requested (e.g. via URL query parameters in the Browser host).
/// Running automation from there guarantees the Avalonia view tree has been loaded and rendered
/// at least once, so subsequent <c>InvalidateVisual</c> calls actually trigger paint.
/// </summary>
public Func<IHostApp, Task>? AutomatedStartupRunner => _automatedStartupRunner;

Expand Down Expand Up @@ -119,7 +119,7 @@ public partial class App : Application
/// <param name="deleteScript">Optional callback to remove a script by file name (browser: from localStorage).</param>
/// <param name="loadExamples">Optional callback to seed bundled example scripts into the host's script storage.</param>
/// <param name="automatedStartupRunner">
/// Optional automated-startup delegate invoked from <see cref="ViewModels.MainViewModel.SetDefaultSystemSelectionAsync"/>
/// Optional automated-startup delegate invoked from <see cref="ViewModels.MainViewModel.InitializeAsync"/>
/// after the view tree has been loaded. When non-null, the UI's default system selection is
/// suppressed and the runner is invoked instead — used by the Browser host for URL-driven
/// automation, by Desktop for CLI-driven automation, and as a no-op (<c>_ =&gt; Task.CompletedTask</c>)
Expand Down Expand Up @@ -456,11 +456,25 @@ private void InitializeHostApp()
_scriptingConfig);

// Wire Lua scripting engine (NoScriptingEngine used when null, e.g. in WASM)
_hostApp.SetScriptingEngine(_scriptingEngine ?? new NoScriptingEngine());

// Signal waiters (e.g. automated startup on a background thread) that HostApp is ready.
// TrySetResult guarantees all writes above are visible to awaiters before they resume.
s_hostAppReady.TrySetResult(_hostApp);
_hostApp.SetScriptingEngine(_scriptingEngine ?? new NoScriptingEngine());

// Normal startup should have a selected system before the first view layout. Otherwise
// bindings that size the emulator display evaluate with no selected system and briefly
// fall back to 320x200 before MainView.Loaded selects the default system. Automated
// startup still runs after the view is loaded because it may need a rendered tree.
if (_automatedStartupRunner == null)
{
_logger.LogInformation(
"Selecting default system '{DefaultEmulator}' before Avalonia view creation",
_emulatorConfig.DefaultEmulator);
#pragma warning disable VSTHRD002
_hostApp.SelectSystem(_emulatorConfig.DefaultEmulator).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
}

// Signal waiters (e.g. automated startup on a background thread) that HostApp is ready.
// TrySetResult guarantees all writes above are visible to awaiters before they resume.
s_hostAppReady.TrySetResult(_hostApp);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,11 +872,9 @@ public MainViewModel(
.Select(config => GetAudioToolTip(config))
.ToProperty(this, x => x.AudioTooltip);

// A host-config update can rebuild the selected system's temporary instance and thereby
// change its screen dimensions (e.g. ROMs downloaded during automated startup make the C64
// buildable, so CurrentSystemScreenInfo goes from the default fallback to the real size).
// The system/variant identity does not change in that case, so refresh the emulator display
// size here too — otherwise the display container keeps the default 320x200 size.
// A host-config update can rebuild the selected system's temporary instance or change the
// config-derived pre-build screen dimensions. The system/variant identity does not change in
// that case, so refresh the emulator display size here too.
_hostApp
.WhenAnyValue(x => x.CurrentHostSystemConfig)
.Subscribe(_ =>
Expand Down Expand Up @@ -1245,8 +1243,11 @@ private async Task SetDefaultSystemSelectionAsync()
return;
}

_logger.LogInformation($"Setting default system '{_emulatorConfig.DefaultEmulator}' during MainViewModel initialization");
await HostApp.SelectSystem(_emulatorConfig.DefaultEmulator);
if (HostApp.SelectedSystemName != _emulatorConfig.DefaultEmulator)
{
_logger.LogInformation($"Setting default system '{_emulatorConfig.DefaultEmulator}' during MainViewModel initialization");
await HostApp.SelectSystem(_emulatorConfig.DefaultEmulator);
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Highbyte.DotNet6502.Systems.Commodore64.Cartridge.SwiftLink;
using Highbyte.DotNet6502.Systems.Commodore64.Transport;
using Highbyte.DotNet6502.Systems.Commodore64.Models;
using Highbyte.DotNet6502.Systems.Commodore64.Video;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -65,6 +66,14 @@ public virtual IEnumerable<string> GetUserContentDirectories()
public Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig)
=> Task.FromResult(C64ModelInventory.C64Models.Keys.ToList());

public IScreen? GetScreenInfo(string configurationVariant, ISystemConfig systemConfig)
{
if (!C64ModelInventory.C64Models.TryGetValue(configurationVariant, out var c64Model))
return null;

return new Vic2Screen(c64Model.Vic2Models.First(), c64Model.CPUFrequencyHz);
}

/// <summary>
/// Creates a fresh host config via the supplied factory and binds it from the matching
/// <see cref="IConfiguration"/> section. Hosts that store config elsewhere (e.g. browser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ protected GenericComputerSystemConfigurerCore(
public virtual Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig)
=> Task.FromResult(((GenericComputerSystemConfig)systemConfig).ExamplePrograms.Keys.ToList());

public IScreen? GetScreenInfo(string configurationVariant, ISystemConfig systemConfig)
{
var genericSystemConfig = (GenericComputerSystemConfig)systemConfig;
var genericComputerConfig = GenericComputerExampleConfigs.GetExampleConfig(configurationVariant, genericSystemConfig);
var screen = genericComputerConfig.Memory.Screen;
const int characterWidth = 8;
const int characterHeight = 8;

return new ScreenInfo(
screen.Cols * characterWidth,
screen.Rows * characterHeight,
(screen.Cols + (2 * screen.BorderCols)) * characterWidth,
(screen.Rows + (2 * screen.BorderRows)) * characterHeight,
genericComputerConfig.ScreenRefreshFrequencyHz);
}

/// <summary>
/// Creates a fresh host config via the supplied factory and binds it from the matching
/// <see cref="IConfiguration"/> section. Hosts that store config elsewhere (e.g. browser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public virtual IEnumerable<string> GetUserContentDirectories()
public virtual Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig)
=> Task.FromResult(new List<string> { VariantNtsc, VariantPal });

public IScreen? GetScreenInfo(string configurationVariant, ISystemConfig systemConfig)
{
var vic20Config = BuildVic20ConfigForVariant(configurationVariant);
return new ScreenInfo(
Vic20Config.DrawableAreaWidth,
Vic20Config.DrawableAreaHeight,
vic20Config.MaxVisibleWidth,
vic20Config.MaxVisibleHeight,
vic20Config.ScreenRefreshFrequencyHz);
}

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Host config binding is limited to known application config models that are rooted by the host application.")]
public virtual Task<IHostSystemConfig> GetNewHostSystemConfig()
{
Expand Down
53 changes: 37 additions & 16 deletions src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,19 @@ public List<string> AllSelectedSystemConfigurationVariants
}
set
{
_allSelectedSystemConfigurationVariants = value;
OnAfterAllSystemConfigurationVariantsChanged();
SetAllSelectedSystemConfigurationVariants(value);
}
}

private SystemRunner? _systemRunner = null;
public SystemRunner? CurrentSystemRunner => _systemRunner;
public ISystem? CurrentRunningSystem => _systemRunner?.System;
public IScreen? CurrentSystemScreenInfo => _systemRunner != null ? _systemRunner?.System.Screen : _selectedSystemTemporary?.Screen;
public IScreen? CurrentSystemScreenInfo => _systemRunner != null
? _systemRunner.System.Screen
: _selectedSystemTemporary?.Screen
?? (_currentHostSystemConfig != null
? _systemList.GetScreenInfo(_selectedSystemName, _selectedSystemConfigurationVariant, _currentHostSystemConfig)
: null);
public EmulatorState EmulatorState
{
get
Expand Down Expand Up @@ -437,19 +441,16 @@ public async Task SelectSystem(string systemName)
_selectedSystemName = systemName;
CurrentHostSystemConfig = await _systemList.GetHostSystemConfig(_selectedSystemName);

OnAfterSelectedSystemChanged();

SelectedSystemChanged?.Invoke(this, EventArgs.Empty);
_scriptingEngine.InvokeEvent("on_system_selected", SelectedSystemName);

// If system changed, make sure any state regarding the system variant is also in sync
if (systemChanged)
{
AllSelectedSystemConfigurationVariants = await _systemList.GetSystemConfigurationVariants(systemName, CurrentHostSystemConfig);
SetAllSelectedSystemConfigurationVariants(
await _systemList.GetSystemConfigurationVariants(systemName, CurrentHostSystemConfig),
notify: false);
if (_allSelectedSystemConfigurationVariants.Count > 0)
{
var selectedSystemConfigurationVariant = _allSelectedSystemConfigurationVariants.First();
await SelectSystemConfigurationVariant(selectedSystemConfigurationVariant);
_selectedSystemConfigurationVariant = _allSelectedSystemConfigurationVariants.First();
await UpdateSelectedSystemTemporary();
}
else
{
Expand All @@ -466,10 +467,26 @@ public async Task SelectSystem(string systemName)
_selectedSystemTemporary = null;
}
}

OnAfterAllSystemConfigurationVariantsChanged();
OnAfterSelectedSystemVariantChanged();
OnAfterSelectedSystemChanged();

SelectedSystemVariantChanged?.Invoke(this, EventArgs.Empty);
SelectedSystemChanged?.Invoke(this, EventArgs.Empty);
_scriptingEngine.InvokeEvent("on_variant_selected", SelectedSystemConfigurationVariant);
_scriptingEngine.InvokeEvent("on_system_selected", SelectedSystemName);
}

public virtual void OnAfterAllSystemConfigurationVariantsChanged() { }

private void SetAllSelectedSystemConfigurationVariants(List<string> variants, bool notify = true)
{
_allSelectedSystemConfigurationVariants = variants;
if (notify)
OnAfterAllSystemConfigurationVariantsChanged();
}

public async Task SelectSystemConfigurationVariant(string configurationVariant)
{
if (EmulatorState != EmulatorState.Uninitialized)
Expand All @@ -479,7 +496,16 @@ public async Task SelectSystemConfigurationVariant(string configurationVariant)
throw new DotNet6502Exception($"System configuration variant not found: {configurationVariant}");

_selectedSystemConfigurationVariant = configurationVariant;
await UpdateSelectedSystemTemporary();

OnAfterSelectedSystemVariantChanged();

SelectedSystemVariantChanged?.Invoke(this, EventArgs.Empty);
_scriptingEngine.InvokeEvent("on_variant_selected", SelectedSystemConfigurationVariant);
}

private async Task UpdateSelectedSystemTemporary()
{
// Pre-create a temporary variable to contain the system if it is valid.
// This is useful if the system has not been started yet, but client requests the system object.
if (CurrentHostSystemConfig.IsValid(out _))
Expand All @@ -490,11 +516,6 @@ public async Task SelectSystemConfigurationVariant(string configurationVariant)
{
_selectedSystemTemporary = null;
}

OnAfterSelectedSystemVariantChanged();

SelectedSystemVariantChanged?.Invoke(this, EventArgs.Empty);
_scriptingEngine.InvokeEvent("on_variant_selected", SelectedSystemConfigurationVariant);
}

public virtual void OnAfterSelectedSystemChanged() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public interface ISystemConfigurer
{
public string SystemName { get; }
public Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig);
public IScreen? GetScreenInfo(string configurationVariant, ISystemConfig systemConfig) => null;
public Task<ISystem> BuildSystem(string configurationVariant, ISystemConfig systemConfig);
public Task<IHostSystemConfig> GetNewHostSystemConfig();
public Task PersistHostSystemConfig(IHostSystemConfig hostSystemConfig);
Expand Down
35 changes: 35 additions & 0 deletions src/libraries/Highbyte.DotNet6502.Systems/ScreenInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Highbyte.DotNet6502.Systems;

/// <summary>
/// Immutable screen geometry for cases where a host knows the selected system/variant but does
/// not yet have a built emulator instance. This lets UI code size the emulator display from
/// configuration-derived dimensions without forcing CPU, memory, ROM, or render setup.
/// </summary>
public sealed class ScreenInfo : IScreen
{
/// <summary>
/// Creates a screen descriptor using drawable and visible pixel dimensions.
/// </summary>
public ScreenInfo(
int drawableAreaWidth,
int drawableAreaHeight,
int visibleWidth,
int visibleHeight,
float refreshFrequencyHz)
{
DrawableAreaWidth = drawableAreaWidth;
DrawableAreaHeight = drawableAreaHeight;
VisibleWidth = visibleWidth;
VisibleHeight = visibleHeight;
RefreshFrequencyHz = refreshFrequencyHz;
}

public int DrawableAreaWidth { get; }
public int DrawableAreaHeight { get; }
public int VisibleWidth { get; }
public int VisibleHeight { get; }
public bool HasBorder => VisibleWidth > DrawableAreaWidth || VisibleHeight > DrawableAreaHeight;
public int VisibleLeftRightBorderWidth => (VisibleWidth - DrawableAreaWidth) / 2;
public int VisibleTopBottomBorderHeight => (VisibleHeight - DrawableAreaHeight) / 2;
public float RefreshFrequencyHz { get; }
}
8 changes: 8 additions & 0 deletions src/libraries/Highbyte.DotNet6502.Systems/SystemList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ public async Task<ISystem> BuildSystem(string systemName, string configurationVa
return system;
}

public IScreen? GetScreenInfo(string systemName, string configurationVariant, IHostSystemConfig hostSystemConfig)
{
if (!Systems.Contains(systemName))
throw new DotNet6502Exception($"System does not exist: {systemName}");

return _systemConfigurers[systemName].GetScreenInfo(configurationVariant, hostSystemConfig.SystemConfig);
}

public async Task<SystemRunner> BuildSystemRunner(
string systemName,
string configurationVariant)
Expand Down
48 changes: 48 additions & 0 deletions tests/Highbyte.DotNet6502.Systems.Tests/HostAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,61 @@ public async Task UpdateHostSystemConfig_WhenConfigBecomesValid_RebuildsTemporar
Assert.NotNull(systemAfterFixup);
}

[Fact]
public async Task CurrentSystemScreenInfo_WhenSelectedSystemCannotBeBuilt_UsesConfigurerScreenInfo()
{
// Arrange: select a system, then make config invalid so no temporary system can be built.
var testApp = BuildTestHostApp();
await testApp.SelectSystem(TestSystem.SystemName);
testApp.UpdateHostSystemConfig(new TestHostSystemConfig { TestIsValid = false });

// Act
var screenInfo = testApp.CurrentSystemScreenInfo;

// Assert
Assert.NotNull(screenInfo);
Assert.Equal(123, screenInfo.VisibleWidth);
Assert.Equal(67, screenInfo.VisibleHeight);
}

[Fact]
public async Task SelectSystem_WhenPreviousSystemIsInvalid_UpdatesVariantBeforeSystemChangedNotification()
{
// Arrange: the first system is selected but cannot be built, so screen info must come from
// the selected system/variant rather than from a temporary system instance.
var testApp = BuildTestHostApp();
await testApp.SelectSystem(TestSystem.SystemName);
testApp.UpdateHostSystemConfig(new TestHostSystemConfig { TestIsValid = false });
testApp.ReadScreenInfoOnSelectedSystemChanged = true;

// Act: switch to a system whose GetScreenInfo rejects stale variants.
await testApp.SelectSystem(TestSystem2.SystemName);

// Assert: reading screen info during the selected-system notification saw the new variant.
Assert.Equal(TestSystem2Configurer.Variant, testApp.SelectedSystemConfigurationVariant);
Assert.NotNull(testApp.ScreenInfoReadDuringSelectedSystemChanged);
Assert.Equal(246, testApp.ScreenInfoReadDuringSelectedSystemChanged.VisibleWidth);
Assert.Equal(134, testApp.ScreenInfoReadDuringSelectedSystemChanged.VisibleHeight);
}

public class TestHostApp : HostApp
{
public TestHostApp(
SystemList systemList
) : base("TestHost", systemList, new NullLoggerFactory())
{
}

public bool ReadScreenInfoOnSelectedSystemChanged { get; set; }
public IScreen? ScreenInfoReadDuringSelectedSystemChanged { get; private set; }

public override void OnAfterSelectedSystemChanged()
{
if (ReadScreenInfoOnSelectedSystemChanged)
ScreenInfoReadDuringSelectedSystemChanged = CurrentSystemScreenInfo;

base.OnAfterSelectedSystemChanged();
}
}

private TestHostApp BuildTestHostApp(bool setContexts = true, bool initContexts = true)
Expand Down
11 changes: 10 additions & 1 deletion tests/Highbyte.DotNet6502.Systems.Tests/SystemConfigurerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class TestSystemConfigurer : ISystemConfigurer
public string SystemName => TestSystem.SystemName;
public Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig) => Task.FromResult(new List<string> { "DEFAULT" });

public IScreen? GetScreenInfo(string configurationVariant, ISystemConfig systemConfig)
=> new ScreenInfo(100, 50, 123, 67, 60);

public Task<IHostSystemConfig> GetNewHostSystemConfig()
{
return Task.FromResult<IHostSystemConfig>(new TestHostSystemConfig());
Expand Down Expand Up @@ -51,7 +54,13 @@ IHostSystemConfig hostSystemConfig
public class TestSystem2Configurer : ISystemConfigurer
{
public string SystemName => TestSystem2.SystemName;
public Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig) => Task.FromResult(new List<string> { "DEFAULT" });
public const string Variant = "TEST2";
public Task<List<string>> GetConfigurationVariants(ISystemConfig systemConfig) => Task.FromResult(new List<string> { Variant });

public IScreen? GetScreenInfo(string configurationVariant, ISystemConfig systemConfig)
=> configurationVariant == Variant
? new ScreenInfo(200, 100, 246, 134, 60)
: throw new ArgumentException($"Unexpected variant '{configurationVariant}'.", nameof(configurationVariant));

public Task<IHostSystemConfig> GetNewHostSystemConfig()
{
Expand Down
Loading
Loading