mirror of
https://github.com/GMMan/DrawingSongHLE.git
synced 2025-12-17 01:16:11 +01:00
314 lines
11 KiB
C#
314 lines
11 KiB
C#
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.Formats.Gif;
|
|
using SixLabors.ImageSharp.IO;
|
|
using SixLabors.ImageSharp.Memory;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEngine;
|
|
using UnityEngine.Profiling;
|
|
|
|
public class AnimationDecoder : MonoBehaviour
|
|
{
|
|
BufferedReadStream srcStream;
|
|
BinaryReader br;
|
|
byte[,] drawBuffer;
|
|
Buffer2D<byte> decodeBuffer;
|
|
byte[] cachedCaption;
|
|
Language cachedLang;
|
|
int cachedCaptionIndex;
|
|
int decodingState;
|
|
bool decodingActiveValue;
|
|
MemoryAllocator memoryAllocator;
|
|
|
|
// GIF stuff
|
|
ushort width;
|
|
ushort height;
|
|
byte minCodeSize;
|
|
|
|
public MainController mainController;
|
|
public ResourceManager resourceManager;
|
|
|
|
public int framesDecoded { get; private set; }
|
|
public bool decodingActive
|
|
{
|
|
get
|
|
{
|
|
return decodingActiveValue;
|
|
}
|
|
set
|
|
{
|
|
if (srcStream != null && framesDecoded < Config.numFrames)
|
|
{
|
|
decodingActiveValue = value;
|
|
}
|
|
else
|
|
{
|
|
decodingActiveValue = false;
|
|
}
|
|
}
|
|
}
|
|
public byte[,] indexedPixels => drawBuffer;
|
|
|
|
// Start is called before the first frame update
|
|
void Start()
|
|
{
|
|
memoryAllocator = new ArrayPoolMemoryAllocator();
|
|
drawBuffer = new byte[Config.ANIMATION_HEIGHT, Config.ANIMATION_WIDTH];
|
|
}
|
|
|
|
// Update is called once per frame
|
|
void Update()
|
|
{
|
|
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
ResetController();
|
|
}
|
|
|
|
public void AnimUpdate()
|
|
{
|
|
switch (decodingState)
|
|
{
|
|
case 0:
|
|
// Decode frame
|
|
// In actual firmware 1/4 of the frame is decoded, but we're fast enough on a PC,
|
|
// so decode the entire thing and skip decoding for the next 3 frames
|
|
// (timing shouldn't be affected due to using fixed update)
|
|
Profiler.BeginSample("GIF decode frame");
|
|
DecodeFrame();
|
|
Profiler.EndSample();
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
case 3:
|
|
break;
|
|
case 4:
|
|
// Transfer decoded frame to buffer and apply captions
|
|
FinishFrame();
|
|
break;
|
|
}
|
|
|
|
++decodingState;
|
|
if (decodingState > 4)
|
|
{
|
|
++framesDecoded;
|
|
decodingState = 0;
|
|
decodingActive = false;
|
|
}
|
|
}
|
|
|
|
public void ResetController()
|
|
{
|
|
decodingActive = false;
|
|
if (srcStream != null)
|
|
{
|
|
srcStream.Dispose();
|
|
br = null;
|
|
srcStream = null;
|
|
}
|
|
if (decodeBuffer != null)
|
|
{
|
|
decodeBuffer.Dispose();
|
|
decodeBuffer = null;
|
|
}
|
|
if (drawBuffer != null)
|
|
{
|
|
Array.Clear(drawBuffer, 0, drawBuffer.Length);
|
|
}
|
|
}
|
|
|
|
public void LoadResources()
|
|
{
|
|
srcStream = new BufferedReadStream(new Configuration(), new MemoryStream(resourceManager.GetAnimation()));
|
|
br = new BinaryReader(srcStream);
|
|
|
|
br.ReadBytes(6); // Skip signature and version
|
|
width = br.ReadUInt16();
|
|
height = br.ReadUInt16();
|
|
byte packed = br.ReadByte();
|
|
br.ReadBytes(2); // skip the rest of the header
|
|
int colorDepth = (packed & 3) + 1;
|
|
if ((packed & 0x80) != 0)
|
|
{
|
|
int gctSize = 1 << colorDepth;
|
|
br.ReadBytes(gctSize * 3); // ignore GCT since it's overridden by custom palette
|
|
}
|
|
|
|
minCodeSize = br.ReadByte();
|
|
decodingActive = true;
|
|
decodingState = 0;
|
|
framesDecoded = 0;
|
|
// Assuming here GIF width and height matches what we have hardcoded, but in reality
|
|
// the firmware treats the buffer as 1-dimensional, so if you change the GIF size
|
|
// it'll look funny on firmware but crash here
|
|
decodeBuffer = memoryAllocator.Allocate2D<byte>(new Size(width, height));
|
|
|
|
cachedLang = Language.Max;
|
|
cachedCaption = null;
|
|
cachedCaptionIndex = -1;
|
|
}
|
|
|
|
void DecodeFrame()
|
|
{
|
|
using (LzwDecoder decoder = new LzwDecoder(memoryAllocator, srcStream))
|
|
decoder.DecodePixels(minCodeSize, decodeBuffer);
|
|
//Debug.Log($"Frame {framesDecoded}, Decode pos: 0x{srcStream.Position:x6}");
|
|
br.ReadByte(); // skip frame terminator
|
|
}
|
|
|
|
void FinishFrame()
|
|
{
|
|
// Apply decoded frames
|
|
for (int y = 0; y < height; ++y)
|
|
{
|
|
var row = decodeBuffer.GetRowSpan(y);
|
|
for (int x = 0; x < width; ++x)
|
|
{
|
|
drawBuffer[y, x] ^= row[x];
|
|
}
|
|
}
|
|
|
|
// Note at this point we haven't updated the frame counter for the current frame
|
|
if (framesDecoded == 8)
|
|
{
|
|
// Clear banner so frame result for fade is correct
|
|
// But why is it here? Banner is drawn in by the main controller, not animation controller
|
|
for (int y = 0; y < Config.langBannerHeight; ++y)
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
drawBuffer[y, x] = 2;
|
|
}
|
|
}
|
|
else if (framesDecoded == 13)
|
|
{
|
|
// Clear last line in buffer
|
|
// Presumably no caption covers this spot?
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
drawBuffer[Config.ANIMATION_HEIGHT - 1, x] = 1;
|
|
}
|
|
}
|
|
else if (framesDecoded > 17)
|
|
{
|
|
// Render captions
|
|
int y = Config.ANIMATION_HEIGHT - 1 - Config.captionHeightGeneral;
|
|
|
|
// Note: even though these two cases do mostly the same things, there are very slight differences
|
|
// between how they do them. For example, JP's caption clear uses a slightly different height than
|
|
// ROW
|
|
if (mainController.currentLanguage == Language.Japanese)
|
|
{
|
|
int captionIndex;
|
|
for (captionIndex = 0; captionIndex < Config.captionTimesJp.Length; ++captionIndex)
|
|
{
|
|
if (framesDecoded < Config.captionTimesJp[captionIndex]) break;
|
|
}
|
|
if (captionIndex >= Config.captionTimesJp.Length)
|
|
captionIndex = Config.captionTimesJp.Length - 1;
|
|
int captionTime = Config.captionTimesJp[captionIndex];
|
|
|
|
// Caching is not in the firmware because if can access flash directly as memory
|
|
if (cachedLang != mainController.currentLanguage || cachedCaptionIndex != captionIndex || cachedCaption == null)
|
|
{
|
|
cachedLang = mainController.currentLanguage;
|
|
cachedCaptionIndex = captionIndex;
|
|
cachedCaption = resourceManager.GetRawCaption(cachedLang, captionIndex);
|
|
}
|
|
|
|
if (captionTime - framesDecoded <= 2)
|
|
{
|
|
// Clear caption area for the last two frames of current caption
|
|
int endY = y + Config.captionHeightGeneral + 1; // It's actually 2 in firmware; why did they overflow?
|
|
for (; y < endY; ++y)
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
drawBuffer[y, x] = Config.captionBgColor;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
byte b = 0;
|
|
byte bitsRemaining = 0;
|
|
int i = 0;
|
|
var fgColor = captionIndex == Config.captionTimesJp.Length - 1 ? Config.captionLastColor : Config.captionFgColor;
|
|
for (int yOff = 0; yOff < Config.captionHeightJp; ++yOff)
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
if (bitsRemaining == 0)
|
|
{
|
|
b = cachedCaption[i++];
|
|
bitsRemaining = 8;
|
|
}
|
|
|
|
drawBuffer[y + yOff, x] = (b & 1) == 1 ? fgColor : Config.captionBgColor;
|
|
b >>= 1;
|
|
--bitsRemaining;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int captionIndex;
|
|
for (captionIndex = 0; captionIndex < Config.captionTimesRow.Length; ++captionIndex)
|
|
{
|
|
if (framesDecoded < Config.captionTimesRow[captionIndex]) break;
|
|
}
|
|
if (captionIndex >= Config.captionTimesRow.Length)
|
|
captionIndex = Config.captionTimesRow.Length - 1;
|
|
int captionTime = Config.captionTimesRow[captionIndex];
|
|
|
|
// Caching is not in the firmware because if can access flash directly as memory
|
|
if (cachedLang != mainController.currentLanguage || cachedCaptionIndex != captionIndex || cachedCaption == null)
|
|
{
|
|
cachedLang = mainController.currentLanguage;
|
|
cachedCaptionIndex = captionIndex;
|
|
cachedCaption = resourceManager.GetRawCaption(cachedLang, captionIndex);
|
|
}
|
|
|
|
// Always clear the top part of the caption area (used for JP ruby)
|
|
int endYTop = y + Config.captionHeightGeneral - Config.captionHeightRow;
|
|
for (; y < endYTop; ++y)
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
drawBuffer[y, x] = Config.captionBgColor;
|
|
}
|
|
|
|
if (captionTime - framesDecoded <= 2)
|
|
{
|
|
// Clear caption area for the last two frames of current caption
|
|
int endY = y + Config.captionHeightRow;
|
|
for (; y < endY; ++y)
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
drawBuffer[y, x] = Config.captionBgColor;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
byte b = 0;
|
|
byte bitsRemaining = 0;
|
|
int i = 0;
|
|
var fgColor = captionIndex == Config.captionTimesRow.Length - 1 ? Config.captionLastColor : Config.captionFgColor;
|
|
for (int yOff = 0; yOff < Config.captionHeightRow; ++yOff)
|
|
for (int x = 0; x < Config.ANIMATION_WIDTH; ++x)
|
|
{
|
|
if (bitsRemaining == 0)
|
|
{
|
|
b = cachedCaption[i++];
|
|
bitsRemaining = 8;
|
|
}
|
|
|
|
drawBuffer[y + yOff, x] = (b & 1) == 1 ? fgColor : Config.captionBgColor;
|
|
b >>= 1;
|
|
--bitsRemaining;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|