Additional input (#3)

* Implemented additional input codes in the emulator

* Explained new inputs in the README

* Added additional inputs to the specification and changed the layout.

* Made a more advanced input-tester example that shows all keys.
This commit is contained in:
JanNeuendorf 2024-12-18 13:20:42 +01:00 committed by GitHub
parent d7740c2e4e
commit 4ba681cc52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 159 additions and 67 deletions

2
Cargo.lock generated
View File

@ -553,7 +553,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "svc16"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"anyhow",
"clap",

View File

@ -1,6 +1,6 @@
[package]
name = "svc16"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = ["Jan Neuendorf"]
description = "An emulator for a simple virtual computer"

View File

@ -68,14 +68,31 @@ The screen has a resolution of $256*256=2^{16}$ pixels. The color of each pixel
### Input
The only supported inputs are the mouse position and the left and right mouse keys.
<div align="left">
<img src="examples/input_tester.gif" alt="Input demo" width="200"/>
</div>
The only supported inputs are the mouse position and a list of eight keys.
These keys are supposed to represent the face buttons of an NES controller.
The codes for the **A** and **B** keys also represent the left and right mouse buttons.
On synchronization the input on the last frame is loaded into the input-buffer.
On synchronization the new input is loaded into the input-buffer.
The *position code* is the index of the pixel, the mouse is currently on.
The *key code* is given by left_mouse+2*right_mouse. So it can have the values 0 1 2 or 3.
The *key code* uses bitflags. We count from the least significant bit.
|Bit|Input Name|Mapping in reference emulator|
|-| -|-|
|0|**A** / Mouse Left| **Space** / Mouse Left|
|1|**B** / Mouse Right| **B** / Mouse Right|
|2|**Up** | **Up** / **W**|
|3|**Down** | **Down** / **S**|
|4|**Left** | **Left** / **A**|
|5|**Right** | **Right** / **D**|
|6|**Select** | **N**|
|7|**Start** | **M**|
There is no guarantee that the inputs are synced on the next frame. Before the first synchronization, the input codes are zero.
### Synchronization

Binary file not shown.

BIN
examples/input_tester.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
examples/input_tester.svc16 Normal file

Binary file not shown.

View File

@ -1,8 +1,11 @@
#import "@preview/colorful-boxes:1.4.0":slanted-colorbox
#let title= "SVC16: A Simple Virtual Computer"
#set text(size: 8pt)
#set page(
paper:"a4",
margin: 60pt,
columns: 1,
margin: (left:40pt,right:40pt,top:60pt,bottom:60pt),
flipped: false,
columns: 2,
numbering: "- 1 -",
foreground: rotate(24deg,
text(48pt, fill: rgb("#ff4e375f"))[
@ -12,15 +15,22 @@ foreground: rotate(24deg,
#let not-specified(txt) = slanted-colorbox(
title: "Not specified",
color: "gray",
color: "gray"
)[#txt]
#show link: underline
#set heading(numbering:"1.1")
#set table(inset:10pt,fill:rgb("#f5f5f5"),stroke:(paint:rgb("#000000"),thickness: 1pt))
#{
= SVC16: A Simple Virtual Computer
set align(center)
text(17pt,emph(title))
}
== Motivation and Goals
= Motivation and Goals
We want to fully specify a very simple virtual computer that can be emulated.
The goal is to recreate the feeling of writing games for a system with very tight hardware constraints without having to deal with the complicated reality of real retro systems.
@ -29,7 +39,7 @@ The instruction set and the design in general are in no way meant to resemble so
It is also not intended to be as simple and elegant as it could possibly be. This might make it easier to explain but harder to develop for.
Since learning about assemblers and compilers is the point, we provide no guidelines on how to build the programs.
=== Reproducibility
== Reproducibility
The biggest secondary goal is to design a system that behaves the same everywhere.
The question of how the emulation is run should never matter for the person writing the program or game.
@ -38,7 +48,7 @@ It also means, that the performance characteristics must be the same.
An emulator can either run the system at the intended speed, or it can not.
== General Principles
= General Principles
Every value is represented as a (little-endian) unsigned 16-bit integer.
That includes numbers, addresses, colors, the instruction pointer and the input.
@ -50,9 +60,9 @@ This includes manipulations of the instruction pointer.
Division by zero crashes the program.
== The Simulated System
= The Simulated System
#figure(
image("sketch.svg", width: 70%),
image("sketch.svg", width: 90%),
caption: [A sketch of all components of the virtual computer.],
) <sketch>
@ -62,7 +72,7 @@ The screen itself has a fixed resolution ($256 times 256$).
The instruction pointer is stored separately.
It always starts at zero.
=== Screen and Colors
== Screen and Colors <screen>
The color of each pixel is represented with 16-bits using `RGB565`.
The coordinate $(x,y)$ of the screen maps to the index $256 y + x$ in the screen buffer.
The coordinate $(0,0)$ is in the upper left-hand corner. Changes to the screen-buffer are not reflected on the screen until the system is synchronized.
@ -73,20 +83,49 @@ The coordinate $(0,0)$ is in the upper left-hand corner. Changes to the screen-b
- It is not fixed what the screen shows before it is first synchronized.
- A cursor can be shown on the window, as long as its position matches the mouse position passed to the system]
=== Input
The only supported inputs are the mouse position and the left and right mouse keys.
This is because it makes it much easier to emulate the system consistently.
The input the system sees is only allowed to change at synchronization.
The input is represented with two values: the *position code* and the *key code*.
The *position code* is the index of the pixel, the mouse is currently on. The *key code* is given by left_mouse+2*right_mouse. So it can have the values 0 1 2 or 3.
== Input
The only supported inputs are the mouse position and a list of eight keys.
These keys are supposed to represent the face buttons of an NES controller.
The codes for the *A* and *B* keys also represent the left and right mouse buttons.
On synchronization the new input is loaded into the input-buffer.
Before the first synchronization, both input codes are zero.
The *position code* is the index of the pixel, the mouse is currently on.
It follows the same convention as the screen index explained in @screen.
=== Synchronization
#let input_table=table(
columns: (auto, auto, auto,auto),
align: horizon,
table.header(
[*Bit*],[*Controller Key*], [*Mouse Key*],[*Suggested Mapping*],
),
[0],[*A*],[Left],[*Space* / Mouse Left],
[1],[*B*],[Right],[*B* / Mouse Right],
[2],[*Up*],[-],[*Up* / *W*],
[3],[*Down*],[-],[*Down* / *S*],
[4],[*Left*],[-],[*Left* / *A*],
[5],[*Right*],[-],[*Right* / *D*],
[6],[*Select*],[-],[*N*],
[7],[*Select*],[-],[*M*],
)
#figure(
input_table,
caption: [The available input codes. We count from the least significant bit.],
) <inputs>
The *key code* uses bitflags.
The bits in @inputs are supposed to indicate if a key is currently pressed (and not if it was just pressed or released). As an example, if only the *A* and the *Down* key are pressed, the key code is equal to the number $2^0+2^3=9$.
#not-specified[
- It is not guaranteed on which frame the virtual machine sees an input activate or deactivate.
- The emulator might not allow arbitrary combinations of buttons to be pressed simultaneously.
]
== Synchronization
When the console executes the *Sync* instruction, the screen buffer is drawn to the screen.
It is not cleared. The system will be put to sleep until the beginning of the next frame.
The targeted timing is 30fps. There is a hard limit of 3000000 instructions per frame.
@ -94,7 +133,7 @@ This means that if the Sync command has not been called for 3000000 instructions
This can mean that an event (like a mouse click) is never handled.
An alternative way to describe it is that the syncing happens automatically every frame and the instructions each take $frac(1,30*3000000)$ seconds. Then the *Sync* command just sleeps until the next frame starts.
== Instruction Set
= Instruction Set
All instructions are 4 values long. A value is, of course, a `u16`.
@ -104,32 +143,44 @@ In the following table, all instructions are listed. `@arg1` refers to the value
When the instruction pointer advances, it does so by four positions.
#table(
columns: (auto, auto, auto,auto),
inset: 11pt,
#let instruction_table=table(
columns: (auto,auto,auto),
align: horizon,
table.header(
[*Opcode*], [*Name*], [*Advances*],[*Effect*],
[*Opcode*], [*Name*],[*Effect*],
),
[0],[*Set*],[yes],[`@arg1=arg2`],
[1],[*GoTo*],[if skipped],[`if(not @arg3){inst_ptr=@arg1+arg2}`],
[2],[*Skip*],[if skipped],[`if(not @arg3){inst_ptr=inst_ptr+4*arg1-4*arg2}`],
[3],[*Add*],[yes],[`@arg3=(@arg1+@arg2)`],
[4],[*Sub*],[yes],[`@arg3=(@arg1-@arg2)`],
[5],[*Mul*],[yes],[`@arg3=(@arg1*@arg2)`],
[6],[*Div*],[yes],[`@arg3=(@arg1/@arg2)`],
[7],[*Cmp*],[yes],[`@arg3=(@arg1<@arg2)` (as unsigned)],
[8],[*Deref*],[yes],[`@arg2=@(@arg1+arg3)`],
[9],[*Ref*],[yes],[`@(@arg1+arg3)=@arg2`],
[10],[*Inst*],[yes],[`@arg1=inst_ptr` (Advances after instruction is run.)],
[11],[*Print*],[yes],[Writes `color=@arg1` to `index=@arg2` of screen buffer.],
[12],[*Read*],[yes],[Copies `index=@arg1` of the screen buffer to `@arg2`.],
[13],[*Band*],[yes],[`@arg3=@arg1&@arg2` (binary and)],
[14],[*Xor*],[yes],[`@arg3=@arg1^@arg2` (binary exclusive or)],
[15],[*Sync*],[yes],[Puts `@arg1=position_code`, `@arg2=key_code` and synchronizes (in that order).],
)
[0],[*Set*],[`@arg1=arg2`],
[1],[*GoTo*],[`if(not @arg3){inst_ptr=@arg1+arg2}`],
[2],[*Skip*],[
```
if(not @arg3){
inst_ptr=inst_ptr+4*arg1-4*arg2
}
```
],
[3],[*Add*],[`@arg3=(@arg1+@arg2)`],
[4],[*Sub*],[`@arg3=(@arg1-@arg2)`],
[5],[*Mul*],[`@arg3=(@arg1*@arg2)`],
[6],[*Div*],[`@arg3=(@arg1/@arg2)`],
[7],[*Cmp*],[`@arg3=(@arg1<@arg2)` (as unsigned)],
[8],[*Deref*],[`@arg2=@(@arg1+arg3)`],
[9],[*Ref*],[`@(@arg1+arg3)=@arg2`],
[10],[*Inst*],[`@arg1=inst_ptr`],
[11],[*Print*],[Writes `color=@arg1` to `index=@arg2` of the screen buffer.],
[12],[*Read*],[Copies `index=@arg1` of the screen buffer to `@arg2`.],
[13],[*Band*],[`@arg3=@arg1&@arg2` (binary and)],
[14],[*Xor*],[`@arg3=@arg1^@arg2` (binary exclusive or)],
[15],[*Sync*],[Puts `@arg1=position_code`, `@arg2=key_code` and synchronizes (in that order).],
)
== Constructing the Program
#figure(
instruction_table,
caption: [The instruction set.],
) <instructions>
Every instruction shown in @instructions advances the instruction pointer by four positions _after_ it is completed. The exceptions to this are the *GoTo* and *Skip* instructions. They only do this, if the condition is _not_ met.
= Constructing the Program
A program is really just the initial state of the main memory.
There is no distinction between memory that contains instructions and memory that contains some other asset.
@ -138,7 +189,7 @@ The maximum size is $2*2^16 upright("bytes") approx 131.1 upright("kB")$.
It can be shorter, in which case the end is padded with zeroes.
The computer will begin by executing the instruction at index 0.
== Handling Exceptions
= Handling Exceptions
There are only two reasons the program can fail (for internal reasons).
- It tries to divide by zero
@ -153,14 +204,15 @@ There is intentionally no way of restarting or even quitting a program from with
- It is not guaranteed that the emulator closes if an exception occurs. (So you can not use it to quit a program.)
]
== Example Program
= Example Program
#[
#show raw: it => block(
fill: rgb("#0000000f"),
fill: rgb("#f5f5f5"),
inset: 10pt,
radius: 4pt,
stroke: (paint: rgb("9e9e9e"),thickness: 2pt),
text(fill: rgb("#000000"), it))
@ -168,16 +220,24 @@ A simple example would be to print all $2^16$ possible colors to the screen.
We make our lives easier, by mapping each index of the screen buffer to the color which is encoded with the index.
Here, we use the names of the opcodes instead of their numbers.
```
Set 501 1 0 // Write the value 1 to address 501
Set 502 65535 0 // Write the largest possible value to 502
Print 500 500 0 // Display color=@500 at screen-index=@500
Add 500 501 500 // Increment the color/screen-index
Cmp 500 502 503 // See if we are not at the max number
Xor 503 501 503 // Negate it
Skip 0 4 503 // Unless we are at the max number, go back 4 instructions
Sync 0 0 0 // Sync
GoTo 0 0 0 // Repeat to keep the window open
```typ
// Write the value 1 to address 501
Set 501 1 0
// Write the largest possible value to 502
Set 502 65535 0
// Display color=@500 at screen-index=@500
Print 500 500 0
// Increment the color/screen-index
Add 500 501 500
// See if we are not at the max number and negate it.
Cmp 500 502 503
Xor 503 501 503
// Unless we are at the max number,
// go back 4 instructions.
Skip 0 4 503
// Sync and repeat.
Sync 0 0 0
GoTo 0 0 0
```
We could rely on the fact that the value at index 500 starts at zero and we did not have to initialize it.
@ -224,7 +284,7 @@ When we run this, we should see the output shown in @colors.
]
== Miscellaneous
= Miscellaneous
Further information, examples and a reference emulator can be found at #link("https://github.com/JanNeuendorf/SVC16").
Everything contained in this project is provided under the _MIT License_.
Do with it whatever you want.

View File

@ -90,15 +90,30 @@ fn get_input_code(window: &Window) -> (u16, u16) {
let mp = window.get_mouse_pos(minifb::MouseMode::Clamp).unwrap();
let pos_code = mp.1 as u16 * 256 + mp.0 as u16;
let mut key_code = 0_u16;
if window.get_mouse_down(minifb::MouseButton::Left)
|| window.is_key_down(Key::A)
|| window.is_key_down(Key::Space)
{
if window.get_mouse_down(minifb::MouseButton::Left) || window.is_key_down(Key::Space) {
key_code += 1;
}
if window.get_mouse_down(minifb::MouseButton::Right) || window.is_key_down(Key::B) {
key_code += 2;
}
if window.is_key_down(Key::Up) || window.is_key_down(Key::W) {
key_code += 4;
}
if window.is_key_down(Key::Down) || window.is_key_down(Key::S) {
key_code += 8;
}
if window.is_key_down(Key::Left) || window.is_key_down(Key::A) {
key_code += 16;
}
if window.is_key_down(Key::Right) || window.is_key_down(Key::D) {
key_code += 32;
}
if window.is_key_down(Key::N) {
key_code += 64;
}
if window.is_key_down(Key::M) {
key_code += 128;
}
(pos_code, key_code)
}