public GLBitmapAtlas CreateBitmapAtlasFromResources(string[] names) { var bitmaps = new Bitmap[names.Length]; var elementSizeX = 0; var elementSizeY = 0; for (int i = 0; i < names.Length; i++) { var bmp = LoadBitmapFromResourceWithScaling(names[i]); elementSizeX = Math.Max(elementSizeX, bmp.Width); elementSizeY = Math.Max(elementSizeY, bmp.Height); bitmaps[i] = bmp; } var elementsPerRow = MaxAtlasResolution / elementSizeX; var numRows = Utils.DivideAndRoundUp(names.Length, elementsPerRow); var atlasSizeX = elementsPerRow * elementSizeX; var atlasSizeY = numRows * elementSizeY; var textureId = CreateEmptyTexture(atlasSizeX, atlasSizeY, true); var elementRects = new Rectangle[names.Length]; GLES11.GlBindTexture(GLES11.GlTexture2d, textureId); for (int i = 0; i < names.Length; i++) { var bmp = bitmaps[i]; var row = i / elementsPerRow; var col = i % elementsPerRow; elementRects[i] = new Rectangle( col * elementSizeX, row * elementSizeY, bmp.Width, bmp.Height); GLUtils.TexSubImage2D(GLES11.GlTexture2d, 0, elementRects[i].X, elementRects[i].Y, bmp); bmp.Recycle(); } return(new GLBitmapAtlas(textureId, atlasSizeX, atlasSizeY, elementRects, true, true)); }
const int MaxDpcmSize = 0x2c00; // 11KB public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal) { #if !DEBUG try #endif { if (songIds.Length == 0) { return(false); } Debug.Assert(!originalProject.UsesAnyExpansionAudio || !pal); if (songIds.Length > MaxSongs) { Array.Resize(ref songIds, MaxSongs); } var project = originalProject.DeepClone(); project.DeleteAllSongsBut(songIds); project.SetExpansionAudioMask(ExpansionType.NoneMask); var headerBytes = new byte[RomHeaderLength]; var codeBytes = new byte[RomCodeAndTocSize + RomTileSize]; // Load ROM header (16 bytes) + code/tiles (12KB). string romName = "FamiStudio.Rom.rom"; if (project.UsesFamiTrackerTempo) { romName += "_famitracker"; } romName += pal ? "_pal" : "_ntsc"; romName += ".nes"; var romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName); romBinStream.Read(headerBytes, 0, RomHeaderLength); romBinStream.Seek(-RomCodeAndTocSize - RomTileSize, SeekOrigin.End); romBinStream.Read(codeBytes, 0, RomCodeAndTocSize + RomTileSize); Log.LogMessage(LogSeverity.Info, $"ROM code and graphics size: {codeBytes.Length} bytes."); // Build project info + song table of content. var projectInfo = BuildProjectInfo(songIds, name, author); var songTable = BuildSongTableOfContent(project); // Gathersong data. var songBanks = new List <List <byte> >(); // Export each song individually, build TOC at the same time. for (int i = 0; i < project.Songs.Count; i++) { if (i == MaxSongs) { Log.LogMessage(LogSeverity.Warning, $"Too many songs. There is a hard limit of {MaxSongs} at the moment. Ignoring any extra songs."); break; } var song = project.Songs[i]; var songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, RomSongDataStart, RomDpcmStart, pal ? MachineType.PAL : MachineType.NTSC); if (songBytes.Length > MaxSongSize) { Log.LogMessage(LogSeverity.Warning, $"Song {song.Name} has a size of {songBytes.Length}, which is larger than the maximum allowed for ROM export ({MaxSongSize}). Truncating."); Array.Resize(ref songBytes, MaxSongSize); } var numBanks = Utils.DivideAndRoundUp(songBytes.Length, RomBankSize); Debug.Assert(numBanks <= 2); var songBank = songBanks.Count; var songAddr = RomSongDataStart; // If single bank, look for an existing bank with some free space at the end. if (numBanks == 1) { var foundExistingBank = false; for (int j = 0; j < songBanks.Count; j++) { var freeSpace = RomBankSize - songBanks[j].Count; if (songBytes.Length <= freeSpace) { songBank = j; songAddr = RomSongDataStart + songBanks[j].Count; songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, songAddr, RomDpcmStart, pal ? MachineType.PAL : MachineType.NTSC); Debug.Assert(songBytes.Length <= freeSpace); foundExistingBank = true; break; } } // No free space found, allocation a new partial bank. if (!foundExistingBank) { songBanks.Add(new List <byte>()); } songBanks[songBank].AddRange(songBytes); } else { // When a song uses 2 banks, allocate a new full one and a partial one. var bank0 = new List <byte>(); var bank1 = new List <byte>(); for (int j = 0; j < RomBankSize; j++) { bank0.Add(songBytes[j]); } for (int j = RomBankSize; j < songBytes.Length; j++) { bank1.Add(songBytes[j]); } songBanks.Add(bank0); songBanks.Add(bank1); } songTable[i].bank = (byte)songBank; songTable[i].address = (ushort)songAddr; songTable[i].flags = (byte)(song.UsesDpcm ? 1 : 0); Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' size: {songBytes.Length} bytes."); } //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray()); // Add extra empty banks if we haven't reached the minimum. if (songBanks.Count < RomMinNumberBanks) { for (int i = songBanks.Count; i < RomMinNumberBanks; i++) { songBanks.Add(new List <byte>()); } } else if ((songBanks.Count & 1) != 0) { songBanks.Add(new List <byte>()); } // Build final song bank data. var songBanksBytes = new byte[songBanks.Count * RomBankSize]; for (int i = 0; i < songBanks.Count; i++) { Array.Copy(songBanks[i].ToArray(), 0, songBanksBytes, i * RomBankSize, songBanks[i].Count); } // Patch in code (project info and song table are after the code, 0xf000). Marshal.Copy(new IntPtr(&projectInfo), codeBytes, RomTocOffset, sizeof(RomProjectInfo)); for (int i = 0; i < MaxSongs; i++) { fixed(RomSongEntry *songEntry = &songTable[i]) Marshal.Copy(new IntPtr(songEntry), codeBytes, RomTocOffset + sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry)); } // Patch header (iNES header always counts in 16KB banks, MMC3 counts in 8KB banks) headerBytes[RomHeaderPrgOffset] = (byte)((songBanks.Count + RomCodeDpcmNumBanks) * RomBankSize / 0x4000); // Build final ROM and save. var romBytes = new List <byte>(); romBytes.AddRange(headerBytes); romBytes.AddRange(songBanksBytes); // Samples are at the end, right before the source engine code. MMC3 second to last and last banks respectively. if (project.UsesSamples) { // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM. var dpcmBytes = project.GetPackedSampleData(); Log.LogMessage(LogSeverity.Info, $"DPCM size: {dpcmBytes.Length} bytes."); if (dpcmBytes.Length > MaxDpcmSize) { Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for ROM export ({MaxDpcmSize}). Truncating."); } // Always allocate the full 11KB of samples. Array.Resize(ref dpcmBytes, MaxDpcmSize); romBytes.AddRange(dpcmBytes); } else { romBytes.AddRange(new byte[MaxDpcmSize]); } romBytes.AddRange(codeBytes); File.WriteAllBytes(filename, romBytes.ToArray()); Log.LogMessage(LogSeverity.Info, $"ROM export successful, final file size {romBytes.Count} bytes."); } #if !DEBUG catch (Exception e) { Log.LogMessage(LogSeverity.Error, "Please contact the developer on GitHub!"); Log.LogMessage(LogSeverity.Error, e.Message); Log.LogMessage(LogSeverity.Error, e.StackTrace); return(false); } #endif return(true); }
public GLBitmapAtlas CreateBitmapAtlasFromResources(string[] names) { var bitmaps = new Bitmap[names.Length]; var elementSizeX = 0; var elementSizeY = 0; for (int i = 0; i < names.Length; i++) { var bmp = LoadBitmapFromResourceWithScaling(names[i]); elementSizeX = Math.Max(elementSizeX, bmp.Width); elementSizeY = Math.Max(elementSizeY, bmp.Height); bitmaps[i] = bmp; } var elementsPerRow = MaxAtlasResolution / elementSizeX; var numRows = Utils.DivideAndRoundUp(names.Length, elementsPerRow); var atlasSizeX = elementsPerRow * elementSizeX; var atlasSizeY = numRows * elementSizeY; var textureId = CreateEmptyTexture(atlasSizeX, atlasSizeY); var elementRects = new Rectangle[names.Length]; GL.BindTexture(TextureTarget.Texture2D, textureId); for (int i = 0; i < names.Length; i++) { var bmp = bitmaps[i]; var row = i / elementsPerRow; var col = i % elementsPerRow; elementRects[i] = new Rectangle( col * elementSizeX, row * elementSizeY, bmp.Width, bmp.Height); #if FAMISTUDIO_WINDOWS var bmpData = bmp.LockBits( new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); var ptr = bmpData.Scan0; var stride = bmpData.Stride; var format = PixelFormat.Bgra; #else var ptr = bmp.Pixels; var stride = bmp.Rowstride; var format = PixelFormat.Rgba; #endif Debug.Assert(stride == bmp.Width * 4); GL.TexSubImage2D(TextureTarget.Texture2D, 0, elementRects[i].X, elementRects[i].Y, bmp.Width, bmp.Height, format, PixelType.UnsignedByte, ptr); #if FAMISTUDIO_WINDOWS bmp.UnlockBits(bmpData); #endif bmp.Dispose(); } return(new GLBitmapAtlas(textureId, atlasSizeX, atlasSizeY, elementRects)); }