mirror of
https://github.com/JanNeuendorf/SVC16.git
synced 2025-06-03 01:50:18 +00:00
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:
parent
d7740c2e4e
commit
4ba681cc52
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -553,7 +553,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "svc16"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
@ -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"
|
||||
|
25
README.md
25
README.md
@ -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
BIN
examples/input_tester.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 176 KiB |
BIN
examples/input_tester.svc16
Normal file
BIN
examples/input_tester.svc16
Normal file
Binary file not shown.
@ -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.
|
||||
|
23
src/main.rs
23
src/main.rs
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user