diff options
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | Program.cs | 195 | ||||
| -rw-r--r-- | emulator.sql | 698 | ||||
| -rw-r--r-- | pgch8.csproj | 14 | ||||
| -rw-r--r-- | pgch8.sln | 16 | ||||
| -rw-r--r-- | test_opcode.ch8 | bin | 0 -> 478 bytes |
6 files changed, 929 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58343ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +/.idea diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..800ee12 --- /dev/null +++ b/Program.cs @@ -0,0 +1,195 @@ +namespace pgch8; + +using System.Text; +using Npgsql; + +class Program +{ + private static NpgsqlConnection _conn; + private static Screen _screen; + + static void Main(string[] args) + { + CreateConnection(); + Program.Reset(); + + CreateScreen(); + WriteRom("/Users/melodidey/Source/csharp/pgch8/test_opcode.ch8"); + WriteFont([ + 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 + 0x20, 0x60, 0x20, 0x20, 0x70, // 1 + 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 + 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 + 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 + 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 + 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 + 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 + 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 + 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 + 0xF0, 0x90, 0xF0, 0x90, 0x90, // A + 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B + 0xF0, 0x80, 0x80, 0x80, 0xF0, // C + 0xE0, 0x90, 0x90, 0x90, 0xE0, // D + 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E + 0xF0, 0x80, 0xF0, 0x80, 0x80 // F + ]); + + while (true) + { + Program.Clock(); + if (Program.IsScreenDirty()) + { + _screen.Pixels = Program.GetScreenPixels(); + _screen.Render(); + Program.MarkScreenClean(); + } + + Thread.Sleep(1000 / 60); + } + } + + private static void Reset() + { + Program.Execute("select private.reset();"); + } + + private static void Clock() + { + Program.Execute("select private.clock();"); + } + + private static bool IsScreenDirty() + { + return Program.QuerySingle<bool>("select private.is_screen_dirty();"); + } + + private static void MarkScreenClean() + { + Program.Execute("select private.mark_screen_clean();"); + } + + private static bool[] GetScreenPixels() + { + return Program.Query( + "select value from private.display order by i;", + reader => + { + List<bool> pixels = new(); + while (reader.Read()) + { + pixels.Add(reader.GetBoolean(0)); + } + return pixels.ToArray(); + }); + } + + private static void CreateConnection() + { + Program._conn = new("Host=localhost;Username=melodidey;Database=emulator"); + Program._conn.Open(); + } + + private static void CreateScreen() + { + int screenWidth = QuerySingle<int>("select value from private.constants where label = 'Screen Width';"); + int screenHeight = QuerySingle<int>("select value from private.constants where label = 'Screen Height';"); + + Program._screen = new Screen(screenWidth, screenHeight); + } + + private static void WriteRom(string path) + { + byte[] rom = File.ReadAllBytes(path); + + int romStart = QuerySingle<int>( + "select value_word from private.constants where label = 'Program Start';" + ); + + for (int i = 0; i < rom.Length; i++) + { + Execute( + "update private.memory set value = @p1 where address = @p0;", + romStart + i, + rom[i] + ); + } + } + + private static void WriteFont(byte[] font) + { + int fontStart = QuerySingle<int>( + "select value_word from private.constants where label = 'Font Start';" + ); + + for (int i = 0; i < font.Length; i++) + { + Execute( + "update private.memory set value = @p1 where address = @p0;", + fontStart + i, + font[i] + ); + } + } + + static void Execute(string query, params object[] parameters) + { + using var cmd = new NpgsqlCommand(query, Program._conn); + for (int i = 0; i < parameters.Length; i++) + { + cmd.Parameters.AddWithValue($"@p{i}", parameters[i]); + } + cmd.ExecuteNonQuery(); + } + + static T Query<T>(string query, Func<NpgsqlDataReader, T> map) + { + using var cmd = new NpgsqlCommand(query, Program._conn); + using var reader = cmd.ExecuteReader(); + return map(reader); + } + + static T QuerySingle<T>(string query) + { + try + { + return Query(query, reader => + { + reader.Read(); + return reader.GetFieldValue<T>(0); + }); + } + catch (Exception ex) + { + Console.WriteLine($"Error executing query '{query}': {ex.Message}"); + throw; + } + } +} + +class Screen +{ + public int Width { get; set; } + public int Height { get; set; } + public bool[] Pixels { get; set; } + + public Screen(int width, int height) + { + Width = width; + Height = height; + Pixels = new bool[width * height]; + } + + public void Render() + { + StringBuilder sb = new(); + for (int i = 0; i < Pixels.Length; i++) + { + sb.Append(Pixels[i] ? "#" : " "); + if ((i + 1) % Width == 0) + sb.AppendLine(); + } + + Console.Clear(); + Console.WriteLine(sb.ToString()); + } +}
\ No newline at end of file diff --git a/emulator.sql b/emulator.sql new file mode 100644 index 0000000..10bb18a --- /dev/null +++ b/emulator.sql @@ -0,0 +1,698 @@ +drop schema if exists private cascade; + +create schema private; + +create table private.constants +( + id integer primary key, + label varchar(20) not null, + value integer, + value_word integer +); + +create index idx_constants_label on private.constants (label); + +create table private.registers +( + id integer primary key, + value integer not null +); + +create table private.state +( + id integer primary key, + label varchar(20) not null, + value integer, + value_word integer +); + +create table private.timers +( + id integer primary key, + label varchar(20) not null, + value integer not null +); + +create table private.inputs +( + id integer primary key, + value integer not null +); + +create table private.memory +( + address integer primary key, + value integer not null +); + +create table private.display +( + i integer primary key, + value boolean +); + +create table private.stack +( + id integer primary key, + value integer not null +); + +-- contains the current opcode and its parts for easy access +-- parts are: opcode, k, x, y, n, nn, nnn +create or replace view private.opcode_parts as +select s.value_word as opcode, + s.value_word & 61440 >> 12 as k, + (s.value_word & 3840) >> 8 as x, + (s.value_word & 240) >> 4 as y, + s.value_word & 15 as n, + s.value_word & 255 as nn, + s.value_word & 4095 as nnn +from private.state s +where s.id = 4; + +create or replace function private.reset() + returns void as +$$ +begin + truncate table private.constants; + insert into private.constants (id, label, value, value_word) + values (1, 'Font Start', null, 61440), + (2, 'Program Start', null, 512), + (3, 'Screen Width', 64, null), + (4, 'Screen Height', 32, null), + (5, 'Register Count', 16, null), + (6, 'Stack Depth', 16, null), + (7, 'Input Count', 16, null); + + + truncate table private.state; + insert into private.state (id, label, value, value_word) + values (1, 'Program Counter', null, (select value_word from private.constants where label = 'Program Start')), + (2, 'Index Register', null, 0), + (3, 'Stack Pointer', 0, null), + (4, 'Opcode', null, 0), + (5, 'Screen Dirty', 0, null); + + + truncate table private.registers; + insert into private.registers (id, value) + select gs, 0 + from generate_series(0, + (select value from private.constants where label = 'Register Count') - 1) as gs; + + + truncate table private.memory; + insert into private.memory (address, value) + select gs, 0 + from generate_series(0, 4095) as gs; + + + truncate table private.display; + insert into private.display (i, value) + select gs, false + from generate_series(0, + (select value from private.constants where label = 'Screen Width') * + (select value from private.constants where label = 'Screen Height') - 1) as gs; + + + truncate table private.timers; + insert into private.timers (id, label, value) + values (1, 'Delay Timer', 0), + (2, 'Sound Timer', 0); + + + truncate table private.stack; + insert into private.stack (id, value) + select gs, 0 + from generate_series(0, + (select value from private.constants where label = 'Stack Depth') - 1) as gs; + + + truncate table private.inputs; + insert into private.inputs (id, value) + select gs, 0 + from generate_series(0, + (select value from private.constants where label = 'Input Count') - 1) as gs; +end; +$$ language plpgsql; + + +create or replace function private.read_register(register_id integer) + returns integer as +$$ +declare + reg_value integer; +begin + select value + into reg_value + from private.registers + where id = register_id; + return reg_value; +end; +$$ language plpgsql; + +create or replace function private.write_register(register_id integer, new_value integer) + returns void as +$$ +begin + update private.registers + set value = new_value + where id = register_id; +end; +$$ language plpgsql; + +create or replace function private.push_stack(new_value integer) + returns void as +$$ +begin + -- stack[SP] = new_value + update private.stack + set value = new_value + where id = (select value from private.state where id = 3); + + -- SP++ + update private.state + set value = value + 1 + where id = 3; +end; +$$ language plpgsql; + +create or replace function private.pop_stack() + returns integer as +$$ +declare + stack_value integer; +begin + -- SP-- + update private.state + set value = value - 1 + where id = 3; + + -- return stack[SP] + select value + into stack_value + from private.stack + where id = (select value from private.state where id = 3); + return stack_value; +end; +$$ language plpgsql; + +create or replace function private.read_memory(read_address integer) + returns integer as +$$ +declare + mem_value integer; +begin + select value + into mem_value + from private.memory + where address = read_address; + return mem_value; +end; +$$ language plpgsql; + +create or replace function private.write_memory(write_address integer, new_value integer) + returns void as +$$ +begin + update private.memory + set value = new_value + where address = write_address; +end; +$$ language plpgsql; + +create or replace function private.fetch() + returns void as +$$ +begin + -- fetch the opcode at the current program counter + update private.state + set value_word = + ((select value from private.memory where address = (select value_word from private.state where id = 1)) << 8) + | + (select value from private.memory where address = (select value_word from private.state where id = 1) + 1) + where id = 4; +end; +$$ language plpgsql; + +create or replace function private.increment_program_counter() + returns void as +$$ +begin + update private.state + set value_word = value_word + 2 + where id = 1; +end; +$$ language plpgsql; + +-- section for opcode execution functions + +-- JUMP +create or replace function private.exec_1nnn(op private.opcode_parts) + returns void as +$$ +begin + update private.state + set value_word = op.nnn + where id = 1; -- Program Counter +end; +$$ language plpgsql; + +-- CALL +create or replace function private.exec_2nnn(op private.opcode_parts) + returns void as +$$ +declare + current_pc integer; +begin + select value_word + into current_pc + from private.state + where id = 1; + + perform private.push_stack(current_pc); + + update private.state + set value_word = op.nnn + where id = 1; +end; +$$ language plpgsql; + + +-- RETURN +create or replace function private.exec_group_0(op private.opcode_parts) + returns void as +$$ +begin + if op.opcode = 238 then -- 0x00EE = 238 decimal + update private.state + set value_word = private.pop_stack() + where id = 1; + elsif op.opcode = 224 then -- 0x00E0 = 224 decimal + update private.display + set value = false; + else + raise exception 'Unknown 0-group opcode %', op.opcode; + end if; +end; +$$ language plpgsql; + +-- SET REGISTER +create or replace function private.exec_6xnn(op private.opcode_parts) + returns void as +$$ +begin + update private.registers + set value = op.nn + where id = op.x; +end; +$$ language plpgsql; + +-- ADD TO REGISTER +create or replace function private.exec_7xnn(op private.opcode_parts) + returns void as +$$ +begin + update private.registers + set value = (value + op.nn) % 256 + where id = op.x; +end; +$$ language plpgsql; + +-- SKIP IF EQUAL +create or replace function private.exec_3xnn(op private.opcode_parts) + returns void as +$$ +declare + vx integer; +begin + select value + into vx + from private.registers + where id = op.x; + + if vx = op.nn then + update private.state + set value_word = value_word + 2 + where id = 1; + end if; +end; +$$ language plpgsql; + +-- SKIP IF NOT EQUAL +create or replace function private.exec_4xnn(op private.opcode_parts) + returns void as +$$ +declare + vx integer; +begin + select value + into vx + from private.registers + where id = op.x; + + if vx != op.nn then + update private.state + set value_word = value_word + 2 + where id = 1; + end if; +end; +$$ language plpgsql; + +-- SKIP IF REGISTERS EQUAL +create or replace function private.exec_5xy0(op private.opcode_parts) + returns void as +$$ +declare + vx integer; + vy integer; +begin + select value into vx from private.registers where id = op.x; + select value into vy from private.registers where id = op.y; + + if vx = vy then + update private.state + set value_word = value_word + 2 + where id = 1; + end if; +end; +$$ language plpgsql; + +-- SKIP IF REGISTERS NOT EQUAL +create or replace function private.exec_9xy0(op record) + returns void as +$$ +declare + vx integer; + vy integer; +begin + select value into vx from private.registers where id = op.x; + select value into vy from private.registers where id = op.y; + + if vx <> vy then + update private.state set value_word = value_word + 2 where id = 1; + end if; +end; +$$ language plpgsql; + +-- ARITHMETIC AND BITWISE OPERATIONS +create or replace function private.exec_group_8(op record) + returns void as +$$ +declare + vx integer; + vy integer; + result integer; +begin + select value into vx from private.registers where id = op.x; + select value into vy from private.registers where id = op.y; + + case op.n + when 0 then update private.registers set value = vy where id = op.x; + when 1 then update private.registers set value = (vx | vy) where id = op.x; + when 2 then update private.registers set value = (vx & vy) where id = op.x; + when 3 then update private.registers set value = (vx # vy) where id = op.x; + when 4 then + result := vx + vy; + update private.registers set value = result % 256 where id = op.x; + update private.registers set value = (result > 255)::int where id = 15; + when 5 then + update private.registers set value = (vx - vy + 256) % 256 where id = op.x; + update private.registers set value = (vx >= vy)::int where id = 15; + when 6 then + update private.registers set value = vx >> 1 where id = op.x; + update private.registers set value = (vx & 1) where id = 15; + when 7 then + update private.registers set value = (vy - vx + 256) % 256 where id = op.x; + update private.registers set value = (vy >= vx)::int where id = 15; + when 14 then + update private.registers set value = (vx << 1) % 256 where id = op.x; + update private.registers set value = ((vx & 128) >> 7) where id = 15; + else raise exception 'Unknown 8xy? opcode %', op.opcode; + end case; +end; +$$ language plpgsql; + +-- SET INDEX REGISTER +create or replace function private.exec_annn(op record) + returns void as +$$ +begin + update private.state set value_word = op.nnn where id = 2; +end; +$$ language plpgsql; + +-- JUMP WITH OFFSET +create or replace function private.exec_bnnn(op record) + returns void as +$$ +declare + v0 integer; +begin + select value into v0 from private.registers where id = 0; + + update private.state + set value_word = op.nnn + v0 + where id = 1; +end; +$$ language plpgsql; + +-- RANDOM NUMBER +create or replace function private.exec_cxnn(op record) + returns void as +$$ +declare + rnd integer; +begin + rnd := floor(random() * 256); + update private.registers + set value = (rnd & op.nn) + where id = op.x; +end; +$$ language plpgsql; + +-- INPUT +create or replace function private.exec_group_e(op record) + returns void as +$$ +declare + vx integer; + pressed integer; +begin + select value into vx from private.registers where id = op.x; + select value into pressed from private.inputs where id = vx; + + if op.nn = 158 then -- 9E + if pressed = 1 then + update private.state set value_word = value_word + 2 where id = 1; + end if; + + elsif op.nn = 161 then -- A1 + if pressed = 0 then + update private.state set value_word = value_word + 2 where id = 1; + end if; + + else + raise exception 'Unknown Ex?? opcode %', op.opcode; + end if; +end; +$$ language plpgsql; + +-- FGROUP +create or replace function private.exec_group_f(op record) + returns void as +$$ +declare + vx integer; + i integer; +begin + select value into vx from private.registers where id = op.x; + select value_word into i from private.state where id = 2; + + case op.nn + + when 7 then -- Fx07 + update private.registers + set value = (select value from private.timers where label = 'Delay Timer') + where id = op.x; + + when 15 then -- Fx15 + update private.timers set value = vx where label = 'Delay Timer'; + + when 18 then -- Fx18 + update private.timers set value = vx where label = 'Sound Timer'; + + when 30 then -- Fx1E + update private.state set value_word = i + vx where id = 2; + + when 41 then -- Fx29 + update private.state set value_word = vx * 5 where id = 2; + + when 51 then -- Fx33 (BCD) + update private.memory set value = vx / 100 where address = i; + update private.memory set value = (vx / 10) % 10 where address = i + 1; + update private.memory set value = vx % 10 where address = i + 2; + + when 85 then -- Fx55 + for i in 0..op.x + loop + update private.memory + set value = (select value from private.registers where id = i) + where address = i + i; + end loop; + + when 101 then -- Fx65 + for i in 0..op.x + loop + update private.registers + set value = (select value from private.memory where address = i + i) + where id = i; + end loop; + + else raise exception 'Unknown Fx?? opcode %', op.opcode; + end case; +end; +$$ language plpgsql; + +create or replace function private.exec_dxyn(op record) + returns void as +$$ +declare + vx integer; + vy integer; + i_reg integer; + sprite_row integer; + sprite_byte integer; + x_pos integer; + y_pos integer; + pixel_idx integer; + old_pixel boolean; + collision integer := 0; + row integer; + col integer; +begin + -- fetch registers and I + select value into vx from private.registers where id = op.x; + select value into vy from private.registers where id = op.y; + select value_word into i_reg from private.state where id = 2; + + -- clear VF + update private.registers set value = 0 where id = 15; + + -- for each row of sprite + for row in 0..op.n - 1 + loop + -- read byte from memory + select value into sprite_byte from private.memory where address = i_reg + row; + + -- draw each bit + for col in 0..7 + loop + -- only process if bit is 1 + if ((sprite_byte >> (7 - col)) & 1) = 1 then + -- calculate pixel coordinates (wrap) + x_pos := (vx + col) % 64; + y_pos := (vy + row) % 32; + + pixel_idx := y_pos * 64 + x_pos; + + -- get old pixel + select value into old_pixel from private.display where private.display.i = pixel_idx; + + -- check collision + if old_pixel then + collision := 1; + end if; + + -- XOR pixel + update private.display + set value = not old_pixel + where private.display.i = pixel_idx; + end if; + end loop; + end loop; + + -- set VF if any collision + if collision = 1 then + update private.registers set value = 1 where id = 15; + end if; + + -- mark screen dirty + update private.state + set value = 1 + where id = 5; +end; +$$ language plpgsql; + +-- exit section + +create or replace function private.mark_screen_clean() + returns void as +$$ +begin + update private.state + set value = 0 + where id = 5; +end; +$$ language plpgsql; + +create or replace function private.is_screen_dirty() + returns boolean as +$$ +declare + dirty boolean; +begin + select value::boolean + into dirty + from private.state + where id = 5; + return dirty; +end; +$$ language plpgsql; + +create or replace function private.clock() + returns void as +$$ +declare + op private.opcode_parts; +begin + -- fetch + perform private.fetch(); + perform private.increment_program_counter(); + + -- opcode dispatch + select * + into op + from private.opcode_parts; + + case op.k + when 0 then perform private.exec_group_0(op); + when 1 then perform private.exec_1nnn(op); + when 2 then perform private.exec_2nnn(op); + when 3 then perform private.exec_3xnn(op); + when 4 then perform private.exec_4xnn(op); + when 5 then perform private.exec_5xy0(op); + when 6 then perform private.exec_6xnn(op); + when 7 then perform private.exec_7xnn(op); + when 8 then perform private.exec_group_8(op); + when 9 then perform private.exec_9xy0(op); + when 10 then perform private.exec_annn(op); + when 11 then perform private.exec_bnnn(op); + when 12 then perform private.exec_cxnn(op); + when 13 then perform private.exec_dxyn(op); + when 14 then perform private.exec_group_e(op); + when 15 then perform private.exec_group_f(op); + else raise exception 'Unknown opcode %', op.opcode; + end case; + + -- decrement timers if they are above 0 + update private.timers + set value = value - 1 + where value > 0; +end; +$$ language plpgsql; + + +select private.reset(); + +--select private.clock();
\ No newline at end of file diff --git a/pgch8.csproj b/pgch8.csproj new file mode 100644 index 0000000..c0140e4 --- /dev/null +++ b/pgch8.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Npgsql" Version="10.0.1" /> + </ItemGroup> + +</Project> diff --git a/pgch8.sln b/pgch8.sln new file mode 100644 index 0000000..a3b4880 --- /dev/null +++ b/pgch8.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgch8", "pgch8.csproj", "{A7F7E4C2-2B28-45DC-B503-A395676FA4A3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A7F7E4C2-2B28-45DC-B503-A395676FA4A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7F7E4C2-2B28-45DC-B503-A395676FA4A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7F7E4C2-2B28-45DC-B503-A395676FA4A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7F7E4C2-2B28-45DC-B503-A395676FA4A3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/test_opcode.ch8 b/test_opcode.ch8 Binary files differnew file mode 100644 index 0000000..f540f69 --- /dev/null +++ b/test_opcode.ch8 |
