rubyboy 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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