/* Von Millhausen's SF2000 Tools Shared JS Library =============================================== There's a bit of overlap between what my SF2000 tools do (e.g., CRC32 recalculation is fairly common, etc.), and there's some general functions that might be useful for future tools as well (e.g., `bisrv.asd` hash checking), so rather than repeating code in each separate tool, any "shared" functions will go here. Just like the tools themselves, this file should be considered CC0 Public Domain (https://creativecommons.org/publicdomain/zero/1.0/) Version 1.6: Added support for the (hopefully not broken) October 13th BIOS in getFirmwareHash() and knownHash() Version 1.5: Added support for the (broken) October 7th BIOS in getFirmwareHash() and knownHash() Version 1.4: Re-ordered the getFirmwareHash() zeroing-out checks to match the expected order to find them in in the BIOS file - just some future-proofing in case some user-substituted content (e.g., a boot logo) happens to contain bytes that a subsequent call to findSequence() might accidentally match. I also added some comments with the rough location of each block within the file, so if I have to add something else in the future I'll be able to slide it into the right place :) Version 1.3: Added support for blanking out the SNES audio bitrate and cycles bits that `bnister` identified as a workaround for the "start-SNES-games- twice" issue that cropped up in firmware versions after March Version 1.2: Added support for blanking out the power curve monitoring bytes in getFirmwareHash(), and updated the hashes accordingly in knownHash() Version 1.1: Added support for the August 3rd BIOS in getFirmwareHash() and knownHash() Version 1.0: Initial version */ // This function takes in a Uint8Array object, and patches bytes 0x18c to // 0x18f with an updated CRC32 calculated on bytes 512 to the end of the // array. Credit to `bnister` for this code! function patchCRC32(data) { var c; var tabCRC32 = new Int32Array(256); for (var i = 0; i < 256; i++) { c = i << 24; for (var j = 0; j < 8; j++) { c = c & (1 << 31) ? c << 1 ^ 0x4c11db7 : c << 1; } tabCRC32[i] = c; } c = ~0; for (var i = 512; i < data.length; i++) { c = c << 8 ^ tabCRC32[c >>> 24 ^ data[i]]; } data[0x18c] = c & 255; data[0x18d] = c >>> 8 & 255; data[0x18e] = c >>> 16 & 255; data[0x18f] = c >>> 24; } // Returns an SHA-256 hash of a given firmware (ignoring common user changes), // or returns false on failure... function getFirmwareHash(data) { // Data should be a Uint8Array, which as an object is passed by reference... // we're going to be manipulating that data before generating our hash, but we // don't want to modify the original object at all... so we'll create a copy, // and work only on the copy... var dataCopy = data.slice(); // Only really worthwhile doing this for big bisrv.asd files... if (dataCopy.length > 12600000) { // First, replace CRC32 bits with 00... dataCopy[0x18C] = 0x00; dataCopy[0x18D] = 0x00; dataCopy[0x18E] = 0x00; dataCopy[0x18F] = 0x00; // Next we'll look for (and zero out) the five bytes that the power // monitoring functions of the SF2000 use for switching the UI's battery // level indicator. These unfortunately can't be searched for - they're just // in specific known locations for specific firmware versions... // Location: Approximately 0x35A8F8 (about 25% of the way through the file) var prePowerCurve = findSequence([0x11, 0x05, 0x00, 0x02, 0x24], dataCopy); if (prePowerCurve > -1) { var powerCurveFirstByteLocation = prePowerCurve + 5; switch (powerCurveFirstByteLocation) { case 0x35A8F8: // Seems to match mid-March layout... dataCopy[0x35A8F8] = 0x00; dataCopy[0x35A900] = 0x00; dataCopy[0x35A9B0] = 0x00; dataCopy[0x35A9B8] = 0x00; dataCopy[0x35A9D4] = 0x00; break; case 0x35A954: // Seems to match April 20th layout... dataCopy[0x35A954] = 0x00; dataCopy[0x35A95C] = 0x00; dataCopy[0x35AA0C] = 0x00; dataCopy[0x35AA14] = 0x00; dataCopy[0x35AA30] = 0x00; break; case 0x35C78C: // Seems to match May 15th layout... dataCopy[0x35C78C] = 0x00; dataCopy[0x35C794] = 0x00; dataCopy[0x35C844] = 0x00; dataCopy[0x35C84C] = 0x00; dataCopy[0x35C868] = 0x00; break; case 0x35C790: // Seems to match May 22nd layout... dataCopy[0x35C790] = 0x00; dataCopy[0x35C798] = 0x00; dataCopy[0x35C848] = 0x00; dataCopy[0x35C850] = 0x00; dataCopy[0x35C86C] = 0x00; break; case 0x3564EC: // Seems to match August 3rd layout... dataCopy[0x3564EC] = 0x00; dataCopy[0x3564F4] = 0x00; dataCopy[0x35658C] = 0x00; dataCopy[0x356594] = 0x00; dataCopy[0x3565B0] = 0x00; break; case 0x356638: // Seems to match October 7th/13th layout... dataCopy[0x356638] = 0x00; dataCopy[0x356640] = 0x00; dataCopy[0x3566D8] = 0x00; dataCopy[0x3566E0] = 0x00; dataCopy[0x3566FC] = 0x00; break; default: return false; } } else { return false; } // Next identify the emulator button mappings (if they exist), and blank // them out too... // Location: Approximately 0x8D6200 (about 75% of the way through the file) var preButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x71, 0xDB, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], dataCopy); if (preButtonMapOffset > -1) { var postButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00], dataCopy, preButtonMapOffset); if (postButtonMapOffset > -1) { for (var i = preButtonMapOffset + 16; i < postButtonMapOffset; i++) { dataCopy[i] = 0x00; } } else { return false; } } else { return false; } // Next identify the boot logo position, and blank it out too... // Location: Approximately 0x9B3520 (about 80% of the way through the file) var badExceptionOffset = findSequence([0x62, 0x61, 0x64, 0x5F, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00], dataCopy); if (badExceptionOffset > -1) { var bootLogoStart = badExceptionOffset + 16; for (var i = bootLogoStart; i < (bootLogoStart + 204800); i++) { dataCopy[i] = 0x00; } } else { return false; } // Next we'll look for and zero out the bytes used for SNES audio rate and // CPU cycles, in case folks want to patch those bytes to correct SNES // first-launch issues on newer firmwares... // Location: Approximately 0xC0A170 (about 99% of the way through the file) var preSNESBytes = findSequence([0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80], dataCopy); if (preSNESBytes > -1) { var snesAudioBitrateBytes = preSNESBytes + 8; var snesCPUCyclesBytes = snesAudioBitrateBytes + 8; dataCopy[snesAudioBitrateBytes] = 0x00; dataCopy[snesAudioBitrateBytes + 1] = 0x00; dataCopy[snesCPUCyclesBytes] = 0x00; dataCopy[snesCPUCyclesBytes + 1] = 0x00; } else { return false; } // If we're here, we've zeroed-out all of the bits of the firmware that are // semi-user modifiable (CRC32 bits, boot logo, button mappings and power // curve bytes); now we can generate a hash of what's left and compare it // against some known values... return crypto.subtle.digest("SHA-256", dataCopy.buffer) .then(function(digest) { var array = Array.from(new Uint8Array(digest)); var hash = array.map(byte => ("00" + byte.toString(16)).slice(-2)).join(""); return hash; }) .catch(function(error) { return false; }); } else { return false; } } // This function searches for array needle in array haystack starting at offset // and returns the zero-based index of the first match found, or -1 if not // found... function findSequence(needle, haystack, offset) { // If offset is not provided, default to 0... offset = offset || 0; // Loop through the haystack array starting from the offset... for (var i = offset; i < haystack.length - needle.length + 1; i++) { // Assume a match until proven otherwise... var match = true; // Loop through the needle array and compare each byte... for (var j = 0; j < needle.length; j++) { if (haystack[i + j] !== needle[j]) { // Mismatch found, break the inner loop and continue the outer loop... match = false; break; } } // If match is still true after the inner loop, we have found a match; // return the index of the start of the match... if (match) { return i; } } // If we reach this point, no match was found... return -1; } // Generic download function, for sending data to the user's browser as a // download... function downloadToBrowser(data, type, name) { // Send the data to the user's browser as a file download... var link = document.createElement("a"); link.href = window.URL.createObjectURL(new Blob([data], {type: type})); link.download = name; link.style.display = "none"; document.body.appendChild(link); link.click(); window.URL.revokeObjectURL(link.href); document.body.removeChild(link); } // This simple function takes in a string representing an SHA1 hash, and // compares it against the known hashes my tools use for the various stock // firmware versions; returns a string with my internal "MM.DD" firmware naming // convention, or false if the provided hash doesn't match... function knownHash(hash) { switch (hash) { case "149706c009c446267e767313e149adc733d167d25e731694b2bdb1646a41ed08": return "03.15"; case "151d5eeac148cbede3acba28823c65a34369d31b61c54bdd8ad049767d1c3697": return "04.20"; case "ab0ce4923086afc535154023ddea1d355bcedb89e6314a47d9c1b77c7a9e75e3": return "05.15"; case "67c5dfc5825a0d9cf953206c2231b29512482e97fef688fe32bf5c31acdb370a": return "05.22"; case "5335860d13214484eeb1260db8fe322efc87983b425ac5a5f8b0fcdf9588f40a": return "08.03"; case "b88458bf2c25d3a34ab57ee149f36cfdc6b8a5138d5c6ed147fbea008b4659db": return "10.07"; case "08bd07ab3313e3f00b922538516a61b5846cde34c74ebc0020cd1a0b557dd54b": return "10.13"; default: return false; } } // Takes in an ImageData object, and returns a Uint8Array object containing the // data in little-endian RGB565 format... function imageDataToRgb565(input) { // Loop through the image data, and convert it to little-endian RGB565. First, // we'll store the raw RGB565-converted integers in an array, one entry per // pixel... var intArray = []; var pixelCount = 0; for (var i = 0; i < input.data.length; i += 4){ // Read in the raw source RGB colours from the image data stream... var red = input.data[i]; var green = input.data[i+1]; var blue = input.data[i+2]; // Use some shifting and masking to get a big-endian version of the RGB565 // colour and store it in our array before moving on... intArray[pixelCount] = ((red & 248)<<8) + ((green & 252)<<3) + (blue>>3); pixelCount++; } // Create a data buffer and a data view; we'll use the view to convert our int // array data to little-endian format (the "true" below) to be stored in the // buffer... var buffer = new ArrayBuffer(intArray.length * 2); var dataView = new DataView(buffer); for (var i = 0; i < intArray.length; i++) { dataView.setInt16(i * 2, intArray[i], true); } // Use the buffer to fill a Uint8Array, which we'll return... return new Uint8Array(buffer); } // Takes in an ImageData object, and returns a Uint8Array object containing the // data in BGRA format... function imageDataToBgra(input) { // This is pretty simple - we just loop through the input data (which is in // RGBA format), and swap the Red and Blue channels to output BGRA instead... output = new Uint8Array(input.data.length); for (var i = 0; i < input.data.length; i += 4) { output[i] = input.data[i + 2]; output[i + 1] = input.data[i + 1]; output[i + 2] = input.data[i]; output[i + 3] = input.data[i + 3]; } return output; } // Takes in a Uint8Array object containing little-endian RGB565 image data, a // width and a height, and outputs an ImageData object... function rgb565ToImageData(input, width, height) { // Create an output ImageData object of the specified dimensions; it'll // default to transparent black, but we'll fill it with our input data... output = new ImageData(width, height); outputIndex = 0; for (var i = 0; i < input.length; i += 2) { // Check to make sure we haven't run out of space in our output buffer... if (outputIndex < output.data.length) { // Read in two bytes, representing one RGB565 pixel in little-endian // format... var byte1 = input[i]; var byte2 = input[i + 1]; // Extract the red, green and blue components from them. The first five // bits of byte2 are red, the first three bits of byte1 and the last // three bits of byte 2 are green, and the last five bits of byte1 are // blue... var red = (byte2 & 0b11111000) >> 3; var green = ((byte1 & 0b11100000) >> 5) | ((byte2 & 0b00000111) << 3); var blue = byte1 & 0b00011111; // These values are in 5-bit/6-bit ranges; we need to scale them to 8-bit // ranges for the colours to look right... red = Math.round(red * 255 / 31); green = Math.round(green * 255 / 63); blue = Math.round(blue * 255 / 31); // Finally, store the RGB values in our ImageData's data array, being // sure to set the A component to 255... output.data[outputIndex] = red; output.data[outputIndex + 1] = green; output.data[outputIndex + 2] = blue; output.data[outputIndex + 3] = 255; outputIndex += 4; } else { // Oops, we've run out of room in our output data buffer; no point in // trying to process any more input data! break; } } // If we've run out of input data, but haven't reached the end of our // ImageData object's buffer, fill the rest of that buffer with white... while (outputIndex / 4 < width * height) { output.data[outputIndex] = 255; output.data[outputIndex + 1] = 255; output.data[outputIndex + 2] = 255; output.data[outputIndex + 3] = 255; outputIndex += 4; } // Finally, return our ImageData object... return output; } // Takes in a Uint8Array object containing raw BGRA image data, a width and a // height, and outputs an ImageData object... function bgraToImageData(input, width, height) { // Create an output ImageData object of the specified dimensions; it'll // default to transparent black, but we'll fill it with our input data... output = new ImageData(width, height); outputIndex = 0; for (var i = 0; i < input.length; i += 4) { // Check to make sure we haven't run out of space in our output buffer... if (outputIndex < output.data.length) { // The input data is *nearly* RGBA as it is - it's just that the R and B // colour channels are swapped... so, just swap them back! output.data[i] = input[i + 2]; output.data[i + 1] = input[i + 1]; output.data[i + 2] = input[i]; output.data[i + 3] = input[i + 3]; outputIndex += 4; } else { // Oops, we've run out of room in our output data buffer; no point in // trying to process any more input data! break; } } // If we've run out of input data, but haven't reached the end of our // ImageData object's buffer, fill the rest of that buffer with white... while (outputIndex / 4 < width * height) { output.data[outputIndex] = 255; output.data[outputIndex + 1] = 255; output.data[outputIndex + 2] = 255; output.data[outputIndex + 3] = 255; outputIndex += 4; } // Finally, return our ImageData object... return output; } // This function takes in a Javascript Image object, and outputs it as an // ImageData object instead... function imageToImageData(image) { // Create a virtual canvas, and load it up with our image file... var canvas = document.createElement("canvas"); var context = canvas.getContext("2d"); canvas.width = image.width; canvas.height = image.height; // Draw our image to the canvas, which will allow us to get data about the // image... context.drawImage(image, 0, 0, image.width, image.height); var imageData = context.getImageData(0, 0, image.width, image.height); // Return the ImageData object... return imageData; } // This function takes in an ImageData object, and returns a new ImageData // object containing a scaled version of the original image. The new image's // width, height and scaling method are also specified... function scaleImage(input, newWidth, newHeight, method) { // Utility function which takes in an ImageData object, and returns // a (partially) upscaled version using bilinear filtering... function _bilinear(imageData, newWidth, newHeight) { var canvas = document.createElement("canvas"); var context = canvas.getContext("2d"); canvas.width = imageData.width; canvas.height = imageData.height; context.putImageData(imageData, 0, 0); var outCanvas = document.createElement("canvas"); var outContext = outCanvas.getContext("2d"); outContext.imageSmoothingEnabled = true; outContext.imageSmoothingQuality = "high"; outCanvas.width = newWidth; outCanvas.height = newHeight; outContext.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, newWidth, newHeight); return outContext.getImageData(0, 0, newWidth, newHeight); } // Utility function which takes in an ImageData object, and returns // a (partially) downscaled version using gaussian resampling... // [Side note: I asked Bing AI for this function; ain't technology grand!] function _gaussian(imageData, newWidth, newHeight) { // Get the original width and height of the image data let oldWidth = imageData.width; let oldHeight = imageData.height; // Get the pixel data array of the image data let oldData = imageData.data; // Create a new pixel data array for the scaled image data let newData = new Uint8ClampedArray(newWidth * newHeight * 4); // Calculate the scaling factor along each axis let scaleX = newWidth / oldWidth; let scaleY = newHeight / oldHeight; // Calculate the radius of the Gaussian kernel based on the scaling factor let radiusX = Math.ceil(1 / scaleX); let radiusY = Math.ceil(1 / scaleY); // Calculate the size of the Gaussian kernel along each axis let sizeX = radiusX * 2 + 1; let sizeY = radiusY * 2 + 1; // Create a Gaussian kernel array let kernel = new Float32Array(sizeX * sizeY); // Calculate the standard deviation of the Gaussian distribution based on // the radius let sigmaX = radiusX / 3; let sigmaY = radiusY / 3; // Calculate the inverse of the variance of the Gaussian distribution along // each axis let invVarX = 1 / (2 * sigmaX * sigmaX); let invVarY = 1 / (2 * sigmaY * sigmaY); // Calculate the normalization factor for the Gaussian kernel let norm = Math.sqrt(2 * Math.PI * sigmaX * sigmaY); // Loop through each element in the Gaussian kernel array for (let ky = -radiusY; ky <= radiusY; ky++) { for (let kx = -radiusX; kx <= radiusX; kx++) { // Calculate the index of the element in the Gaussian kernel array let k = (ky + radiusY) * sizeX + (kx + radiusX); // Calculate the value of the element using the Gaussian formula kernel[k] = Math.exp(-(kx * kx) * invVarX - (ky * ky) * invVarY) / norm; } } // Loop through each pixel in the new image data for (let y = 0; y < newHeight; y++) { for (let x = 0; x < newWidth; x++) { // Calculate the corresponding coordinates in the old image data let oldX = x / scaleX; let oldY = y / scaleY; // Initialize the RGBA values of the pixel to zero let r7 = 0; let g7 = 0; let b7 = 0; let a7 = 0; // Initialize the sum of the kernel values to zero let sum = 0; // Loop through each element in the Gaussian kernel array for (let ky = -radiusY; ky <= radiusY; ky++) { for (let kx = -radiusX; kx <= radiusX; kx++) { // Calculate the index of the element in the Gaussian kernel array let k = (ky + radiusY) * sizeX + (kx + radiusX); // Get the value of the element in the Gaussian kernel array let w = kernel[k]; // Calculate the coordinates of the pixel in the old image data that // corresponds to this element let x1 = Math.round(oldX + kx); let y1 = Math.round(oldY + ky); // Clamp the coordinates to the valid range x1 = Math.max(0, Math.min(x1, oldWidth - 1)); y1 = Math.max(0, Math.min(y1, oldHeight - 1)); // Get the index of the pixel in the old pixel data array let i1 = (y1 * oldWidth + x1) * 4; // Get the RGBA values of the pixel in the old pixel data array let r1 = oldData[i1]; let g1 = oldData[i1 + 1]; let b1 = oldData[i1 + 2]; let a1 = oldData[i1 + 3]; // Multiply the RGBA values by the kernel value and add them to the // pixel values r7 += r1 * w; g7 += g1 * w; b7 += b1 * w; a7 += a1 * w; // Add the kernel value to the sum sum += w; } } // Divide the RGBA values by the sum to get an average value r7 /= sum; g7 /= sum; b7 /= sum; a7 /= sum; // Round the RGBA values to integers r7 = Math.round(r7); g7 = Math.round(g7); b7 = Math.round(b7); a7 = Math.round(a7); // Get the index of the pixel in the new pixel data array let j = (y * newWidth + x) * 4; // Set the RGBA values of the pixel in the new pixel data array newData[j] = r7; newData[j + 1] = g7; newData[j + 2] = b7; newData[j + 3] = a7; } } // Create and return a new ImageData object with the new pixel data array // and dimensions return new ImageData(newData, newWidth, newHeight); } // Get the original width and height... var width = input.width; var height = input.height; // Before we consider doing *any* scaling, let's check to make sure the new // dimensions are different from the old ones; if they're not, there's no // point in doing any scaling! if (width == newWidth && height == newHeight) { return input; } // If we're here, then we're really scaling; the process to follow is // *heavily* dependent upon the provided method, so let's switch based on // that... switch (method) { // If the method is "Nearest Neighbour"... case "Nearest Neighbour": // Create a new canvas element to draw the scaled image (we'll use the // canvas to get our output ImageData object)... var canvas = document.createElement("canvas"); var context = canvas.getContext("2d"); // Set the canvas size to the new dimensions... canvas.width = newWidth; canvas.height = newHeight; // Create a new image data object to store the scaled pixel data... var scaledData = context.createImageData(newWidth, newHeight); // Loop through each pixel of the new image... for (var y = 0; y < newHeight; y++) { for (var x = 0; x < newWidth; x++) { // Calculate the index of the new pixel in the scaled data array... var index = (y * newWidth + x) * 4; // Calculate the x and y coordinates of the corresponding pixel in // the original image... var x2 = Math.floor(x * width / newWidth); var y2 = Math.floor(y * height / newHeight); // Calculate the index of the original pixel in the data array... var index2 = (y2 * width + x2) * 4; // Copy the color values from the original pixel to the new pixel... scaledData.data[index] = input.data[index2]; // Red scaledData.data[index + 1] = input.data[index2 + 1]; // Green scaledData.data[index + 2] = input.data[index2 + 2]; // Blue scaledData.data[index + 3] = input.data[index2 + 3]; // Alpha } } // Finally, return the scaled ImageData object... return scaledData; // If the method is "Bilinear"... case "Bilinear": // OK, "Bilinear" is a bit of a lie... bilinear filtering is fine when // you're *upscaling* an image, but if you're *downscaling* an image // by more than half the original's width/height, then true bilinear // filtering creates just as much of an aliased mess as nearest // neighbour filtering. Most image editing apps therefore cheat and // use a resampling algorithm when downscaling, and bilinear filtering // when upscaling... so that's what we're going to do here too! We'll // use gaussian resampling for any image axis that's being downscaled, // and bilinear for any axis that's being upscaled; this should give // the user a result that's much closer to what they'd expect to see... // Let's see which kind of scaling scenario we're in... if (newWidth > width && newHeight > height) { // All dimensions being upscaled, so we'll use bilinear filtering for // everything... return _bilinear(input, newWidth, newHeight); } else if (newWidth < width && newHeight < height) { // All dimensions being downscaled, so we'll use gaussian resampling // for everything... return _gaussian(input, newWidth, newHeight); } else { // It's a mix! if (newWidth < width) { // Gaussian for width, bilinear for height... let partial = _gaussian(input, newWidth, height); return _bilinear(partial, newWidth, newHeight); } else if (newHeight < height) { // Gaussian for height, bilinear for width... let partial = _gaussian(input, width, newHeight); return _bilinear(partial, newWidth, newHeight); } } break; } } // This utility function is used for adding info, warning and error messages to // the appropriate spots in my tools. It uses custom-modified versions of the // SVG Famfamfam Silk icon set via https://github.com/frhun/silk-icon-scalable function setMessage(type, divID, text) { var icon; switch(type) { case "info": icon = ''; break; case "warning": icon = ''; break; case "error": icon = ''; break; } document.getElementById(divID).innerHTML = "
" + icon + " " + text + "
"; return; }