-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path3dscapture.js
390 lines (275 loc) · 12.3 KB
/
3dscapture.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/* Sudo3DSCapture */
const VID_3DS = 0x16D0;
const PID_3DS = 0x06A3;
const DEFAULT_CONFIGURATION = 1;
const CAPTURE_INTERFACE = 0;
const CAPTURE_ENDPOINT = 0x2;
const CMDOUT_CAPTURE_START = 0x40;
const FRAMEWIDTH = 240;
const FRAMEHEIGHT = 720;
// Size of frame data (3 bytes per pixel)
const FRAMESIZE = FRAMEWIDTH * FRAMEHEIGHT * 3;
// Debug stuff so we can fiddle with params live
// We can specify amount of extra data to pull (includes audio) after FRAMESIZE bytes
var extraDataSize = 256; // 2000;
// frameStartOffset - change where we start reading bytes from in the source data
// 0x45 seems to translate the frame to the correct position most of the time, though the colors are still off
// var frameStartOffset = 0x45; // this is decimal 69
var frameStartOffset = 0;
// We can reduce the furthest offset we read with this.
// This is checked with:
// if ((readPos * 3) + 2 < result.data.byteLength - byteLengthOffset) {
// Setting this to 0x100 (256) wipes out the last line of rapidly shifting colors at the bottom which I think is audio data
// However, we are one line of pixels short - we are missing 720 bytes of frame data as the last line seems transparent.
// I think our missing data might be thrown in an additional bulk_in packet that we're not grabbing.
// We seem to have fixed it but i want to preserve the above notes.
var byteLengthOffset = 0;
// If we change the order of the colors from RGB to BRG, we get the correct color. I think we have a weird issue with our offsets
// and if we solve those we won't have to shift the color order anymore
var rOrder = 0; // normally 0
var gOrder = 1; // normally 1
var bOrder = 2; // normally 2
// If we want to split the screen, we need to read 288000 (240*400*3) bytes for the top screen and 230400 bytes (240*320*3) for the bottom screen
var topScreenSize = 288000;
var bottomScreenSize = 230400;
// Choose how frequently we do the updates (you'll need to stop and start capture for it to take effect)
// 36 seems to minimize the glitchy frames
// 1000/35 is a bit under 30 FPS
// This is one more thing that should be fixable by fixing when we have incomplete frame data
var pollFrequency = 36;
// Split screen off by default
var doSplitScreen = true;
// Hack to skip render if previous data was < 518144
// This won't be needed once we can work out how to pull the entire frame data from multiple packets before rendering
var lastDataPacketTooSmall = false;
// De-asyncing thingy that doesn't seem to do much to help
var droppedFrames = 0;
var lastDrawCompleted = true;
var device, poller;
function pairUSB() {
const filters = [{
vendorId: VID_3DS,
productId: 0x06A3
}];
navigator.usb.requestDevice({
filters: filters
}).then(usbDevice => {
log("Connected: " + usbDevice.productName);
device = usbDevice;
return device.open(); // Begin a session.
}).then(() => device.selectConfiguration(DEFAULT_CONFIGURATION)) // Select configuration #1 for the device.
.then(() => device.claimInterface(CAPTURE_INTERFACE)) // Request exclusive control over interface
.catch(error => {
log(error);
});
}
function queryUSB() {
navigator.usb.getDevices().then(devices => {
log("Total devices: " + devices.length);
devices.forEach(device => {
log("Product name: " + device.productName + ", serial number " + device.serialNumber);
});
});
}
function startCapture() {
if (typeof device != "object") {
pairUSB();
}
lastDrawCompleted = true;
poller = setInterval(getFrame, pollFrequency);
}
function stopCapture() {
clearInterval(poller);
lastDrawCompleted = true;
}
function setFrameDelay(delay) {
pollFrequency = delay;
}
function getBytesIn(endpoint, length) {
return device.transferIn(endpoint, length)
.then(result => { return result })
.catch(error => { console.error("getBytesIn: " + error) });
}
async function getFrame() {
if (!lastDrawCompleted) { return; }
// Send 0x40 to request frame
await device.controlTransferOut({
requestType: 'vendor',
recipient: 'device',
request: CMDOUT_CAPTURE_START,
value: 0x00,
index: 0x00
});
let transferSize;
let bytesNeeded = FRAMESIZE + extraDataSize;
let frameData = new Uint8Array(bytesNeeded);
let bytesIn = 0;
//console.log("we need " + bytesNeeded);
do {
transferSize = ((FRAMESIZE - bytesIn) + 0x1ff) & ~0x1ff;
const result = await getBytesIn(CAPTURE_ENDPOINT, transferSize)
.catch(error => {
console.error("getFrame inner do loop: " + error);
});
//console.log(result.data.byteLength);
if (result.status == "ok") {
for (i = 0; i < result.data.byteLength; i++) {
frameData[bytesIn + i] = result.data.getUint8(i);
}
bytesIn = (bytesIn + result.data.byteLength);
} else {
// something didn't happen right
console.error("error thingy");
}
//} while (bytesIn < bytesNeeded)
} while (false);
if (lastDrawCompleted) {
await writeResult(frameData);
}
}
var topOffset = 239;
var btmOffset = 239;
async function writeResult(result) {
lastDrawCompleted = false;
//console.log(result);
logStatus(result.byteLength + "(" + (result.byteLength / 3) + " out of 172800 pixels)");
//console.log(result.byteLength);
// Hacky if condition that doesn't solve the underlying issue of incomplete/misread frame
// lastDataPacketTooSmall just skips frames after we manage to pick up on the "remaing frame data" bulk_in sent
// All this will be solved once we fix that.
if (result.byteLength >= 518144 && !lastDataPacketTooSmall) {
if (doSplitScreen) {
var topScreen = document.getElementById("topscreen");
var bottomScreen = document.getElementById("bottomscreen");
var topContext = topScreen.getContext('2d');
var bottomContext = bottomScreen.getContext('2d');
//topContext.clearRect(0, 0, topScreen.width, topScreen.height);
//bottomContext.clearRect(0, 0, bottomScreen.width, bottomScreen.height);
// putImageData doesn't work with rotation so we're using a buffer canvas
/*
var bufferTop = document.createElement('canvas');
var bufferBottom = document.createElement('canvas');
bufferTop.width = bufferBottom.width = 240;
bufferTop.height = 400;
bufferBottom.height = 320;
var bufferTopContext = bufferTop.getContext('2d');
var bufferBottomContext = bufferBottom.getContext('2d');
var topImage = bufferTopContext.createImageData(bufferTop.width, bufferTop.height);
var bottomImage = bufferBottomContext.createImageData(bufferBottom.width, bufferBottom.height);
*/
var topImage = topContext.createImageData(topScreen.width, topScreen.height);
var bottomImage = bottomContext.createImageData(bottomScreen.width, bottomScreen.height);
// foreach number of pixels
// Top Screen
for (let y = 0; y < 400; y++) {
for (let x = 0; x < 240; x++) {
const srcPtr = (y * 240) - x + topOffset; // 239
const dstPtr = (x * 400) + y;
topImage.data[4 * dstPtr + 0] = result[3 * srcPtr + 0];
topImage.data[4 * dstPtr + 1] = result[3 * srcPtr + 1];
topImage.data[4 * dstPtr + 2] = result[3 * srcPtr + 2];
topImage.data[4 * dstPtr + 3] = 0xFF;
}
}
// Bottom screen
for (let y = 0; y < 320; y++) {
for (let x = 0; x < 240; x++) {
const srcPtr = (y * 240) - x + btmOffset; // 239
const dstPtr = (x * 320) + y;
bottomImage.data[4 * dstPtr + 0] = result[3 * srcPtr + 288000 + 0];
bottomImage.data[4 * dstPtr + 1] = result[3 * srcPtr + 288000 + 1];
bottomImage.data[4 * dstPtr + 2] = result[3 * srcPtr + 288000 + 2];
bottomImage.data[4 * dstPtr + 3] = 0xFF;
}
}
/*
for (var i = 96000; i < 172800; i++) {
bottomOffset = i - 96000;
bottomImage.data[(4 * bottomOffset) + 0] = result[(3 * i) + 0];
bottomImage.data[(4 * bottomOffset) + 1] = result[(3 * i) + 1];
bottomImage.data[(4 * bottomOffset) + 2] = result[(3 * i) + 2];
bottomImage.data[(4 * bottomOffset) + 3] = 0xFF;
}
*/
// Here is where we would handle any extra data past offset 518400 (including sound data)
/*
bufferTopContext.putImageData(topImage, 0, 0);
bufferBottomContext.putImageData(bottomImage, 0, 0);
// For some reason this is just making things spin wildly, disabled for now
topContext.rotate(-90);
topContext.drawImage(bufferTop, 0, 0);
bottomContext.rotate(-90);
bottomContext.drawImage(bufferBottom, 0, 0);
*/
topContext.putImageData(topImage, 0, 0);
bottomContext.putImageData(bottomImage, 0, 0);
// Rotate and paint to test screens
//var top2 = document.getElementById("top2");
//var bottom2 = document.getElementById("bottom2");
//var top2Context = top2.getContext('2d');
//var bottom2Context = bottom2.getContext('2d');
//top2Context.rotate(-90);
//top2Context.scale(-1,-1);
//top2Context.drawImage(topScreen, 0, 0);
//bottom2Context.rotate(-90);
//bottom2Context.drawImage(bottomScreen, 0, 0);
} else {
var canvas = document.getElementById("screen");
var context = canvas.getContext('2d');
var imageData = context.createImageData(canvas.width, canvas.height);
// foreach number of pixels
for (var i = 0; i < 172800; i++) {
readPos = i + frameStartOffset;
if ((readPos * 3) + 2 < result.byteLength - byteLengthOffset) {
imageData.data[(4 * i) + 0] = result[(3 * readPos) + rOrder]
imageData.data[(4 * i) + 1] = result[(3 * readPos) + gOrder]
imageData.data[(4 * i) + 2] = result[(3 * readPos) + bOrder]
imageData.data[(4 * i) + 3] = 0xFF;
}
}
//for (var i = 518400; i < result.data.byteLength; i++) {
// pcmPlayer.feed(result.data.getUint8(i));
//}
context.putImageData(imageData, 0, 0);
}
} else if (result.data.byteLength >= 518144) {
lastDataPacketTooSmall = false;
} else {
if (!lastDataPacketTooSmall) {
// automatically adjust poll frequency, but only once
if (result.data.byteLength > 0 && !(result.data.byteLength > 4000 && result.data.byteLength < 4500)) {
document.getElementById("frame_delay_select").stepUp();
document.getElementById("frame_delay_select").onchange();
}
} else {
lastDataPacketTooSmall = true;
}
}
lastDrawCompleted = true;
droppedFrames = 0;
}
function toggleSplitScreen() {
doSplitScreen = !doSplitScreen;
if (doSplitScreen) {
document.getElementById("canvas-container").style.display = "none";
document.getElementById("split-canvas-container").style.display = "grid";
} else {
document.getElementById("canvas-container").style.display = "block";
document.getElementById("split-canvas-container").style.display = "none";
}
}
// Debug and logging stuff
function log(string) {
var pre = document.getElementById("output");
pre.innerText = pre.innerText + string + "\n";
}
function logStatus(string) {
var pre = document.getElementById("status");
pre.innerText = string;
}
function buf2hex(buffer) { // buffer is an ArrayBuffer
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
function showTime() {
return new Date().toLocaleString().replace(',', '');
}