diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d04a22f3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,282 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space + +# Code files +[*.cs] +indent_size = 4 +insert_final_newline = true +charset = utf-8 +#charset = utf-8-bom + +# Xml project files +[*.csproj] +indent_size = 2 + +# Xml config files +[*.{props,targets}] +indent_size = 2 + +# Dotnet code style settings: +[*.cs] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_style_require_accessibility_modifiers = always:warning + +# No blank line between System.* and Microsoft.* +dotnet_separate_import_directive_groups = false + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false +dotnet_style_allow_statement_immediately_after_block_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false + +# dotnet_style_allow_multiple_blank_lines_experimental +dotnet_diagnostic.IDE2000.severity = warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.IDE2001.severity = none + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.IDE2002.severity = warning + +# dotnet_style_allow_statement_immediately_after_block_experimental +dotnet_diagnostic.IDE2003.severity = suggestion + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.IDE2004.severity = warning + + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:error +dotnet_style_null_propagation = true:error +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = false +dotnet_style_prefer_conditional_expression_over_assignment = false +dotnet_style_prefer_auto_properties = false + +# Avoid "this." and "Me." if not necessary +#dotnet_style_qualification_for_field = false:error +#dotnet_style_qualification_for_property = true:error +#dotnet_style_qualification_for_method = false:error +#dotnet_style_qualification_for_event = false:error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Prefer read-only on fields +dotnet_style_readonly_field = true:warning + +# Naming Rules +dotnet_naming_rule.interfaces_must_be_pascal_cased_and_prefixed_with_I.symbols = interface_symbols +dotnet_naming_rule.interfaces_must_be_pascal_cased_and_prefixed_with_I.style = pascal_case_and_prefix_with_I_style +dotnet_naming_rule.interfaces_must_be_pascal_cased_and_prefixed_with_I.severity = warning + +dotnet_naming_rule.externally_visible_members_must_be_pascal_cased.symbols = externally_visible_symbols +dotnet_naming_rule.externally_visible_members_must_be_pascal_cased.style = pascal_case_style +dotnet_naming_rule.externally_visible_members_must_be_pascal_cased.severity = warning + +dotnet_naming_rule.parameters_must_be_camel_cased.symbols = parameter_symbols +dotnet_naming_rule.parameters_must_be_camel_cased.style = camel_case_style +dotnet_naming_rule.parameters_must_be_camel_cased.severity = warning + +dotnet_naming_rule.constants_must_be_pascal_cased.symbols = constant_symbols +dotnet_naming_rule.constants_must_be_pascal_cased.style = pascal_case_style +dotnet_naming_rule.constants_must_be_pascal_cased.severity = warning + +dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.symbols = private_static_field_symbols +dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.style = camel_case_and_prefix_with_s_underscore_style +dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.severity = warning + +dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.symbols = private_field_symbols +dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.style = camel_case_and_prefix_with_underscore_style +dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.severity = warning + +# Symbols +dotnet_naming_symbols.externally_visible_symbols.applicable_kinds = class,struct,interface,enum,property,method,field,event,delegate +dotnet_naming_symbols.externally_visible_symbols.applicable_accessibilities = public,internal,friend,protected,protected_internal,protected_friend,private_protected + +dotnet_naming_symbols.interface_symbols.applicable_kinds = interface +dotnet_naming_symbols.interface_symbols.applicable_accessibilities = * + +dotnet_naming_symbols.parameter_symbols.applicable_kinds = parameter +dotnet_naming_symbols.parameter_symbols.applicable_accessibilities = * + +dotnet_naming_symbols.constant_symbols.applicable_kinds = field +dotnet_naming_symbols.constant_symbols.required_modifiers = const +dotnet_naming_symbols.constant_symbols.applicable_accessibilities = * + +dotnet_naming_symbols.private_static_field_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_field_symbols.required_modifiers = static,shared +dotnet_naming_symbols.private_static_field_symbols.applicable_accessibilities = private + +dotnet_naming_symbols.private_field_symbols.applicable_kinds = field +dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private + +# Styles +dotnet_naming_style.camel_case_style.capitalization = camel_case + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.required_prefix = s_ +dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.capitalization = camel_case + +dotnet_naming_style.camel_case_and_prefix_with_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization = camel_case + +dotnet_naming_style.pascal_case_and_prefix_with_I_style.required_prefix = I +dotnet_naming_style.pascal_case_and_prefix_with_I_style.capitalization = pascal_case + +# CSharp code style settings: +# Prefer "var" only when the type is apparent +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:silent + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Identation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false + +# Spacing options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_before_open_square_brackets = false +csharp_space_around_declaration_statements = false +csharp_space_around_binary_operators = before_and_after +csharp_space_after_cast = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_before_dot = false +csharp_space_after_dot = false +csharp_space_before_comma = false +csharp_space_after_comma = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_semicolon_in_for_statement = true + +# Wrapping +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +# Code block +csharp_prefer_braces = when_multiline:warning + + +# ----------------------------------------------- +# Misc Code Styles that should affect code analysis +# ----------------------------------------------- +# IDE0005: Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = warning + +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0052: Remove unread private members +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0054: Use compound assignment +dotnet_style_prefer_compound_assignment = false:suggestion + +# IDE0055: Fix formatting. All formatting options have rule ID IDE0055 and title Fix formatting +dotnet_diagnostic.IDE0055.severity = warning + + +# ----------------------------------------------- +# Misc Code Rules that should be disabled in code analysis +# ----------------------------------------------- + +# CA1303: Do not pass literals as localized parameters +#dotnet_diagnostic.CA1303.severity = none + +# CA1812: An internal class that is apparently never instantiated. If so, remove the code from the assembly. If this class is intended to contain only static members, make it static (Shared in Visual Basic). +#dotnet_diagnostic.CA1812.severity = none + +# CA1707: Identifiers should not contain underscores +#dotnet_diagnostic.CA1707.severity = none + +# CA1062: Validate arguments of public methods +#dotnet_diagnostic.CA1062.severity = none + +# CA1710: Identifiers should have correct suffix +#dotnet_diagnostic.CA1710.severity = none + +# Don't use keywords +#dotnet_diagnostic.CA1716.severity = none + +# CA1063: Implement IDisposable Correctly +#dotnet_diagnostic.CA1063.severity = none + +# CA1816: Dispose methods should call SuppressFinalize +#dotnet_diagnostic.CA1816.severity = none + +# CA1305: Specify IFormatProvider +#dotnet_diagnostic.CA1305.severity = none + +# CA1308: Normalize strings to uppercase +#dotnet_diagnostic.CA1308.severity = none + +# CA1307: Specify StringComparison +#dotnet_diagnostic.CA1307.severity = none + +# CA1030: Use events where appropriate +#dotnet_diagnostic.CA1030.severity = none + +#dotnet_diagnostic.CA1062.severity = none +#dotnet_diagnostic.CS1591.severity = none +#dotnet_diagnostic.CS1573.severity = none + +# CA1724: Type names should not match namespaces +#dotnet_diagnostic.CA1724.severity = none diff --git a/.github/workflows/dotnet-blazor-publish-example-to-gh-pages.yml b/.github/workflows/dotnet-blazor-publish-example-to-gh-pages.yml index ac4684e1..056e2f70 100644 --- a/.github/workflows/dotnet-blazor-publish-example-to-gh-pages.yml +++ b/.github/workflows/dotnet-blazor-publish-example-to-gh-pages.yml @@ -8,7 +8,8 @@ on: env: CONFIGURATION: "Release" - PROJECT_FILE: "Examples/BlazorWasmTest/BlazorWasmTest.csproj" + #PROJECT_FILE: "Examples/BlazorWasmTest/BlazorWasmTest.csproj" + PROJECT_FILE: "Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.csproj" PROJECT_GH_PAGES_DIR: "blazorexample" BUILD_OUTPUT_WORKING_DIR: "build" GH_PAGES_WORKING_DIR: "ghpages" @@ -27,6 +28,10 @@ jobs: with: dotnet-version: 6.0.x + - name: Install DotNet workload - WASM tools + run: | + dotnet workload install wasm-tools + # Create GitHub Pages root folder contents - name: Create GH root folder and add .nojekyll file do disable built-in Jekyll CMS run: | diff --git a/Examples/BlazorWasmSkiaTest/App.razor b/Examples/BlazorWasmSkiaTest/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.csproj b/Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.csproj new file mode 100644 index 00000000..062a8490 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.csproj @@ -0,0 +1,42 @@ + + + + net6.0 + enable + enable + + true + + + + + + + + + + false + -O1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.sln b/Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.sln new file mode 100644 index 00000000..2b5281bc --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/BlazorWasmSkiaTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWasmSkiaTest", "BlazorWasmSkiaTest.csproj", "{41C81D23-C130-47DC-B741-4F4D134CE003}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {41C81D23-C130-47DC-B741-4F4D134CE003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41C81D23-C130-47DC-B741-4F4D134CE003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41C81D23-C130-47DC-B741-4F4D134CE003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41C81D23-C130-47DC-B741-4F4D134CE003}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60EEAAA6-9D93-4957-9663-11304A1550A8} + EndGlobalSection +EndGlobal diff --git a/Examples/BlazorWasmSkiaTest/Helpers/EmulatorHelper.cs b/Examples/BlazorWasmSkiaTest/Helpers/EmulatorHelper.cs new file mode 100644 index 00000000..6dc1859c --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Helpers/EmulatorHelper.cs @@ -0,0 +1,172 @@ +using Highbyte.DotNet6502; + +namespace BlazorWasmSkiaTest.Helpers +{ + public class EmulatorHelper + { + private Computer? _computer; + private readonly Random _rnd = new(); + + private readonly ushort _screenMemoryAddress = DEFAULT_SCREEN_MEMORY_ADDRESS; + private readonly ushort _colorMemoryAddress = DEFAULT_COLOR_MEMORY_ADDRESS; + private readonly ushort _borderColorAddress = DEFAULT_BORDER_COLOR_ADDRESS; + private readonly ushort _backgroundColorAddress = DEFAULT_BACKGROUND_COLOR_ADDRESS; + + public int MaxCols { get; private set; } = DEFAULT_MAX_COLS; + public int MaxRows { get; private set; } = DEFAULT_MAX_ROWS; + + private enum ScreenStatusBitFlags : int + { + HostNewFrame = 0, + EmulatorDoneForFrame = 1, + } + + private const int DEFAULT_MAX_COLS = 40; + private const int DEFAULT_MAX_ROWS = 25; + private const ushort DEFAULT_SCREEN_MEMORY_ADDRESS = 0x0400; + private const ushort DEFAULT_COLOR_MEMORY_ADDRESS = 0xd800; + private const ushort DEFAULT_BORDER_COLOR_ADDRESS = 0xd020; + private const ushort DEFAULT_BACKGROUND_COLOR_ADDRESS = 0xd021; + + // Memory address in emulator that the 6502 program and the host will use to communicate if current frame is done or not. + private const ushort SCREEN_REFRESH_STATUS_ADDRESS = 0xd000; + // Currently pressed key on host (ASCII byte). If no key is pressed, value is 0x00 + private const ushort KEY_PRESSED_ADDRESS = 0xd030; + // Currently down key on host (ASCII byte). If no key is down, value is 0x00 + private const ushort KEY_DOWN_ADDRESS = 0xd031; + // Currently released key on host (ASCII byte). If no key is down, value is 0x00 + private const ushort KEY_RELEASED_ADDRESS = 0xd031; + + // Memory address to store a randomly generated value between 0-255 + private const ushort RANDOM_VALUE_ADDRESS = 0xd41b; + + public EmulatorHelper(int? cols, int? rows, ushort? screenMemoryAddress, ushort? colorMemoryAddress) + { + if (cols.HasValue) + MaxCols = cols.Value; + if (rows.HasValue) + MaxRows = rows.Value; + if (screenMemoryAddress.HasValue) + _screenMemoryAddress = screenMemoryAddress.Value; + if (colorMemoryAddress.HasValue) + _colorMemoryAddress = colorMemoryAddress.Value; + } + + public void InitDotNet6502Computer(byte[] prgBytes) + { + // First two bytes of binary file is assumed to be start address, little endian notation. + var fileHeaderLoadAddress = ByteHelpers.ToLittleEndianWord(prgBytes[0], prgBytes[1]); + // The rest of the bytes are considered the code & data + var codeAndDataActual = new byte[prgBytes.Length - 2]; + Array.Copy(prgBytes, 2, codeAndDataActual, 0, prgBytes.Length - 2); + + var mem = new Memory(); + mem.StoreData(fileHeaderLoadAddress, codeAndDataActual); + + // Initialize emulator with CPU, memory, and execution parameters + var computerBuilder = new ComputerBuilder(); + computerBuilder + .WithCPU() + .WithStartAddress(fileHeaderLoadAddress) + .WithMemory(mem) + // .WithInstructionExecutedEventHandler( + // (s, e) => System.Diagnostics.Debug.WriteLine(OutputGen.GetLastInstructionDisassembly(e.CPU, e.Mem))) + .WithExecOptions(options => + { + options.MaxNumberOfInstructions = 10; + options.ExecuteUntilInstruction = OpCodeId.BRK; // Emulator will stop executing when a BRK instruction is reached. + }); + _computer = computerBuilder.Build(); + + InitEmulatorScreenMemory(); + } + + private void InitEmulatorScreenMemory() + { + var mem = _computer!.Mem; + // Common bg and border color for entire screen, controlled by specific address + mem[_borderColorAddress] = 0x0e; // light blue + mem[_backgroundColorAddress] = 0x06; // blue + + var currentScreenAddress = _screenMemoryAddress; + var currentColorAddress = _colorMemoryAddress; + for (var row = 0; row < MaxRows; row++) + { + for (var col = 0; col < MaxCols; col++) + { + mem[currentScreenAddress++] = 0x20; // 32 (0x20) = space + mem[currentColorAddress++] = 0x0e; // light blue + } + } + } + + public void ExecuteEmulator() + { + AssertComputerInitialized(); + + // Set emulator Refresh bit + // Emulator will wait until this bit is set until "redrawing" new data into memory + _computer!.Mem.SetBit(SCREEN_REFRESH_STATUS_ADDRESS, (int)ScreenStatusBitFlags.HostNewFrame); + + var shouldExecuteEmulator = true; + while (shouldExecuteEmulator) + { + // Execute a number of instructions + // TODO: _computer there a more optimal number of instructions to execute before we check if emulator code has flagged it's done via memory flag? + _computer.Run(new ExecOptions { MaxNumberOfInstructions = 10 }); // TODO: What is the optimal number of cycles to execute in each loop? + shouldExecuteEmulator = !_computer.Mem.IsBitSet(SCREEN_REFRESH_STATUS_ADDRESS, (int)ScreenStatusBitFlags.EmulatorDoneForFrame); + } + + // Clear the flag that the emulator set to indicate it's done. + _computer.Mem.ClearBit(SCREEN_REFRESH_STATUS_ADDRESS, (int)ScreenStatusBitFlags.EmulatorDoneForFrame); + } + + public void KeyDown(int keyCode) + { + AssertComputerInitialized(); + _computer!.Mem[KEY_DOWN_ADDRESS] = (byte)keyCode; + } + + public void KeyUp(int keyCode) + { + AssertComputerInitialized(); + _computer!.Mem[KEY_DOWN_ADDRESS] = 0x00; + } + + public void GenerateRandomNumber() + { + AssertComputerInitialized(); + _computer!.Mem[RANDOM_VALUE_ADDRESS] = (byte)_rnd.Next(0, 255); + } + + public byte GetScreenCharacter(int col, int row) + { + AssertComputerInitialized(); + return _computer!.Mem[(ushort)(_screenMemoryAddress + row * MaxCols + col)]; + } + + public byte GetScreenCharacterForegroundColor(int col, int row) + { + AssertComputerInitialized(); + return _computer!.Mem[(ushort)(_colorMemoryAddress + row * MaxCols + col)]; + } + + public byte GetBackgroundColor() + { + AssertComputerInitialized(); + return _computer!.Mem[_backgroundColorAddress]; + } + + public byte GetBorderColor() + { + AssertComputerInitialized(); + return _computer!.Mem[_borderColorAddress]; + } + + private void AssertComputerInitialized() + { + if (_computer == null) + throw new InvalidOperationException($"Computer object not initialized. Call {nameof(InitDotNet6502Computer)}"); + } + } +} diff --git a/Examples/BlazorWasmSkiaTest/Pages/Index.razor b/Examples/BlazorWasmSkiaTest/Pages/Index.razor new file mode 100644 index 00000000..83cb123f --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Pages/Index.razor @@ -0,0 +1,66 @@ +@page "/" +@using BlazorWasmSkiaTest.Helpers +@using SkiaSharp +@using SkiaSharp.Views.Blazor +@*@implements IDisposable*@ + +Blazor Wasm Skia Test +@* +*@ + + + +@code +{ + + private void OnKeyPress(KeyboardEventArgs e) + { + } + + private void OnKeyDown(KeyboardEventArgs e) + { + int keyCode=0; + if(e.Key.Length==1) + keyCode = (int)e.Key[0]; + + _emulatorHelper!.KeyDown(keyCode); + } + + private void OnKeyUp(KeyboardEventArgs e) + { + int keyCode=0; + if(e.Key.Length==1) + keyCode = (int)e.Key[0]; + + _emulatorHelper!.KeyUp(keyCode); + } + + //private void OnPointerDown(PointerEventArgs e) + //{ + //} + + //private void OnPointerMove(PointerEventArgs e) + //{ + //} + + //private void OnPointerUp(PointerEventArgs e) + //{ + //} + + //private void OnTouchMove(TouchEventArgs e) + //{ + //} + + //private void OnMouseWheel(WheelEventArgs e) + //{ + //} +} diff --git a/Examples/BlazorWasmSkiaTest/Pages/Index.razor.cs b/Examples/BlazorWasmSkiaTest/Pages/Index.razor.cs new file mode 100644 index 00000000..66a5ba0e --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Pages/Index.razor.cs @@ -0,0 +1,200 @@ +using System.Reflection; +using BlazorWasmSkiaTest.Helpers; +using BlazorWasmSkiaTest.Skia; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; +using SkiaSharp; +using SkiaSharp.Views.Blazor; + +namespace BlazorWasmSkiaTest.Pages +{ + public partial class Index + { + + private const string DEFAULT_PRG_URL = "6502binaries/hostinteraction_scroll_text_and_cycle_colors.prg"; + private const int TextSize = 20; + + private EmulatorHelper? _emulatorHelper; + private EmulatorRenderer? _emulatorRenderer; + + [Inject] + public HttpClient? HttpClient { get; set; } + + [Inject] + public NavigationManager? NavManager { get; set; } + + protected async override void OnInitialized() + { + var uri = NavManager!.ToAbsoluteUri(NavManager.Uri); + // Load 6502 program binary + var prgBytes = await Load6502Binary(uri); + + // Init 6502 emulator + (int? cols, int? rows, ushort? screenMemoryAddress, ushort? colorMemoryAddress) = GetScreenSize(uri); + + _emulatorHelper = new EmulatorHelper(cols, rows, screenMemoryAddress, colorMemoryAddress); + _emulatorHelper.InitDotNet6502Computer(prgBytes); + + // Init emulator renderer (Skia) + var timer = new PeriodicAsyncTimer(); + //SKTypeface typeFace = await LoadFont("../fonts/C64_Pro_Mono-STYLE.woff2"); + SKTypeface typeFace = LoadEmbeddedFont("C64_Pro_Mono-STYLE.ttf"); + var skColorMaps = new SKPaintMaps(TextSize, typeFace, ColorMaps.C64ColorMap); + _emulatorRenderer = new EmulatorRenderer(timer, skColorMaps, TextSize, _emulatorHelper); + } + + private (int? cols, int? rows, ushort? screenMemoryAddress, ushort? colorMemoryAddress) GetScreenSize(Uri uri) + { + int? cols = null; + int? rows = null; + ushort? screenMemoryAddress = null; + ushort? colorMemoryAddress = null; + + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("cols", out var colsParameter)) + { + if (int.TryParse(colsParameter, out int colsParsed)) + cols = colsParsed; + else + cols = null; + } + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("rows", out var rowsParameter)) + { + if (int.TryParse(rowsParameter, out int rowsParsed)) + rows = rowsParsed; + else + rows = null; + } + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("screenMem", out var screenMemParameter)) + { + if (ushort.TryParse(screenMemParameter, out ushort screenMemParsed)) + screenMemoryAddress = screenMemParsed; + else + screenMemoryAddress = null; + } + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("colorMem", out var colorMemParameter)) + { + if (ushort.TryParse(colorMemParameter, out ushort colorMemParsed)) + colorMemoryAddress = colorMemParsed; + else + colorMemoryAddress = null; + } + + return (cols, rows, screenMemoryAddress, colorMemoryAddress); + + } + + private async Task Load6502Binary(Uri uri) + { + byte[] prgBytes; + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("prgEnc", out var prgEnc)) + { + // Query parameter prgEnc must be a valid Base64Url encoded string. + // Examples on how to generate it from a compiled 6502 binary file: + // Linux: + // base64 -w0 myprogram.prg | sed 's/+/-/g; s/\//_/g' + + // https://www.base64encode.org/ + // - Encode files to Base64 format + // - Select file + // - Select options: BINARY, Perform URL-safe encoding (uses Base64Url format) + // - ENCODE + // - CLICK OR TAP HERE to download the encoded file + // - Use the contents in the generated file. + // + // Examples to generate a QR Code that will launch the program in the Base64URL string above: + // Linux: + // qrencode -s 3 -l L -o "myprogram.png" "http://localhost:5000/?prgEnc=THE_PROGRAM_ENCODED_AS_BASE64URL" + prgBytes = Base64UrlDecode(prgEnc.ToString()); + } + else if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("prgUrl", out var prgUrl)) + { + prgBytes = await HttpClient!.GetByteArrayAsync(prgUrl.ToString()); + } + else + { + prgBytes = await HttpClient!.GetByteArrayAsync(DEFAULT_PRG_URL); + } + + return prgBytes; + } + + /// + /// Decode a Base64Url encoded string. + /// The Base64Url standard is a bit different from normal Base64 + /// - Replaces '+' with '-' + /// - Replaces '/' with '_' + /// - Removes trailing '=' padding + /// + /// This method does the above in reverse before decoding it as a normal Base64 string. + /// + /// + /// + private byte[] Base64UrlDecode(string arg) + { + string s = arg; + s = s.Replace('-', '+'); // 62nd char of encoding + s = s.Replace('_', '/'); // 63rd char of encoding + switch (s.Length % 4) // Pad with trailing '='s + { + case 0: break; // No pad chars in this case + case 2: s += "=="; break; // Two pad chars + case 3: s += "="; break; // One pad char + default: + throw new ArgumentException("Illegal base64url string!"); + } + return Convert.FromBase64String(s); // Standard base64 decoder + } + + private async Task LoadFont(string fontUrl) + { + using (Stream file = await HttpClient!.GetStreamAsync(fontUrl)) + using (var memoryStream = new MemoryStream()) + { + await file.CopyToAsync(memoryStream); + //byte[] bytes = memoryStream.ToArray(); + var typeFace = SKTypeface.FromStream(memoryStream); + if (typeFace == null) + throw new ArgumentException($"Cannot load font as a Skia TypeFace. Url: {fontUrl}", nameof(fontUrl)); + return typeFace; + } + } + + private SKTypeface LoadEmbeddedFont(string fullFontName) + { + var assembly = Assembly.GetExecutingAssembly(); + + var resourceName = $"{"BlazorWasmSkiaTest.Resources.Fonts"}.{fullFontName}"; + using (Stream? resourceStream = assembly.GetManifestResourceStream(resourceName)) + { + if (resourceStream == null) + throw new ArgumentException($"Cannot load font from embedded resource. Resource: {resourceName}", nameof(fullFontName)); + + var typeFace = SKTypeface.FromStream(resourceStream); + if (typeFace == null) + throw new ArgumentException($"Cannot load font as a Skia TypeFace from embedded resource. Resource: {resourceName}", nameof(fullFontName)); + return typeFace; + } + } + + protected void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + _emulatorRenderer!.SetSize(e.Info.Width, e.Info.Height); + if (e.Surface.Context is GRContext context && context != null) + { + // If we draw our own images (not directly on the canvas provided), make sure it's within the same contxt + _emulatorRenderer.SetContext(context); + } + _emulatorRenderer.Render(e.Surface.Canvas); + } + + //private void BeforeUnload_BeforeUnloadHandler(object? sender, blazejewicz.Blazor.BeforeUnload.BeforeUnloadArgs e) + //{ + // _emulatorRenderer.Dispose(); + //} + + //public void Dispose() + //{ + // this.BeforeUnload.BeforeUnloadHandler -= BeforeUnload_BeforeUnloadHandler; + //} + } +} diff --git a/Examples/BlazorWasmSkiaTest/Program.cs b/Examples/BlazorWasmSkiaTest/Program.cs new file mode 100644 index 00000000..18debc07 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Program.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +namespace BlazorWasmSkiaTest +{ + public class Program + { + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + builder.RootComponents.Add("head::after"); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + var host = builder.Build(); + + await host.RunAsync(); + } + } +} diff --git a/Examples/BlazorWasmSkiaTest/Properties/launchSettings.json b/Examples/BlazorWasmSkiaTest/Properties/launchSettings.json new file mode 100644 index 00000000..46569f5e --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:23744", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "BlazorWasmSkiaTest": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Examples/BlazorWasmSkiaTest/Resources/Fonts/C64_Pro_Mono-STYLE.ttf b/Examples/BlazorWasmSkiaTest/Resources/Fonts/C64_Pro_Mono-STYLE.ttf new file mode 100644 index 00000000..02ff5fc4 Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/Resources/Fonts/C64_Pro_Mono-STYLE.ttf differ diff --git a/Examples/BlazorWasmSkiaTest/Shared/MainLayout.razor b/Examples/BlazorWasmSkiaTest/Shared/MainLayout.razor new file mode 100644 index 00000000..0de477d3 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Shared/MainLayout.razor @@ -0,0 +1,12 @@ +@inherits LayoutComponentBase + +
+
+
+

A 6502 machine code program, running in a 6502 emulator written in .NET, rendered with SkiaSharp, compiled to WebAssembly via Blazor, executing in a browser!

+
+
+ @Body +
+
+
\ No newline at end of file diff --git a/Examples/BlazorWasmSkiaTest/Shared/MainLayout.razor.css b/Examples/BlazorWasmSkiaTest/Shared/MainLayout.razor.css new file mode 100644 index 00000000..43c355a4 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Shared/MainLayout.razor.css @@ -0,0 +1,70 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/Examples/BlazorWasmSkiaTest/Skia/ColorMaps.cs b/Examples/BlazorWasmSkiaTest/Skia/ColorMaps.cs new file mode 100644 index 00000000..1c25da03 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Skia/ColorMaps.cs @@ -0,0 +1,28 @@ +using SkiaSharp; + +namespace BlazorWasmSkiaTest.Skia +{ + public static class ColorMaps + { + public static Dictionary C64ColorMap = new() + { + { 0x00, new SKColor(0, 0, 0) }, // Black + { 0x01, new SKColor(255, 255, 255) }, // White + { 0x02, new SKColor(136, 0, 0) }, // Red + { 0x03, new SKColor(170, 255, 238) }, // Cyan + { 0x04, new SKColor(204, 68, 204) }, // Violet/purple + { 0x05, new SKColor(0, 204, 85) }, // Green + { 0x06, new SKColor(0, 0, 170) }, // Blue + { 0x07, new SKColor(238, 238, 119) }, // Yellow + { 0x08, new SKColor(221, 136, 185) }, // Orange + { 0x09, new SKColor(102, 68, 0) }, // Brown + { 0x0a, new SKColor(255, 119, 119) }, // Light red + { 0x0b, new SKColor(51, 51, 51) }, // Dark grey + { 0x0c, new SKColor(119, 119, 119) }, // Grey + { 0x0d, new SKColor(170, 255, 102) }, // Light green + { 0x0e, new SKColor(0, 136, 255) }, // Light blue + { 0x0f, new SKColor(187, 187, 187) }, // Light grey + }; + + } +} diff --git a/Examples/BlazorWasmSkiaTest/Skia/EmulatorRenderer.cs b/Examples/BlazorWasmSkiaTest/Skia/EmulatorRenderer.cs new file mode 100644 index 00000000..340c3c28 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Skia/EmulatorRenderer.cs @@ -0,0 +1,151 @@ +using BlazorWasmSkiaTest.Helpers; +using SkiaSharp; + +namespace BlazorWasmSkiaTest.Skia +{ + public class EmulatorRenderer : IDisposable + { + private const int GameLoopInterval = 16; // Number of milliseconds between each invokation of the main game loop + private const int BorderWidthFactor = 3; // The factor applied to _textSizePixels to get the width and height of the border + + private readonly int _textSizePixels; // Size of each charatcter. Font assumed to be monospaced. + private readonly int _borderPixels; // Number of pixels used for border on top/bottom/left/right + + private int _screenWidth; + private int _screenHeight; + + private readonly PeriodicAsyncTimer? _renderLoopTimer; + private readonly SKPaintMaps _sKPaintMaps; + private readonly EmulatorHelper _emulatorHelper; + + public EmulatorRenderer( + PeriodicAsyncTimer? renderLoopTimer, + SKPaintMaps sKPaintMaps, + int textSize, + EmulatorHelper emulatorHelper) + { + _renderLoopTimer = renderLoopTimer; + _sKPaintMaps = sKPaintMaps; + _emulatorHelper = emulatorHelper; + + _textSizePixels = textSize; + _borderPixels = textSize * BorderWidthFactor; + + if (_renderLoopTimer != null) + { + _renderLoopTimer.IntervalMilliseconds = GameLoopInterval; + _renderLoopTimer.Elapsed += GameLoopTimerElapsed; + _renderLoopTimer.Start(); + } + } + + private void GameLoopTimerElapsed(object? sender, EventArgs e) => GameLoopStep(); + + public void SetSize(int width, int height) + { + _screenWidth = width; + _screenHeight = height; + } + + public (int Width, int Height) GetScreenSize() => (_screenWidth, _screenHeight); + + public void SetContext(GRContext context) + { + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] + public void GameLoopStep() + { + DoGameLoopStep(); + } + private void DoGameLoopStep() + { + _emulatorHelper.GenerateRandomNumber(); + _emulatorHelper.ExecuteEmulator(); + } + + public void Render(SKCanvas canvas) + { + using (new SKAutoCanvasRestore(canvas)) + { + RenderFrame(canvas); + } + } + + private void RenderFrame(SKCanvas canvas) + { + // Draw border + var borderColor = _emulatorHelper.GetBorderColor(); + var borderPaint = _sKPaintMaps.GetSKBackgroundPaint(borderColor); + canvas.DrawRect(0, 0, _emulatorHelper.MaxCols * _textSizePixels + _borderPixels * 2, _emulatorHelper.MaxRows * _textSizePixels + _borderPixels * 2, borderPaint); + + // Draw background + using (new SKAutoCanvasRestore(canvas)) + { + var bgColor = _emulatorHelper.GetBackgroundColor(); + var bgPaint = _sKPaintMaps.GetSKBackgroundPaint(bgColor); + canvas.Translate(_borderPixels, _borderPixels); + canvas.DrawRect(0, 0, _emulatorHelper.MaxCols * _textSizePixels, _emulatorHelper.MaxRows * _textSizePixels, bgPaint); + } + + using (new SKAutoCanvasRestore(canvas)) + { + canvas.Translate(_borderPixels, _borderPixels); + // Draw characters + for (var row = 0; row < _emulatorHelper.MaxRows; row++) + { + for (var col = 0; col < _emulatorHelper.MaxCols; col++) + { + var chr = _emulatorHelper.GetScreenCharacter(col, row); + var chrColor = _emulatorHelper.GetScreenCharacterForegroundColor(col, row); + var drawText = GetDrawTextFromCharacter(chr); + var textPaint = _sKPaintMaps.GetSKTextPaint(chrColor); + DrawCharacter(canvas, drawText, col, row, textPaint); + } + } + } + } + + private string GetDrawTextFromCharacter(byte chr) + { + string representAsString; + switch (chr) + { + case 0x00: // Uninitialized + case 0x0a: // NewLine/CarrigeReturn + case 0x0d: // NewLine/CarrigeReturn + representAsString = " "; // Replace with space + break; + case 0xa0: //160, C64 inverted space + case 0xe0: //224, Also C64 inverted space? + // Unicode for Inverted square in https://style64.org/c64-truetype font + representAsString = ((char)0x2588).ToString(); + break; + default: + // Even though both upper and lowercase characters are used in the 6502 program (and in the font), show all as uppercase for C64 look. + representAsString = Convert.ToString((char)chr).ToUpper(); + break; + } + return representAsString; + } + + private void DrawCharacter(SKCanvas canvas, string character, int col, int row, SKPaint textPaint) + { + //var textHeight = textPaint.TextSize; + + var x = col * _textSizePixels; + var y = row * _textSizePixels; + // Make clipping rectangle for the tile we're drawing, to avoid any accidental spill-over to neighboring tiles. + var rect = new SKRect(x, y, x + _textSizePixels, y + _textSizePixels); + using (new SKAutoCanvasRestore(canvas)) + { + canvas.ClipRect(rect, SKClipOperation.Intersect); + canvas.DrawText(character, x, y + (_textSizePixels - 2), textPaint); + } + } + + public void Dispose() + { + } + } +} diff --git a/Examples/BlazorWasmSkiaTest/Skia/PeriodicAsyncTimer.cs b/Examples/BlazorWasmSkiaTest/Skia/PeriodicAsyncTimer.cs new file mode 100644 index 00000000..d2828490 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Skia/PeriodicAsyncTimer.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; + +namespace BlazorWasmSkiaTest.Skia +{ + public class PeriodicAsyncTimer + { + private CancellationTokenSource? _cts; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private long _lastTick; + + public double IntervalMilliseconds { get; set; } + + public long TimeSinceLastTickMilliseconds { get; private set; } + + public event EventHandler? Elapsed; + + public void Dispose() + { + throw new NotImplementedException(); + } + + public void Start() + { + Stop(); + _cts = new CancellationTokenSource(); + _ = StartTimer(); + } + + private async Task StartTimer() + { + var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(IntervalMilliseconds)); + while (await timer.WaitForNextTickAsync(_cts!.Token)) + { + var time = _stopwatch.ElapsedMilliseconds; + TimeSinceLastTickMilliseconds = time - _lastTick; + _lastTick = time; + Elapsed?.Invoke(this, EventArgs.Empty); + } + } + + public void Stop() + { + _cts?.Cancel(); + } + } +} diff --git a/Examples/BlazorWasmSkiaTest/Skia/SKPaintMaps.cs b/Examples/BlazorWasmSkiaTest/Skia/SKPaintMaps.cs new file mode 100644 index 00000000..892a4244 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/Skia/SKPaintMaps.cs @@ -0,0 +1,72 @@ +using SkiaSharp; + +namespace BlazorWasmSkiaTest.Skia +{ + public class SKPaintMaps + { + private readonly Dictionary _emulatorColorTextPaint; + private readonly Dictionary _emulatorColorBackgroundPaint; + + public SKPaintMaps(int textSize, SKTypeface typeFace, Dictionary colorMap) + { + _emulatorColorTextPaint = new Dictionary(); + _emulatorColorBackgroundPaint = new Dictionary(); + foreach (var colorKey in colorMap.Keys) + { + _emulatorColorTextPaint[colorKey] = BuildTextPaint(C64ColorMap[colorKey], textSize, typeFace); + _emulatorColorBackgroundPaint[colorKey] = BuildBackgroundPaint(C64ColorMap[colorKey]); + } + } + + public SKPaint GetSKTextPaint(byte emulatorColor) + { + return _emulatorColorTextPaint[emulatorColor]; + } + + public SKPaint GetSKBackgroundPaint(byte emulatorColor) + { + return _emulatorColorBackgroundPaint[emulatorColor]; + } + + private SKPaint BuildTextPaint(SKColor color, int textSize, SKTypeface typeFace) + { + return new SKPaint + { + TextSize = textSize, + Typeface = typeFace, + //IsAntialias = true, + Color = color, + TextAlign = SKTextAlign.Left, + }; + } + private SKPaint BuildBackgroundPaint(SKColor color) + { + return new SKPaint + { + Color = color, + Style = SKPaintStyle.Fill + }; + } + + public static Dictionary C64ColorMap = new() + { + { 0x00, new SKColor(0, 0, 0) }, // Black + { 0x01, new SKColor(255, 255, 255) }, // White + { 0x02, new SKColor(136, 0, 0) }, // Red + { 0x03, new SKColor(170, 255, 238) }, // Cyan + { 0x04, new SKColor(204, 68, 204) }, // Violet/purple + { 0x05, new SKColor(0, 204, 85) }, // Green + { 0x06, new SKColor(0, 0, 170) }, // Blue + { 0x07, new SKColor(238, 238, 119) }, // Yellow + { 0x08, new SKColor(221, 136, 185) }, // Orange + { 0x09, new SKColor(102, 68, 0) }, // Brown + { 0x0a, new SKColor(255, 119, 119) }, // Light red + { 0x0b, new SKColor(51, 51, 51) }, // Dark grey + { 0x0c, new SKColor(119, 119, 119) }, // Grey + { 0x0d, new SKColor(170, 255, 102) }, // Light green + { 0x0e, new SKColor(0, 136, 255) }, // Light blue + { 0x0f, new SKColor(187, 187, 187) }, // Light grey + }; + + } +} diff --git a/Examples/BlazorWasmSkiaTest/_Imports.razor b/Examples/BlazorWasmSkiaTest/_Imports.razor new file mode 100644 index 00000000..9fee529d --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using BlazorWasmSkiaTest +@using BlazorWasmSkiaTest.Shared \ No newline at end of file diff --git a/Examples/BlazorWasmSkiaTest/global.json b/Examples/BlazorWasmSkiaTest/global.json new file mode 100644 index 00000000..ed3fdeaf --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "6.0.100" + } +} \ No newline at end of file diff --git a/Examples/BlazorWasmSkiaTest/screenshot.png b/Examples/BlazorWasmSkiaTest/screenshot.png new file mode 100644 index 00000000..f477f789 Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/screenshot.png differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/hostinteraction_scroll_text_and_cycle_colors.prg b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/hostinteraction_scroll_text_and_cycle_colors.prg new file mode 100644 index 00000000..ccbfeb1c Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/hostinteraction_scroll_text_and_cycle_colors.prg differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502.prg b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502.prg new file mode 100644 index 00000000..31e42f30 Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502.prg differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_hosted.png b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_hosted.png new file mode 100644 index 00000000..43adc04e Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_hosted.png differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_large_hosted.png b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_large_hosted.png new file mode 100644 index 00000000..3e88ca09 Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_large_hosted.png differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_local.png b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_local.png new file mode 100644 index 00000000..17c367dc Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/6502binaries/snake6502_qr_local.png differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/css/app.css b/Examples/BlazorWasmSkiaTest/wwwroot/css/app.css new file mode 100644 index 00000000..b7f24429 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/wwwroot/css/app.css @@ -0,0 +1,50 @@ +/* @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); */ +/* +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} */ + +a, .btn-link { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/favicon.ico b/Examples/BlazorWasmSkiaTest/wwwroot/favicon.ico new file mode 100644 index 00000000..63e859b4 Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/favicon.ico differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/fonts/C64_Pro_Mono-STYLE.woff2 b/Examples/BlazorWasmSkiaTest/wwwroot/fonts/C64_Pro_Mono-STYLE.woff2 new file mode 100644 index 00000000..a1b72c55 Binary files /dev/null and b/Examples/BlazorWasmSkiaTest/wwwroot/fonts/C64_Pro_Mono-STYLE.woff2 differ diff --git a/Examples/BlazorWasmSkiaTest/wwwroot/index.html b/Examples/BlazorWasmSkiaTest/wwwroot/index.html new file mode 100644 index 00000000..9f080c67 --- /dev/null +++ b/Examples/BlazorWasmSkiaTest/wwwroot/index.html @@ -0,0 +1,26 @@ + + + + + + + DotNet 6502 emulator with Blazor WebAssembly + + + + + + + + +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + diff --git a/README.md b/README.md index 98ddfd16..c6c43ffb 100644 --- a/README.md +++ b/README.md @@ -426,26 +426,26 @@ dotnet run Examples of a [Blazor WebAssembly](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) app running 6502 code with ```Highbyte.DotNet6502``` library. _Notes:_ -- Example uses a grid of `````` elements to represent each screen character. This could probably be done in a more performant way. +- The example uses [SkiaSharp.Views.Blazor](https://github.com/mono/SkiaSharp) for rendering. - The scrolling is choppy due to text-mode only, but the color-cycling works ok. -- Tested on Chrome v89 and Edge v89. +- Tested on Chrome v96 and Edge v96. ### Scroller - [Assembly code](Examples/SadConsoleTest/AssemblerSource/hostinteraction_scroll_text_and_cycle_colors.asm) - A deployed version can be found here [https://highbyte.se/dotnet-6502/blazorexample](https://highbyte.se/dotnet-6502/blazorexample) - + ### Snake game - [Assembly code](Examples/SadConsoleTest/AssemblerSource/snake6502.asm) - Game code based on original found [here](http://skilldrick.github.io/easy6502/#snake) - A deployed version can be found here [https://highbyte.se/dotnet-6502/blazorexample/?screenMem=512&cols=32&rows=32&prgUrl=6502binaries/snake6502.prg](https://highbyte.se/dotnet-6502/blazorexample/?screenMem=512&cols=32&rows=32&prgUrl=6502binaries/snake6502.prg) - Or why not have the 6502 game binary (approx. 450 bytes) encoded inside a QR code :) Aim the camera on your smartphone and follow the link. - + ### Run on local dev machine ``` -cd ./Examples/BlazorWasmTest +cd ./Examples/BlazorWasmSkiaTest dotnet run ``` and open browser at http://localhost:5000.