Skip to content

Commit 0363441

Browse files
authored
Implementation of Chip8 UI in Blazor. (#27)
1 parent bba1a0d commit 0363441

20 files changed

+707
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,5 @@ FodyWeavers.xsd
425425
*.msi
426426
*.msix
427427
*.msm
428-
*.msp
428+
*.msp
429+
/src/Chip8.UI.Blazor/wwwroot/games
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
namespace Chip8.UI.Blazor;
2+
3+
/// <summary>
4+
/// Blazor WebAssembly implementation of <see cref="IDisplay"/>.
5+
/// Maintains a pixel buffer; actual canvas rendering is driven externally via <see cref="GetPixelData"/>.
6+
/// </summary>
7+
internal sealed class BlazorDisplay : IDisplay
8+
{
9+
private const int ScreenWidth = 64;
10+
private const int ScreenHeight = 32;
11+
12+
private readonly bool[,] _pixelBuffer = new bool[ScreenWidth, ScreenHeight];
13+
private readonly byte[] _pixelData = new byte[ScreenWidth * ScreenHeight];
14+
private bool _dirty;
15+
16+
public void Clear()
17+
{
18+
Array.Clear(_pixelBuffer);
19+
_dirty = true;
20+
}
21+
22+
public void ClearPixel(byte x, byte y)
23+
{
24+
_pixelBuffer[x, y] = false;
25+
_dirty = true;
26+
}
27+
28+
public bool GetPixel(byte x, byte y) => _pixelBuffer[x, y];
29+
30+
public void SetPixel(byte x, byte y)
31+
{
32+
_pixelBuffer[x, y] = true;
33+
_dirty = true;
34+
}
35+
36+
public void RenderIfDirty()
37+
{
38+
// No - op in Blazor.Rendering is handled by the game loop via GetPixelData.
39+
}
40+
41+
/// <summary>
42+
/// Returns a flat byte array (64×32, row-major, 1=on 0=off) if the display changed since the last call.
43+
/// Returns null if nothing changed.
44+
/// </summary>
45+
public byte[]? GetPixelData()
46+
{
47+
if (!_dirty)
48+
{
49+
return null;
50+
}
51+
52+
_dirty = false;
53+
54+
for (int y = 0; y < ScreenHeight; y++)
55+
{
56+
for (int x = 0; x < ScreenWidth; x++)
57+
{
58+
_pixelData[y * ScreenWidth + x] = _pixelBuffer[x, y] ? (byte)1 : (byte)0;
59+
}
60+
}
61+
62+
return _pixelData;
63+
}
64+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace Chip8.UI.Blazor;
2+
3+
/// <summary>
4+
/// Blazor WebAssembly implementation of <see cref="IKeyboard"/>.
5+
/// Key state is read from JavaScript as a 16-bit bitmask once per frame via <see cref="UpdateKeyState"/>.
6+
/// </summary>
7+
internal sealed class BlazorKeyboard : IKeyboard
8+
{
9+
private int _keyMask;
10+
11+
private bool _waitingForRelease;
12+
private byte? _pressedKey;
13+
14+
/// <summary>
15+
/// Updates the keyboard state from a bitmask where bit N = 1 means CHIP-8 key N is pressed.
16+
/// Called once per frame from the game loop.
17+
/// </summary>
18+
public void UpdateKeyState(int keyMask)
19+
{
20+
_keyMask = keyMask;
21+
}
22+
23+
public bool IsKeyDown(byte key) => (_keyMask & (1 << key)) != 0;
24+
25+
public byte? WaitForKeyPressAndRelease()
26+
{
27+
if (!_waitingForRelease)
28+
{
29+
for (byte i = 0; i < 16; i++)
30+
{
31+
if (IsKeyDown(i))
32+
{
33+
_waitingForRelease = true;
34+
_pressedKey = i;
35+
return null;
36+
}
37+
}
38+
return null;
39+
}
40+
else
41+
{
42+
if (_pressedKey.HasValue && !IsKeyDown(_pressedKey.Value))
43+
{
44+
_waitingForRelease = false;
45+
var key = _pressedKey;
46+
_pressedKey = null;
47+
return key;
48+
}
49+
return null;
50+
}
51+
}
52+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\Chip8\Chip8.csproj" />
15+
</ItemGroup>
16+
17+
<!-- Copy ROM files from repo games folder to wwwroot/games and generate manifest -->
18+
<ItemGroup>
19+
<_GameFile Include="..\..\games\*" Exclude="..\..\games\*.c8" />
20+
</ItemGroup>
21+
22+
<Target Name="CopyGamesToWwwroot" BeforeTargets="PrepareForBuild">
23+
<MakeDir Directories="wwwroot\games" />
24+
<Copy SourceFiles="@(_GameFile)" DestinationFolder="wwwroot\games" SkipUnchangedFiles="true" />
25+
</Target>
26+
27+
<Target Name="GenerateGameManifest" AfterTargets="CopyGamesToWwwroot">
28+
<ItemGroup>
29+
<_GameFileName Include="@(_GameFile->'&quot;%(Filename)%(Extension)&quot;')" />
30+
</ItemGroup>
31+
<PropertyGroup>
32+
<_GameList>@(_GameFileName, ',')</_GameList>
33+
</PropertyGroup>
34+
<WriteLinesToFile File="wwwroot\games\games.json" Lines="[$(_GameList)]" Overwrite="true" />
35+
</Target>
36+
37+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Router AppAssembly="typeof(App).Assembly">
2+
<Found Context="routeData">
3+
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
4+
<FocusOnNavigate RouteData="routeData" Selector="h1" />
5+
</Found>
6+
<NotFound>
7+
<p>Page not found.</p>
8+
</NotFound>
9+
</Router>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@inherits LayoutComponentBase
2+
3+
@Body
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#blazor-error-ui {
2+
color-scheme: light only;
3+
background: lightyellow;
4+
bottom: 0;
5+
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
6+
box-sizing: border-box;
7+
display: none;
8+
left: 0;
9+
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
10+
position: fixed;
11+
width: 100%;
12+
z-index: 1000;
13+
}
14+
15+
#blazor-error-ui .dismiss {
16+
cursor: pointer;
17+
position: absolute;
18+
right: 0.75rem;
19+
top: 0.5rem;
20+
}

0 commit comments

Comments
 (0)