rubyboy 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d92d323c7cf2886b1808eb6e8e2aabb786f5f4a4c66069e2788924ffa7ce72e
4
- data.tar.gz: e0d5c4e1f1039f54283a5cd307c8fabe29f9dc019516501f9075560c16612af4
3
+ metadata.gz: 12916ff3bf0b3e855d767980218887793bfb1206675487d9aeaac191897e8c73
4
+ data.tar.gz: f8fb9559338f9d0804f05906af939dcf2f735df729a30da6becf5db710934ba4
5
5
  SHA512:
6
- metadata.gz: 6960fa54f9150fafd79388eb9dafc9f787188a7ac1b097885ce3c8cbfe59ea7fbac84f3b8d3008f36c818ae3925b8965fbefaf8793d9211b37e8eb214dbbf9ca
7
- data.tar.gz: 814ee8ce6c307828dde433847eabbb6d681c37d7bf424b6a0e90731c5bc3869060bc6baeaa79f15dfe5364532f070e02f97d4fb99844f115e8d341cc678ee0c4
6
+ metadata.gz: 84b0aa4f4731980e90f176a9c03a1aef3b84cdd2ee3ac9f5ead0b0ae286405b582ed5e78fc86169208ba3d6ec620b70d1d68703a77251ae6579675eecdbd6d42
7
+ data.tar.gz: 1ecb47bb5111749e58c24e289275fb4f587f994aec722ed933f555b01d84ee198c0a7c0a65b18fb94e8ef434ea3390bfb5c7382c0e5383fd9496d4ad8f5bb070
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.4.0] - 2024-09-29
4
+
5
+ - Works on browser using ruby.wasm
6
+
3
7
  ## [1.3.2] - 2024-05-04
4
8
 
5
9
  - Revert "Enable YJIT when initialize"
data/docs/favicon.png ADDED
Binary file
data/docs/index.html ADDED
@@ -0,0 +1,75 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>RUBY BOY</title>
5
+ <meta charset="utf-8"/>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="author" content="sacckey" />
8
+ <meta name="description" content="Game Boy emulator written in Ruby, running on ruby.wasm" />
9
+ <meta property="og:url" content="https://sacckey.github.io/rubyboy/" />
10
+ <meta property="og:title" content="RUBY BOY" />
11
+ <meta property="og:description" content="Game Boy emulator written in Ruby, running on ruby.wasm" />
12
+ <meta property="og:image" content="https://sacckey.github.io/rubyboy/ogp.png" />
13
+ <meta name="twitter:card" content="summary_large_image" />
14
+ <meta name="twitter:site" content="@sacckey" />
15
+ <meta name="twitter:creator" content="@sacckey" />
16
+ <link rel="icon" href="./favicon.png">
17
+ <link rel="stylesheet" href="./styles.css">
18
+ <script src="./index.js" defer></script>
19
+ </head>
20
+ <body>
21
+ <div class="main-content">
22
+ <div class="gameboy">
23
+ <div class="screen-container">
24
+ <canvas id="canvas" width="320" height="288"></canvas>
25
+ </div>
26
+ <div class="controls">
27
+ <div class="d-pad">
28
+ <button class="d-pad-button" id="d-pad-left" title="Left" data-code="KeyA"></button>
29
+ <button class="d-pad-button" id="d-pad-right" title="Right" data-code="KeyD"></button>
30
+ <button class="d-pad-button" id="d-pad-up" title="Up" data-code="KeyW"></button>
31
+ <button class="d-pad-button" id="d-pad-down" title="Down" data-code="KeyS"></button>
32
+ </div>
33
+ <div class="buttons">
34
+ <button class="action-button" id="button-a" title="A" data-code="KeyK"></button>
35
+ <button class="action-button" id="button-b" title="B" data-code="KeyJ"></button>
36
+ </div>
37
+ </div>
38
+ <div class="start-select">
39
+ <button class="start-select-button" title="Select" data-code="KeyU"></button>
40
+ <button class="start-select-button" title="Start" data-code="KeyI"></button>
41
+ </div>
42
+ <img src="./logo-light-23.svg" alt="Ruby Boy">
43
+ </div>
44
+ <div class="control-panel">
45
+ <section id="rom-select">
46
+ <h3>ROM Select</h3>
47
+ <select id="rom-select-box" class="rom-select-box" disabled >
48
+ <option value="tobu.gb">Tobu Tobu Girl</option>
49
+ <option value="bgbtest.gb">BGB Test</option>
50
+ </select>
51
+ <label id="rom-upload-button" class="rom-upload-button disabled">
52
+ <input type="file" id="rom-input" accept=".gb" disabled />
53
+ Upload
54
+ </label>
55
+ </section>
56
+ <section id="controls">
57
+ <h3>Controls</h3>
58
+ <div>
59
+ <kbd>w</kbd> <kbd>a</kbd> <kbd>s</kbd> <kbd>d</kbd>: ↑ ← ↓ →
60
+ </div>
61
+ <div>
62
+ <kbd>k</kbd> <kbd>j</kbd> <kbd>i</kbd> <kbd>u</kbd>: A B Start Select
63
+ </div>
64
+ </section>
65
+ <section id="performance">
66
+ <h3>Performance</h3>
67
+ <p>FPS: <span id="fps-display">0</span></p>
68
+ </section>
69
+ </div>
70
+ </div>
71
+ <footer class="footer">
72
+ <p>&copy; 2024 sacckey | <a href="https://github.com/sacckey/rubyboy" target="_blank" rel="noopener noreferrer">GitHub</a></p>
73
+ </footer>
74
+ </body>
75
+ </html>
data/docs/index.js ADDED
@@ -0,0 +1,113 @@
1
+ const worker = new Worker('worker.js', { type: 'module' });
2
+
3
+ const SCALE = 2;
4
+ const canvas = document.getElementById('canvas');
5
+ const canvasContext = canvas.getContext('2d');
6
+ canvasContext.scale(SCALE, SCALE);
7
+ const tmpCanvas = document.createElement('canvas');
8
+ const tmpCanvasContext = tmpCanvas.getContext('2d');
9
+ tmpCanvas.width = canvas.width;
10
+ tmpCanvas.height = canvas.height;
11
+
12
+ // Display "LOADING..."
13
+ (() => {
14
+ const str = `
15
+ 10000 01110 01110 11110 01110 10001 01110 00000 00000 00000
16
+ 10000 10001 10001 10001 00100 11001 10000 00000 00000 00000
17
+ 10000 10001 10001 10001 00100 10101 10011 00000 00000 00000
18
+ 10000 10001 11111 10001 00100 10011 10001 01100 01100 01100
19
+ 11111 01110 10001 11110 01110 10001 01110 01100 01100 01100
20
+ `;
21
+ const dotSize = 2;
22
+ const rows = str.trim().split('\n')
23
+ const xSpacing = canvas.width / (2 * SCALE) - rows[0].length * dotSize / 2;
24
+ const ySpacing = canvas.height / (2 * SCALE) - rows.length * dotSize / 2;
25
+ canvasContext.fillStyle = 'white';
26
+
27
+ rows.forEach((row, y) => {
28
+ [...row.trim()].forEach((char, x) => {
29
+ if (char === '1') {
30
+ canvasContext.fillRect(
31
+ x * dotSize + xSpacing,
32
+ y * dotSize + ySpacing,
33
+ dotSize,
34
+ dotSize
35
+ );
36
+ }
37
+ });
38
+ });
39
+ })();
40
+
41
+ document.addEventListener('keydown', (event) => {
42
+ worker.postMessage({ type: 'keydown', code: event.code });
43
+ });
44
+ document.addEventListener('keyup', (event) => {
45
+ worker.postMessage({ type: 'keyup', code: event.code });
46
+ });
47
+
48
+ const handleButtonPress = (event) => {
49
+ event.preventDefault();
50
+ worker.postMessage({ type: 'keydown', code: event.target.dataset.code });
51
+ }
52
+ const handleButtonRelease = (event) => {
53
+ event.preventDefault();
54
+ worker.postMessage({ type: 'keyup', code: event.target.dataset.code });
55
+ }
56
+ const buttons = document.querySelectorAll('.d-pad-button, .action-button, .start-select-button');
57
+ buttons.forEach(button => {
58
+ button.addEventListener('mousedown', handleButtonPress);
59
+ button.addEventListener('mouseup', handleButtonRelease);
60
+ button.addEventListener('touchstart', handleButtonPress);
61
+ button.addEventListener('touchend', handleButtonRelease);
62
+ });
63
+
64
+ const romInput = document.getElementById('rom-input');
65
+ romInput.addEventListener('change', (event) => {
66
+ const file = event.target.files[0];
67
+ if (file) {
68
+ const reader = new FileReader();
69
+
70
+ reader.onload = (e) => {
71
+ const romData = e.target.result;
72
+ worker.postMessage({ type: 'loadROM', data: romData }, [romData]);
73
+ };
74
+
75
+ reader.readAsArrayBuffer(file);
76
+ }
77
+ });
78
+
79
+ const romSelectBox = document.getElementById('rom-select-box');
80
+ romSelectBox.addEventListener('change', (event) => {
81
+ worker.postMessage({ type: 'loadPreInstalledRom', romName: event.target.value });
82
+ });
83
+
84
+ const times = [];
85
+ const fpsDisplay = document.getElementById('fps-display');
86
+ worker.onmessage = (event) => {
87
+ if (event.data.type === 'pixelData') {
88
+ const pixelData = new Uint8ClampedArray(event.data.data);
89
+ const imageData = new ImageData(pixelData, 160, 144);
90
+ tmpCanvasContext.putImageData(imageData, 0, 0);
91
+ canvasContext.drawImage(tmpCanvas, 0, 0);
92
+
93
+ const now = performance.now();
94
+ while (times.length > 0 && times[0] <= now - 1000) {
95
+ times.shift();
96
+ }
97
+ times.push(now);
98
+ fpsDisplay.innerText = times.length.toString();
99
+ }
100
+
101
+ if (event.data.type === 'initialized') {
102
+ romSelectBox.disabled = false;
103
+ romInput.disabled = false;
104
+ document.getElementById('rom-upload-button').classList.remove('disabled');
105
+ worker.postMessage({ type: 'startRubyboy' });
106
+ }
107
+
108
+ if (event.data.type === 'error') {
109
+ console.error('Error from Worker:', event.data.message);
110
+ }
111
+ };
112
+
113
+ worker.postMessage({ type: 'initRubyboy' });
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="108" height="18" viewBox="0 0 108 18"><path fill="#fff" d="M5.84 10.68L5.00 10.68L4.23 16.94L0.51 16.94L2.55 0.18L7.64 0.18Q9.18 0.18 10.26 0.51Q11.35 0.84 12.04 1.41Q12.73 1.99 13.05 2.78Q13.36 3.56 13.36 4.48L13.36 4.48Q13.36 5.42 13.12 6.24Q12.87 7.07 12.40 7.77Q11.93 8.46 11.24 9.00Q10.56 9.53 9.68 9.88L9.68 9.88Q10.03 10.07 10.33 10.35Q10.64 10.63 10.81 11.04L10.81 11.04L13.25 16.94L9.87 16.94Q9.40 16.94 9.07 16.76Q8.75 16.57 8.63 16.23L8.63 16.23L6.81 11.27Q6.68 10.95 6.47 10.82Q6.26 10.68 5.84 10.68L5.84 10.68ZM7.29 2.98L5.96 2.98L5.32 8.10L6.69 8.10Q7.50 8.10 8.06 7.85Q8.63 7.60 8.98 7.19Q9.33 6.77 9.48 6.22Q9.64 5.67 9.64 5.06L9.64 5.06Q9.64 4.59 9.50 4.20Q9.36 3.82 9.07 3.55Q8.77 3.28 8.33 3.13Q7.89 2.98 7.29 2.98L7.29 2.98ZM20.79 14.00L20.79 14.00Q21.49 14.00 22.07 13.73Q22.64 13.45 23.08 12.95Q23.52 12.45 23.80 11.74Q24.09 11.03 24.20 10.14L24.20 10.14L25.41 0.18L29.16 0.18L27.95 10.14Q27.75 11.67 27.13 12.95Q26.52 14.24 25.54 15.16Q24.55 16.09 23.25 16.61Q21.95 17.12 20.40 17.12L20.40 17.12Q19.00 17.12 17.90 16.70Q16.80 16.27 16.05 15.50Q15.29 14.72 14.90 13.64Q14.51 12.56 14.51 11.26L14.51 11.26Q14.51 10.72 14.58 10.14L14.58 10.14L15.78 0.18L19.53 0.18L18.31 10.14Q18.29 10.35 18.27 10.55Q18.26 10.75 18.26 10.95L18.26 10.95Q18.26 12.37 18.91 13.18Q19.56 14.00 20.79 14.00ZM35.53 16.94L29.28 16.94L31.35 0.18L37.10 0.18Q38.56 0.18 39.60 0.46Q40.64 0.74 41.30 1.25Q41.96 1.76 42.27 2.48Q42.57 3.20 42.57 4.07L42.57 4.07Q42.57 4.76 42.39 5.40Q42.22 6.04 41.83 6.58Q41.45 7.12 40.84 7.56Q40.23 8.00 39.36 8.30L39.36 8.30Q40.72 8.67 41.37 9.45Q42.02 10.22 42.02 11.41L42.02 11.41Q42.02 12.57 41.58 13.58Q41.14 14.58 40.30 15.33Q39.46 16.08 38.25 16.51Q37.05 16.94 35.53 16.94L35.53 16.94ZM36.32 9.79L33.90 9.79L33.37 14.12L35.83 14.12Q36.52 14.12 37.00 13.92Q37.47 13.72 37.76 13.36Q38.05 12.99 38.19 12.51Q38.32 12.03 38.32 11.45L38.32 11.45Q38.32 11.07 38.22 10.76Q38.12 10.45 37.89 10.24Q37.65 10.03 37.27 9.91Q36.88 9.79 36.32 9.79L36.32 9.79ZM34.73 2.98L34.20 7.30L35.99 7.30Q36.63 7.30 37.13 7.18Q37.64 7.05 38.00 6.74Q38.36 6.44 38.56 5.93Q38.75 5.43 38.75 4.68L38.75 4.68Q38.75 3.75 38.27 3.36Q37.78 2.98 36.75 2.98L36.75 2.98L34.73 2.98ZM58.72 0.18L51.85 10.52L51.06 16.94L47.31 16.94L48.10 10.56L43.79 0.18L47.13 0.18Q47.61 0.18 47.88 0.41Q48.15 0.63 48.28 1.00L48.28 1.00L49.89 6.07Q50.05 6.58 50.18 7.05Q50.31 7.52 50.40 7.96L50.40 7.96Q50.62 7.52 50.89 7.06Q51.15 6.59 51.45 6.07L51.45 6.07L54.30 1.00Q54.48 0.69 54.81 0.44Q55.14 0.18 55.61 0.18L55.61 0.18L58.72 0.18ZM68.31 16.94L62.05 16.94L64.12 0.18L69.87 0.18Q71.33 0.18 72.38 0.46Q73.42 0.74 74.08 1.25Q74.74 1.76 75.04 2.48Q75.35 3.20 75.35 4.07L75.35 4.07Q75.35 4.76 75.17 5.40Q74.99 6.04 74.61 6.58Q74.22 7.12 73.61 7.56Q73.00 8.00 72.14 8.30L72.14 8.30Q73.50 8.67 74.15 9.45Q74.80 10.22 74.80 11.41L74.80 11.41Q74.80 12.57 74.35 13.58Q73.91 14.58 73.07 15.33Q72.23 16.08 71.03 16.51Q69.83 16.94 68.31 16.94L68.31 16.94ZM69.09 9.79L66.68 9.79L66.15 14.12L68.61 14.12Q69.30 14.12 69.77 13.92Q70.24 13.72 70.54 13.36Q70.83 12.99 70.96 12.51Q71.09 12.03 71.09 11.45L71.09 11.45Q71.09 11.07 71.00 10.76Q70.90 10.45 70.66 10.24Q70.43 10.03 70.04 9.91Q69.66 9.79 69.09 9.79L69.09 9.79ZM67.50 2.98L66.98 7.30L68.77 7.30Q69.40 7.30 69.91 7.18Q70.41 7.05 70.78 6.74Q71.14 6.44 71.33 5.93Q71.53 5.43 71.53 4.68L71.53 4.68Q71.53 3.75 71.04 3.36Q70.55 2.98 69.53 2.98L69.53 2.98L67.50 2.98ZM92.32 7.49L92.32 7.49Q92.32 8.84 92.01 10.09Q91.69 11.33 91.11 12.40Q90.53 13.47 89.71 14.34Q88.88 15.21 87.86 15.83Q86.84 16.44 85.64 16.78Q84.44 17.12 83.12 17.12L83.12 17.12Q81.47 17.12 80.15 16.54Q78.83 15.96 77.91 14.96Q76.99 13.95 76.50 12.59Q76.02 11.22 76.02 9.65L76.02 9.65Q76.02 8.29 76.33 7.04Q76.65 5.80 77.23 4.73Q77.81 3.66 78.64 2.78Q79.47 1.91 80.49 1.29Q81.51 0.68 82.71 0.34Q83.90-0.00 85.24-0.00L85.24-0.00Q86.88-0.00 88.20 0.58Q89.52 1.16 90.43 2.17Q91.34 3.19 91.83 4.55Q92.32 5.92 92.32 7.49ZM88.48 7.58L88.48 7.58Q88.48 6.54 88.23 5.70Q87.99 4.85 87.51 4.25Q87.03 3.65 86.34 3.32Q85.65 2.99 84.78 2.99L84.78 2.99Q83.63 2.99 82.72 3.47Q81.81 3.94 81.17 4.81Q80.53 5.67 80.20 6.88Q79.86 8.10 79.86 9.57L79.86 9.57Q79.86 10.60 80.10 11.44Q80.34 12.28 80.80 12.88Q81.27 13.48 81.96 13.81Q82.65 14.13 83.54 14.13L83.54 14.13Q84.69 14.13 85.60 13.66Q86.51 13.19 87.15 12.33Q87.79 11.47 88.14 10.26Q88.48 9.05 88.48 7.58ZM107.41 0.18L100.54 10.52L99.75 16.94L96.00 16.94L96.80 10.56L92.48 0.18L95.82 0.18Q96.30 0.18 96.57 0.41Q96.84 0.63 96.97 1.00L96.97 1.00L98.58 6.07Q98.74 6.58 98.87 7.05Q99.00 7.52 99.10 7.96L99.10 7.96Q99.31 7.52 99.58 7.06Q99.84 6.59 100.14 6.07L100.14 6.07L102.99 1.00Q103.17 0.69 103.50 0.44Q103.83 0.18 104.30 0.18L104.30 0.18L107.41 0.18Z"></path></svg>
data/docs/ogp.png ADDED
Binary file
data/docs/styles.css ADDED
@@ -0,0 +1,245 @@
1
+ body {
2
+ background-color: #f0d0ce;
3
+ display: flex;
4
+ margin: 0;
5
+ font-family: 'Helvetica', 'Arial', sans-serif;
6
+ flex-direction: column;
7
+ min-height: 100vh;
8
+ overflow-x: hidden;
9
+ }
10
+
11
+ .main-content {
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ padding: 20px;
16
+ flex-grow: 1;
17
+ width: 100%;
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ .gameboy {
22
+ background-color: #cc342d;
23
+ border-radius: 10px 10px 70px 10px;
24
+ padding: 25px 20px 40px;
25
+ box-shadow: 5px 5px 15px rgba(0,0,0,0.3);
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ width: 100%;
30
+ max-width: 380px;
31
+ justify-content: flex-start;
32
+ margin-bottom: 20px;
33
+ }
34
+
35
+ .screen-container {
36
+ background-color: #f0d0ce;
37
+ border-radius: 10px;
38
+ padding: 20px;
39
+ margin-bottom: 80px;
40
+ width: 100%;
41
+ box-sizing: border-box;
42
+ }
43
+
44
+ #canvas {
45
+ background-color: #881e1a;
46
+ display: block;
47
+ width: 100%;
48
+ height: auto;
49
+ }
50
+
51
+ .controls {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ width: 100%;
55
+ margin-bottom: 60px;
56
+ }
57
+
58
+ .d-pad {
59
+ width: 100px;
60
+ height: 100px;
61
+ position: relative;
62
+ background-color: #881e1a;
63
+ border-radius: 50%;
64
+ }
65
+
66
+ .d-pad-button {
67
+ position: absolute;
68
+ background-color: #550e0c;
69
+ border: none;
70
+ cursor: pointer;
71
+ }
72
+
73
+ #d-pad-left, #d-pad-right {
74
+ width: 30px;
75
+ height: 30px;
76
+ top: 35px;
77
+ }
78
+
79
+ #d-pad-left {
80
+ left: 5px;
81
+ }
82
+
83
+ #d-pad-right {
84
+ right: 5px;
85
+ }
86
+
87
+ #d-pad-up, #d-pad-down {
88
+ width: 30px;
89
+ height: 30px;
90
+ left: 35px;
91
+ }
92
+
93
+ #d-pad-up {
94
+ top: 5px;
95
+ }
96
+
97
+ #d-pad-down {
98
+ bottom: 5px;
99
+ }
100
+
101
+ .buttons {
102
+ background-color: #881e1a;
103
+ border-radius: 50%;
104
+ width: 100px;
105
+ height: 100px;
106
+ position: relative;
107
+ }
108
+
109
+ .action-button {
110
+ position: absolute;
111
+ background-color: #ff6b66;
112
+ border: none;
113
+ border-radius: 50%;
114
+ width: 35px;
115
+ height: 35px;
116
+ cursor: pointer;
117
+ color: #881e1a;
118
+ }
119
+
120
+ #button-a {
121
+ top: 15px;
122
+ right: 15px;
123
+ }
124
+
125
+ #button-b {
126
+ bottom: 15px;
127
+ left: 15px;
128
+ }
129
+
130
+ .start-select {
131
+ display: flex;
132
+ justify-content: center;
133
+ margin-bottom: 50px;
134
+ }
135
+
136
+ .start-select button {
137
+ background-color: #881e1a;
138
+ border: none;
139
+ width: 50px;
140
+ height: 15px;
141
+ margin: 0 15px;
142
+ transform: rotate(-25deg);
143
+ cursor: pointer;
144
+ }
145
+
146
+ .control-panel {
147
+ width: 100%;
148
+ max-width: 500px;
149
+ background-color: #ffffff;
150
+ border-radius: 10px;
151
+ padding: 20px;
152
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
153
+ box-sizing: border-box;
154
+ }
155
+
156
+ .control-panel section {
157
+ margin-bottom: 20px;
158
+ }
159
+
160
+ .rom-upload-button {
161
+ display: inline-block;
162
+ padding: 10px 20px;
163
+ background-color: #cc342d;
164
+ color: white;
165
+ border: none;
166
+ border-radius: 4px;
167
+ font-family: Arial, sans-serif;
168
+ cursor: pointer;
169
+ transition: all 0.3s ease;
170
+ margin-top: 10px;
171
+ margin-left: 0;
172
+ width: 100%;
173
+ box-sizing: border-box;
174
+ text-align: center;
175
+ }
176
+
177
+ .rom-upload-button.disabled {
178
+ background-color: #cccccc;
179
+ color: #666666;
180
+ cursor: not-allowed;
181
+ opacity: 0.7;
182
+ }
183
+
184
+ .rom-upload-button input[type="file"] {
185
+ display: none;
186
+ }
187
+
188
+ .rom-select-box {
189
+ width: 100%;
190
+ margin-bottom: 10px;
191
+ }
192
+
193
+ .footer {
194
+ background-color: #cc342d;
195
+ color: white;
196
+ text-align: center;
197
+ width: 100%;
198
+ box-sizing: border-box;
199
+ margin-top: 20px;
200
+ }
201
+
202
+ kbd {
203
+ background-color: #eee;
204
+ border-radius: 3px;
205
+ border: 1px solid #b4b4b4;
206
+ box-shadow:
207
+ 0 1px 1px rgba(0, 0, 0, 0.2),
208
+ 0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
209
+ color: #333;
210
+ display: inline-block;
211
+ font-size: 0.85em;
212
+ line-height: 1;
213
+ padding: 2px 4px;
214
+ white-space: nowrap;
215
+ font-family: Courier;
216
+ }
217
+
218
+ @media (min-width: 900px) {
219
+ .main-content {
220
+ flex-direction: row;
221
+ justify-content: center;
222
+ align-items: flex-start;
223
+ padding: 50px;
224
+ }
225
+
226
+ .gameboy {
227
+ margin-right: 50px;
228
+ margin-bottom: 0;
229
+ }
230
+
231
+ .control-panel {
232
+ flex: 1;
233
+ }
234
+
235
+ .rom-upload-button {
236
+ width: auto;
237
+ margin-left: 20px;
238
+ margin-top: 0;
239
+ }
240
+
241
+ .rom-select-box {
242
+ width: auto;
243
+ margin-bottom: 0;
244
+ }
245
+ }
data/docs/worker.js ADDED
@@ -0,0 +1,142 @@
1
+ import { RubyVM } from 'https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.6.2/+esm';
2
+ import { Directory, File, OpenDirectory, OpenFile, PreopenDirectory, WASI } from 'https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.3.0/+esm';
3
+
4
+ const DIRECTION_KEY_MASKS = {
5
+ 'KeyD': 0b0001, // Right
6
+ 'KeyA': 0b0010, // Left
7
+ 'KeyW': 0b0100, // Up
8
+ 'KeyS': 0b1000 // Down
9
+ };
10
+
11
+ const ACTION_KEY_MASKS = {
12
+ 'KeyK': 0b0001, // A
13
+ 'KeyJ': 0b0010, // B
14
+ 'KeyU': 0b0100, // Select
15
+ 'KeyI': 0b1000 // Start
16
+ };
17
+
18
+ class Rubyboy {
19
+ constructor() {
20
+ this.wasmUrl = 'https://proxy.sacckey.dev/rubyboy.wasm';
21
+
22
+ const rootContents = new Map();
23
+ rootContents.set('RUBYBOY_TMP', new Directory(new Map()));
24
+ this.rootFs = rootContents;
25
+
26
+ const args = ['ruby.wasm', '-e_=0'];
27
+ this.wasi = new WASI(args, [], [
28
+ new OpenFile(new File([])), // stdin
29
+ new OpenFile(new File([])), // stdout
30
+ new OpenFile(new File([])), // stderr
31
+ new PreopenDirectory('/', rootContents)
32
+ ], {
33
+ debug: false
34
+ });
35
+
36
+ this.directionKey = 0b1111;
37
+ this.actionKey = 0b1111;
38
+ }
39
+
40
+ async init() {
41
+ let response = await fetch('./rubyboy.wasm');
42
+ if (!response.ok) {
43
+ response = await fetch(this.wasmUrl);
44
+ }
45
+
46
+ const buffer = await response.arrayBuffer();
47
+ const imports = {
48
+ wasi_snapshot_preview1: this.wasi.wasiImport,
49
+ };
50
+ const vm = new RubyVM();
51
+ vm.addToImports(imports);
52
+
53
+ const { instance } = await WebAssembly.instantiate(buffer, imports);
54
+ await vm.setInstance(instance);
55
+ this.wasi.initialize(instance);
56
+ vm.initialize();
57
+
58
+ vm.eval(`
59
+ require 'js'
60
+ require_relative 'lib/executor'
61
+
62
+ $executor = Executor.new
63
+ `);
64
+
65
+ this.vm = vm;
66
+ }
67
+
68
+ sendPixelData() {
69
+ this.vm.eval(`$executor.exec(${this.directionKey}, ${this.actionKey})`);
70
+
71
+ const tmpDir = this.rootFs.get('RUBYBOY_TMP');
72
+ const op = new OpenDirectory(tmpDir);
73
+ const result = op.path_lookup('video.data', 0);
74
+ const file = result.inode_obj;
75
+ const bytes = file.data;
76
+
77
+ postMessage({ type: 'pixelData', data: bytes.buffer }, [bytes.buffer]);
78
+ }
79
+
80
+ emulationLoop() {
81
+ this.sendPixelData();
82
+ setTimeout(this.emulationLoop.bind(this), 0);
83
+ }
84
+ }
85
+
86
+ const rubyboy = new Rubyboy();
87
+
88
+ self.addEventListener('message', async (event) => {
89
+ if (event.data.type === 'initRubyboy') {
90
+ try {
91
+ await rubyboy.init();
92
+ postMessage({ type: 'initialized', message: 'ok' });
93
+ } catch (error) {
94
+ postMessage({ type: 'error', message: error.message });
95
+ }
96
+ }
97
+
98
+ if (event.data.type === 'startRubyboy') {
99
+ try {
100
+ rubyboy.emulationLoop();
101
+ } catch (error) {
102
+ postMessage({ type: 'error', message: error.message });
103
+ }
104
+ }
105
+
106
+ if (event.data.type === 'keydown' || event.data.type === 'keyup') {
107
+ const code = event.data.code;
108
+ const directionKeyMask = DIRECTION_KEY_MASKS[code];
109
+ const actionKeyMask = ACTION_KEY_MASKS[code];
110
+
111
+ if (directionKeyMask) {
112
+ if (event.data.type === 'keydown') {
113
+ rubyboy.directionKey &= ~directionKeyMask;
114
+ } else {
115
+ rubyboy.directionKey |= directionKeyMask;
116
+ }
117
+ }
118
+
119
+ if (actionKeyMask) {
120
+ if (event.data.type === 'keydown') {
121
+ rubyboy.actionKey &= ~actionKeyMask;
122
+ } else {
123
+ rubyboy.actionKey |= actionKeyMask;
124
+ }
125
+ }
126
+ }
127
+
128
+ if (event.data.type === 'loadROM') {
129
+ const romFile = new File(new Uint8Array(event.data.data));
130
+ const tmpDir = rubyboy.rootFs.get('RUBYBOY_TMP');
131
+ tmpDir.contents.set('rom.data', romFile);
132
+ rubyboy.vm.eval(`
133
+ $executor.read_rom_from_virtual_fs
134
+ `);
135
+ }
136
+
137
+ if (event.data.type === 'loadPreInstalledRom') {
138
+ rubyboy.vm.eval(`
139
+ $executor.read_pre_installed_rom("${event.data.romName}")
140
+ `);
141
+ }
142
+ });
data/exe/rubyboy-wasm ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ case ARGV[0]
5
+ when 'build'
6
+ command = %w[build --ruby-version 3.3 -o ./docs/ruby-js.wasm]
7
+ when 'pack'
8
+ command = %w[pack ./docs/ruby-js.wasm --dir ./lib::/lib -o ./docs/rubyboy.wasm]
9
+ else
10
+ puts "Invalid argument. Use 'build' or 'pack'."
11
+ exit 1
12
+ end
13
+
14
+ ENV['BUNDLE_ONLY'] = 'wasm'
15
+
16
+ require 'bundler/setup'
17
+ require 'ruby_wasm'
18
+ require 'ruby_wasm/cli'
19
+
20
+ # Exclude all gems except the 'js' gem for packaging
21
+ definition = Bundler.definition
22
+ excluded_gems = definition.resolve.materialize(definition.requested_dependencies).map(&:name)
23
+ excluded_gems -= %w[js]
24
+ RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems)
25
+
26
+ RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr).run(command)
data/lib/executor.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'js'
4
+ require 'json'
5
+
6
+ require_relative 'rubyboy/emulator_wasm'
7
+
8
+ class Executor
9
+ ALLOWED_ROMS = ['tobu.gb', 'bgbtest.gb'].freeze
10
+
11
+ def initialize
12
+ rom_data = File.open('lib/roms/tobu.gb', 'r') { _1.read.bytes }
13
+ @emulator = Rubyboy::EmulatorWasm.new(rom_data)
14
+ end
15
+
16
+ def exec(direction_key = 0b1111, action_key = 0b1111)
17
+ bin = @emulator.step(direction_key, action_key).pack('C*')
18
+ File.binwrite(File.join('/RUBYBOY_TMP', 'video.data'), bin)
19
+ end
20
+
21
+ def read_rom_from_virtual_fs
22
+ rom_path = '/RUBYBOY_TMP/rom.data'
23
+ raise "ROM file not found in virtual filesystem at #{rom_path}" unless File.exist?(rom_path)
24
+
25
+ rom_data = File.open(rom_path, 'rb') { |file| file.read.bytes }
26
+ @emulator = Rubyboy::EmulatorWasm.new(rom_data)
27
+ end
28
+
29
+ def read_pre_installed_rom(rom_name)
30
+ raise 'ROM not found in allowed ROMs' unless ALLOWED_ROMS.include?(rom_name)
31
+
32
+ rom_path = File.join('lib/roms', rom_name)
33
+ rom_data = File.open(rom_path, 'r') { _1.read.bytes }
34
+ @emulator = Rubyboy::EmulatorWasm.new(rom_data)
35
+ end
36
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require_relative 'audio'
4
+ require_relative 'apu_channels/channel1'
5
+ require_relative 'apu_channels/channel2'
6
+ require_relative 'apu_channels/channel3'
7
+ require_relative 'apu_channels/channel4'
8
+
9
+ module Rubyboy
10
+ class ApuWasm
11
+ def initialize
12
+ @audio = nil
13
+ @nr50 = 0
14
+ @nr51 = 0
15
+ @cycles = 0
16
+ @sampling_cycles = 0
17
+ @fs = 0
18
+ @samples = Array.new(1024, 0.0)
19
+ @sample_idx = 0
20
+ @channel1 = ApuChannels::Channel1.new
21
+ @channel2 = ApuChannels::Channel2.new
22
+ @channel3 = ApuChannels::Channel3.new
23
+ @channel4 = ApuChannels::Channel4.new
24
+ end
25
+
26
+ def step(cycles)
27
+ @cycles += cycles
28
+ @sampling_cycles += cycles
29
+
30
+ @channel1.step(cycles)
31
+ @channel2.step(cycles)
32
+ @channel3.step(cycles)
33
+ @channel4.step(cycles)
34
+
35
+ if @cycles >= 0x2000
36
+ @cycles -= 0x2000
37
+
38
+ @channel1.step_fs(@fs)
39
+ @channel2.step_fs(@fs)
40
+ @channel3.step_fs(@fs)
41
+ @channel4.step_fs(@fs)
42
+
43
+ @fs = (@fs + 1) % 8
44
+ end
45
+
46
+ if @sampling_cycles >= 87
47
+ @sampling_cycles -= 87
48
+
49
+ left_sample = (
50
+ @nr51[7] * @channel4.dac_output +
51
+ @nr51[6] * @channel3.dac_output +
52
+ @nr51[5] * @channel2.dac_output +
53
+ @nr51[4] * @channel1.dac_output
54
+ ) / 4.0
55
+
56
+ right_sample = (
57
+ @nr51[3] * @channel4.dac_output +
58
+ @nr51[2] * @channel3.dac_output +
59
+ @nr51[1] * @channel2.dac_output +
60
+ @nr51[0] * @channel1.dac_output
61
+ ) / 4.0
62
+
63
+ raise "#{@nr51} #{@channel4.dac_output}, #{@channel3.dac_output}, #{@channel2.dac_output},#{@channel1.dac_output}" if left_sample.abs > 1.0 || right_sample.abs > 1.0
64
+
65
+ @samples[@sample_idx * 2] = (@nr50[4..6] / 7.0) * left_sample / 8.0
66
+ @samples[@sample_idx * 2 + 1] = (@nr50[0..2] / 7.0) * right_sample / 8.0
67
+ @sample_idx += 1
68
+ end
69
+
70
+ return if @sample_idx < 512
71
+
72
+ @sample_idx = 0
73
+ @audio.queue(@samples)
74
+ end
75
+
76
+ def read_byte(addr)
77
+ case addr
78
+ when 0xff10..0xff14 then @channel1.read_nr1x(addr - 0xff10)
79
+ when 0xff15..0xff19 then @channel2.read_nr2x(addr - 0xff15)
80
+ when 0xff1a..0xff1e then @channel3.read_nr3x(addr - 0xff1a)
81
+ when 0xff1f..0xff23 then @channel4.read_nr4x(addr - 0xff1f)
82
+ when 0xff24 then @nr50
83
+ when 0xff25 then @nr51
84
+ when 0xff26 then (@channel1.enabled ? 0x01 : 0x00) | (@channel2.enabled ? 0x02 : 0x00) | (@channel3.enabled ? 0x04 : 0x00) | (@channel4.enabled ? 0x08 : 0x00) | 0x70 | (@enabled ? 0x80 : 0x00)
85
+ when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)]
86
+ else raise "Invalid APU read at #{addr.to_s(16)}"
87
+ end
88
+ end
89
+
90
+ def write_byte(addr, val)
91
+ return if !@enabled && ![0xff11, 0xff16, 0xff1b, 0xff20, 0xff26].include?(addr) && !(0xff30..0xff3f).include?(addr)
92
+
93
+ val &= 0x3f if !@enabled && [0xff11, 0xff16, 0xff1b, 0xff20].include?(addr)
94
+
95
+ case addr
96
+ when 0xff10..0xff14 then @channel1.write_nr1x(addr - 0xff10, val)
97
+ when 0xff15..0xff19 then @channel2.write_nr2x(addr - 0xff15, val)
98
+ when 0xff1a..0xff1e then @channel3.write_nr3x(addr - 0xff1a, val)
99
+ when 0xff1f..0xff23 then @channel4.write_nr4x(addr - 0xff1f, val)
100
+ when 0xff24 then @nr50 = val
101
+ when 0xff25 then @nr51 = val
102
+ when 0xff26
103
+ flg = val & 0x80 > 0
104
+ if !flg && @enabled
105
+ (0xff10..0xff25).each { |a| write_byte(a, 0) }
106
+ elsif flg && !@enabled
107
+ @fs = 0
108
+ @channel1.wave_duty_position = 0
109
+ @channel2.wave_duty_position = 0
110
+ @channel3.wave_duty_position = 0
111
+ end
112
+ @enabled = flg
113
+ when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)] = val
114
+ else raise "Invalid APU write at #{addr.to_s(16)}"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'apu_wasm'
4
+ require_relative 'bus'
5
+ require_relative 'cpu'
6
+ require_relative 'emulator'
7
+ require_relative 'ppu_wasm'
8
+ require_relative 'rom'
9
+ require_relative 'ram'
10
+ require_relative 'timer'
11
+ require_relative 'joypad'
12
+ require_relative 'interrupt'
13
+ require_relative 'cartridge/factory'
14
+
15
+ module Rubyboy
16
+ class EmulatorWasm
17
+ CPU_CLOCK_HZ = 4_194_304
18
+ CYCLE_NANOSEC = 1_000_000_000 / CPU_CLOCK_HZ
19
+
20
+ def initialize(rom_data)
21
+ rom = Rom.new(rom_data)
22
+ ram = Ram.new
23
+ mbc = Cartridge::Factory.create(rom, ram)
24
+ interrupt = Interrupt.new
25
+ @ppu = PpuWasm.new(interrupt)
26
+ @timer = Timer.new(interrupt)
27
+ @joypad = Joypad.new(interrupt)
28
+ @apu = ApuWasm.new
29
+ @bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu)
30
+ @cpu = Cpu.new(@bus, interrupt)
31
+ end
32
+
33
+ def step(direction_key, action_key)
34
+ @joypad.direction_button(direction_key)
35
+ @joypad.action_button(action_key)
36
+ loop do
37
+ cycles = @cpu.exec
38
+ @timer.step(cycles)
39
+ return @ppu.buffer if @ppu.step(cycles)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class PpuWasm
5
+ attr_reader :buffer
6
+
7
+ MODE = {
8
+ hblank: 0,
9
+ vblank: 1,
10
+ oam_scan: 2,
11
+ drawing: 3
12
+ }.freeze
13
+
14
+ LCDC = {
15
+ bg_window_enable: 0,
16
+ sprite_enable: 1,
17
+ sprite_size: 2,
18
+ bg_tile_map_area: 3,
19
+ bg_window_tile_data_area: 4,
20
+ window_enable: 5,
21
+ window_tile_map_area: 6,
22
+ lcd_ppu_enable: 7
23
+ }.freeze
24
+
25
+ STAT = {
26
+ ly_eq_lyc: 2,
27
+ hblank: 3,
28
+ vblank: 4,
29
+ oam_scan: 5,
30
+ lyc: 6
31
+ }.freeze
32
+
33
+ SPRITE_FLAGS = {
34
+ bank: 3,
35
+ dmg_palette: 4,
36
+ x_flip: 5,
37
+ y_flip: 6,
38
+ priority: 7
39
+ }.freeze
40
+
41
+ LCD_WIDTH = 160
42
+ LCD_HEIGHT = 144
43
+
44
+ OAM_SCAN_CYCLES = 80
45
+ DRAWING_CYCLES = 172
46
+ HBLANK_CYCLES = 204
47
+ ONE_LINE_CYCLES = OAM_SCAN_CYCLES + DRAWING_CYCLES + HBLANK_CYCLES
48
+
49
+ def initialize(interrupt)
50
+ @mode = MODE[:oam_scan]
51
+ @lcdc = 0x91
52
+ @stat = 0x00
53
+ @scy = 0x00
54
+ @scx = 0x00
55
+ @ly = 0x00
56
+ @lyc = 0x00
57
+ @obp0 = 0x00
58
+ @obp1 = 0x00
59
+ @wy = 0x00
60
+ @wx = 0x00
61
+ @bgp = 0x00
62
+ @vram = Array.new(0x2000, 0x00)
63
+ @oam = Array.new(0xa0, 0x00)
64
+ @wly = 0x00
65
+ @cycles = 0
66
+ @interrupt = interrupt
67
+ @buffer = Array.new(144 * 160 * 4, 0xff)
68
+ @bg_pixels = Array.new(LCD_WIDTH, 0x00)
69
+ end
70
+
71
+ def read_byte(addr)
72
+ case addr
73
+ when 0x8000..0x9fff
74
+ @mode == MODE[:drawing] ? 0xff : @vram[addr - 0x8000]
75
+ when 0xfe00..0xfe9f
76
+ @mode == MODE[:oam_scan] || @mode == MODE[:drawing] ? 0xff : @oam[addr - 0xfe00]
77
+ when 0xff40
78
+ @lcdc
79
+ when 0xff41
80
+ @stat | 0x80 | @mode
81
+ when 0xff42
82
+ @scy
83
+ when 0xff43
84
+ @scx
85
+ when 0xff44
86
+ @ly
87
+ when 0xff45
88
+ @lyc
89
+ when 0xff47
90
+ @bgp
91
+ when 0xff48
92
+ @obp0
93
+ when 0xff49
94
+ @obp1
95
+ when 0xff4a
96
+ @wy
97
+ when 0xff4b
98
+ @wx
99
+ end
100
+ end
101
+
102
+ def write_byte(addr, value)
103
+ case addr
104
+ when 0x8000..0x9fff
105
+ @vram[addr - 0x8000] = value if @mode != MODE[:drawing]
106
+ when 0xfe00..0xfe9f
107
+ @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing]
108
+ when 0xff40
109
+ @lcdc = value
110
+ when 0xff41
111
+ @stat = value & 0x78
112
+ when 0xff42
113
+ @scy = value
114
+ when 0xff43
115
+ @scx = value
116
+ when 0xff44
117
+ # ly is read only
118
+ when 0xff45
119
+ @lyc = value
120
+ when 0xff47
121
+ @bgp = value
122
+ when 0xff48
123
+ @obp0 = value
124
+ when 0xff49
125
+ @obp1 = value
126
+ when 0xff4a
127
+ @wy = value
128
+ when 0xff4b
129
+ @wx = value
130
+ end
131
+ end
132
+
133
+ def step(cycles)
134
+ return false if @lcdc[LCDC[:lcd_ppu_enable]] == 0
135
+
136
+ res = false
137
+ @cycles += cycles
138
+
139
+ case @mode
140
+ when MODE[:oam_scan]
141
+ if @cycles >= OAM_SCAN_CYCLES
142
+ @cycles -= OAM_SCAN_CYCLES
143
+ @mode = MODE[:drawing]
144
+ end
145
+ when MODE[:drawing]
146
+ if @cycles >= DRAWING_CYCLES
147
+ render_bg
148
+ render_window
149
+ render_sprites
150
+ @cycles -= DRAWING_CYCLES
151
+ @mode = MODE[:hblank]
152
+ @interrupt.request(:lcd) if @stat[STAT[:hblank]] == 1
153
+ end
154
+ when MODE[:hblank]
155
+ if @cycles >= HBLANK_CYCLES
156
+ @cycles -= HBLANK_CYCLES
157
+ @ly += 1
158
+ handle_ly_eq_lyc
159
+
160
+ if @ly == LCD_HEIGHT
161
+ @mode = MODE[:vblank]
162
+ @interrupt.request(:vblank)
163
+ @interrupt.request(:lcd) if @stat[STAT[:vblank]] == 1
164
+ else
165
+ @mode = MODE[:oam_scan]
166
+ @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
167
+ end
168
+ end
169
+ when MODE[:vblank]
170
+ if @cycles >= ONE_LINE_CYCLES
171
+ @cycles -= ONE_LINE_CYCLES
172
+ @ly += 1
173
+ handle_ly_eq_lyc
174
+
175
+ if @ly == 154
176
+ @ly = 0
177
+ @wly = 0
178
+ handle_ly_eq_lyc
179
+ @mode = MODE[:oam_scan]
180
+ @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
181
+ res = true
182
+ end
183
+ end
184
+ end
185
+
186
+ res
187
+ end
188
+
189
+ def render_bg
190
+ return if @lcdc[LCDC[:bg_window_enable]] == 0
191
+
192
+ y = (@ly + @scy) % 256
193
+ tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00
194
+ tile_map_addr += (y / 8) * 32
195
+ LCD_WIDTH.times do |i|
196
+ x = (i + @scx) % 256
197
+ tile_index = get_tile_index(tile_map_addr + (x / 8))
198
+ pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
199
+ color = get_color(@bgp, pixel)
200
+ base = @ly * LCD_WIDTH * 4 + i * 4
201
+ @buffer[base] = color
202
+ @buffer[base + 1] = color
203
+ @buffer[base + 2] = color
204
+ @bg_pixels[i] = pixel
205
+ end
206
+ end
207
+
208
+ def render_window
209
+ return if @lcdc[LCDC[:bg_window_enable]] == 0 || @lcdc[LCDC[:window_enable]] == 0 || @ly < @wy
210
+
211
+ rendered = false
212
+ y = @wly
213
+ tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00
214
+ tile_map_addr += (y / 8) * 32
215
+ LCD_WIDTH.times do |i|
216
+ next if i < @wx - 7
217
+
218
+ rendered = true
219
+ x = i - (@wx - 7)
220
+ tile_index = get_tile_index(tile_map_addr + (x / 8))
221
+ pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
222
+ color = get_color(@bgp, pixel)
223
+ base = @ly * LCD_WIDTH * 4 + i * 4
224
+ @buffer[base] = color
225
+ @buffer[base + 1] = color
226
+ @buffer[base + 2] = color
227
+ @bg_pixels[i] = pixel
228
+ end
229
+ @wly += 1 if rendered
230
+ end
231
+
232
+ def render_sprites
233
+ return if @lcdc[LCDC[:sprite_enable]] == 0
234
+
235
+ sprite_height = @lcdc[LCDC[:sprite_size]] == 0 ? 8 : 16
236
+ sprites = []
237
+ cnt = 0
238
+
239
+ @oam.each_slice(4) do |y, x, tile_index, flags|
240
+ y = (y - 16) % 256
241
+ x = (x - 8) % 256
242
+ next if y > @ly || y + sprite_height <= @ly
243
+
244
+ sprites << { y:, x:, tile_index:, flags: }
245
+ cnt += 1
246
+ break if cnt == 10
247
+ end
248
+ sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
249
+
250
+ sprites.each do |sprite|
251
+ flags = sprite[:flags]
252
+ pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0 : @obp1
253
+ tile_index = sprite[:tile_index]
254
+ tile_index &= 0xfe if sprite_height == 16
255
+ y = (@ly - sprite[:y]) % 256
256
+ y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1
257
+ tile_index = (tile_index + 1) % 256 if y >= 8
258
+ y %= 8
259
+
260
+ 8.times do |x|
261
+ x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x
262
+
263
+ pixel = get_pixel(tile_index << 4, 7 - x_flipped, (y % 8) * 2)
264
+ i = (sprite[:x] + x) % 256
265
+
266
+ next if pixel == 0 || i >= LCD_WIDTH
267
+ next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0
268
+
269
+ color = get_color(pallet, pixel)
270
+ base = @ly * LCD_WIDTH * 4 + i * 4
271
+ @buffer[base] = color
272
+ @buffer[base + 1] = color
273
+ @buffer[base + 2] = color
274
+ end
275
+ end
276
+ end
277
+
278
+ private
279
+
280
+ def get_tile_index(tile_map_addr)
281
+ tile_index = @vram[tile_map_addr]
282
+ @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index
283
+ end
284
+
285
+ def get_pixel(tile_index, c, r)
286
+ @vram[tile_index + r][c] + (@vram[tile_index + r + 1][c] << 1)
287
+ end
288
+
289
+ def get_color(pallet, pixel)
290
+ case (pallet >> (pixel * 2)) & 0b11
291
+ when 0 then 0xff
292
+ when 1 then 0xaa
293
+ when 2 then 0x55
294
+ when 3 then 0x00
295
+ end
296
+ end
297
+
298
+ def to_signed_byte(byte)
299
+ byte &= 0xff
300
+ byte > 127 ? byte - 256 : byte
301
+ end
302
+
303
+ def handle_ly_eq_lyc
304
+ if @ly == @lyc
305
+ @stat |= 0x04
306
+ @interrupt.request(:lcd) if @stat[STAT[:lyc]] == 1
307
+ else
308
+ @stat &= 0xfb
309
+ end
310
+ end
311
+ end
312
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyboy
4
- VERSION = '1.3.2'
4
+ VERSION = '1.4.0'
5
5
  end
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyboy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sacckey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-04 00:00:00.000000000 Z
11
+ date: 2024-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -35,6 +35,7 @@ email:
35
35
  executables:
36
36
  - rubyboy
37
37
  - rubyboy-bench
38
+ - rubyboy-wasm
38
39
  extensions: []
39
40
  extra_rdoc_files: []
40
41
  files:
@@ -45,9 +46,18 @@ files:
45
46
  - LICENSE.txt
46
47
  - README.md
47
48
  - Rakefile
49
+ - docs/favicon.png
50
+ - docs/index.html
51
+ - docs/index.js
52
+ - docs/logo-light-23.svg
53
+ - docs/ogp.png
54
+ - docs/styles.css
55
+ - docs/worker.js
48
56
  - exe/rubyboy
49
57
  - exe/rubyboy-bench
58
+ - exe/rubyboy-wasm
50
59
  - lib/bench.rb
60
+ - lib/executor.rb
51
61
  - lib/opcodes.json
52
62
  - lib/roms/bgbtest.gb
53
63
  - lib/roms/cpu_instrs/cpu_instrs.gb
@@ -74,6 +84,7 @@ files:
74
84
  - lib/rubyboy/apu_channels/channel2.rb
75
85
  - lib/rubyboy/apu_channels/channel3.rb
76
86
  - lib/rubyboy/apu_channels/channel4.rb
87
+ - lib/rubyboy/apu_wasm.rb
77
88
  - lib/rubyboy/audio.rb
78
89
  - lib/rubyboy/bus.rb
79
90
  - lib/rubyboy/cartridge/factory.rb
@@ -81,10 +92,12 @@ files:
81
92
  - lib/rubyboy/cartridge/nombc.rb
82
93
  - lib/rubyboy/cpu.rb
83
94
  - lib/rubyboy/emulator.rb
95
+ - lib/rubyboy/emulator_wasm.rb
84
96
  - lib/rubyboy/interrupt.rb
85
97
  - lib/rubyboy/joypad.rb
86
98
  - lib/rubyboy/lcd.rb
87
99
  - lib/rubyboy/ppu.rb
100
+ - lib/rubyboy/ppu_wasm.rb
88
101
  - lib/rubyboy/ram.rb
89
102
  - lib/rubyboy/raylib/audio.rb
90
103
  - lib/rubyboy/raylib/lcd.rb
@@ -96,6 +109,7 @@ files:
96
109
  - lib/rubyboy/version.rb
97
110
  - resource/logo/logo.png
98
111
  - resource/logo/logo.svg
112
+ - resource/logo/rubyboy.png
99
113
  - resource/screenshots/pokemon.png
100
114
  - resource/screenshots/puyopuyo.png
101
115
  - sig/rubyboy.rbs