Skip to content

Commit

Permalink
GCMemcard: Read icons according to logical data offsets instead of ph…
Browse files Browse the repository at this point in the history
…ysical data offsets. Also gets rid of some undefined behavior.
  • Loading branch information
AdmiralCurtiss committed Nov 17, 2019
1 parent 110d6c1 commit 3b67d0d
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 123 deletions.
214 changes: 110 additions & 104 deletions Source/Core/Core/HW/GCMemcard/GCMemcard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1229,139 +1229,145 @@ std::optional<std::vector<u32>> GCMemcard::ReadBannerRGBA8(u8 index) const
return rgba;
}

u32 GCMemcard::ReadAnimRGBA8(u8 index, u32* buffer, u8* delays) const
std::optional<std::vector<GCMemcardAnimationFrameRGBA8>> GCMemcard::ReadAnimRGBA8(u8 index) const
{
if (!m_valid || index >= DIRLEN)
return 0;

// To ensure only one type of icon is used
// Sonic Heroes it the only game I have seen that tries to use a CI8 and RGB5A3 icon
// int fmtCheck = 0;

int formats = GetActiveDirectory().m_dir_entries[index].m_icon_format;
int fdelays = GetActiveDirectory().m_dir_entries[index].m_animation_speed;

int flags = GetActiveDirectory().m_dir_entries[index].m_banner_and_icon_flags;
// Timesplitters 2 and 3 is the only game that I see this in
// May be a hack
// if (flags == 0xFB) flags = ~flags;
// Batten Kaitos has 0x65 as flag too. Everything but the first 3 bytes seems irrelevant.
// Something similar happens with Wario Ware Inc. AnimSpeed

int bnrFormat = (flags & 3);
return std::nullopt;

u32 DataOffset = GetActiveDirectory().m_dir_entries[index].m_image_offset;
u32 DataBlock = GetActiveDirectory().m_dir_entries[index].m_first_block - MC_FST_BLOCKS;
u32 image_offset = GetActiveDirectory().m_dir_entries[index].m_image_offset;
if (image_offset == 0xFFFFFFFF)
return std::nullopt;

if ((DataBlock > m_size_blocks) || (DataOffset == 0xFFFFFFFF))
// Data at m_image_offset stores first the banner, if any, and then the icon data.
// Skip over the banner if there is one.
// See ReadBannerRGBA8() for details on how the banner is stored.
const u8 flags = GetActiveDirectory().m_dir_entries[index].m_banner_and_icon_flags;
const u8 banner_format = (flags & 0b0000'0011);
const u32 banner_pixels = MEMORY_CARD_BANNER_WIDTH * MEMORY_CARD_BANNER_HEIGHT;
if (banner_format == MEMORY_CARD_BANNER_FORMAT_CI8)
image_offset += banner_pixels + MEMORY_CARD_CI8_PALETTE_ENTRIES * 2;
else if (banner_format == MEMORY_CARD_BANNER_FORMAT_RGB5A3)
image_offset += banner_pixels * 2;

// decode icon formats and frame delays
const u16 icon_format = GetActiveDirectory().m_dir_entries[index].m_icon_format;
const u16 animation_speed = GetActiveDirectory().m_dir_entries[index].m_animation_speed;
std::array<u8, MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES> frame_formats;
std::array<u8, MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES> frame_delays;
for (u32 i = 0; i < MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES; ++i)
{
return 0;
frame_formats[i] = (icon_format >> (2 * i)) & 0b11;
frame_delays[i] = (animation_speed >> (2 * i)) & 0b11;
}

u8* animData = (u8*)(m_data_blocks[DataBlock].m_block.data() + DataOffset);
// if first frame format is 0, the entire icon is skipped
if (frame_formats[0] == 0)
return std::nullopt;

switch (bnrFormat)
// calculate byte length of each individual icon frame and full icon data
constexpr u32 pixels_per_frame = MEMORY_CARD_ICON_WIDTH * MEMORY_CARD_ICON_HEIGHT;
u32 data_length = 0;
u32 frame_count = 0;
std::array<u32, MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES> frame_offsets;
bool has_shared_palette = false;
for (u32 i = 0; i < MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES; ++i)
{
case 1:
animData += 96 * 32 + 2 * 256; // image+palette
break;
case 2:
animData += 96 * 32 * 2;
break;
}

int fmts[8];
u8* data[8];
int frames = 0;
if (frame_delays[i] == 0)
{
// frame delay of 0 means we're out of frames
break;
}

for (int i = 0; i < 8; i++)
{
fmts[i] = (formats >> (2 * i)) & 3;
delays[i] = ((fdelays >> (2 * i)) & 3);
data[i] = animData;
// otherwise this counts as a frame, even if the format is none of the three valid ones
// (see the actual icon decoding below for how that is handled)
++frame_count;
frame_offsets[i] = data_length;

if (!delays[i])
if (frame_formats[i] == MEMORY_CARD_ICON_FORMAT_CI8_SHARED_PALETTE)
{
// First icon_speed = 0 indicates there aren't any more icons
break;
data_length += pixels_per_frame;
has_shared_palette = true;
}
// If speed is set there is an icon (it can be a "blank frame")
frames++;
if (fmts[i] != 0)
else if (frame_formats[i] == MEMORY_CARD_ICON_FORMAT_RGB5A3)
{
switch (fmts[i])
{
case CI8SHARED: // CI8 with shared palette
animData += 32 * 32;
break;
case RGB5A3: // RGB5A3
animData += 32 * 32 * 2;
break;
case CI8: // CI8 with own palette
animData += 32 * 32 + 2 * 256;
break;
}
data_length += pixels_per_frame * 2;
}
else if (frame_formats[i] == MEMORY_CARD_ICON_FORMAT_CI8_UNIQUE_PALETTE)
{
data_length += pixels_per_frame + 2 * MEMORY_CARD_CI8_PALETTE_ENTRIES;
}
}

const u16* sharedPal = reinterpret_cast<u16*>(animData);
if (frame_count == 0)
return std::nullopt;

const u32 shared_palette_offset = data_length;
if (has_shared_palette)
data_length += 2 * MEMORY_CARD_CI8_PALETTE_ENTRIES;

for (int i = 0; i < 8; i++)
// now that we have determined the data length, fetch the actual data from the save file
// if anything is sketchy, bail so we don't access out of bounds
auto save_data_bytes = GetSaveDataBytes(index, image_offset, data_length);
if (!save_data_bytes || save_data_bytes->size() != data_length)
return std::nullopt;

// and finally, decode icons into RGBA8
std::array<u16, MEMORY_CARD_CI8_PALETTE_ENTRIES> shared_palette;
if (has_shared_palette)
{
if (!delays[i])
{
// First icon_speed = 0 indicates there aren't any more icons
break;
}
if (fmts[i] != 0)
std::memcpy(shared_palette.data(), save_data_bytes->data() + shared_palette_offset,
2 * MEMORY_CARD_CI8_PALETTE_ENTRIES);
}

std::vector<GCMemcardAnimationFrameRGBA8> output;
for (u32 i = 0; i < frame_count; ++i)
{
GCMemcardAnimationFrameRGBA8& output_frame = output.emplace_back();
output_frame.image_data.resize(pixels_per_frame);
output_frame.delay = frame_delays[i];

// Note on how to interpret this inner loop here: In the general case this just degenerates into
// j == i for every iteration, but in some rare cases (such as Luigi's Mansion or Pikmin) some
// frames will not actually have an associated format. In this case we forward to the next valid
// frame to decode, which appears (at least visually) to match the behavior of the GC BIOS. Note
// that this may end up decoding the same frame multiple times.
// If this happens but no next valid frame exists, we instead return a fully transparent frame,
// again visually matching the GC BIOS. There is no extra code necessary for this as the
// resize() of the vector already initializes it to a fully transparent frame.
for (u32 j = i; j < frame_count; ++j)
{
switch (fmts[i])
if (frame_formats[j] == MEMORY_CARD_ICON_FORMAT_CI8_SHARED_PALETTE)
{
case CI8SHARED: // CI8 with shared palette
Common::DecodeCI8Image(buffer, data[i], sharedPal, 32, 32);
buffer += 32 * 32;
break;
case RGB5A3: // RGB5A3
Common::Decode5A3Image(buffer, (u16*)(data[i]), 32, 32);
buffer += 32 * 32;
Common::DecodeCI8Image(output_frame.image_data.data(),
save_data_bytes->data() + frame_offsets[j], shared_palette.data(),
MEMORY_CARD_ICON_WIDTH, MEMORY_CARD_ICON_HEIGHT);
break;
case CI8: // CI8 with own palette
const u16* paldata = reinterpret_cast<u16*>(data[i] + 32 * 32);
Common::DecodeCI8Image(buffer, data[i], paldata, 32, 32);
buffer += 32 * 32;
}

if (frame_formats[j] == MEMORY_CARD_ICON_FORMAT_RGB5A3)
{
std::array<u16, pixels_per_frame> pxdata;
std::memcpy(pxdata.data(), save_data_bytes->data() + frame_offsets[j],
pixels_per_frame * 2);
Common::Decode5A3Image(output_frame.image_data.data(), pxdata.data(),
MEMORY_CARD_ICON_WIDTH, MEMORY_CARD_ICON_HEIGHT);
break;
}
}
else
{
// Speed is set but there's no actual icon
// This is used to reduce animation speed in Pikmin and Luigi's Mansion for example
// These "blank frames" show the next icon
for (int j = i; j < 8; ++j)

if (frame_formats[j] == MEMORY_CARD_ICON_FORMAT_CI8_UNIQUE_PALETTE)
{
if (fmts[j] != 0)
{
switch (fmts[j])
{
case CI8SHARED: // CI8 with shared palette
Common::DecodeCI8Image(buffer, data[j], sharedPal, 32, 32);
break;
case RGB5A3: // RGB5A3
Common::Decode5A3Image(buffer, (u16*)(data[j]), 32, 32);
buffer += 32 * 32;
break;
case CI8: // CI8 with own palette
const u16* paldata = reinterpret_cast<u16*>(data[j] + 32 * 32);
Common::DecodeCI8Image(buffer, data[j], paldata, 32, 32);
buffer += 32 * 32;
break;
}
}
std::array<u16, MEMORY_CARD_CI8_PALETTE_ENTRIES> paldata;
std::memcpy(paldata.data(), save_data_bytes->data() + frame_offsets[j] + pixels_per_frame,
MEMORY_CARD_CI8_PALETTE_ENTRIES * 2);
Common::DecodeCI8Image(output_frame.image_data.data(),
save_data_bytes->data() + frame_offsets[j], paldata.data(),
MEMORY_CARD_ICON_WIDTH, MEMORY_CARD_ICON_HEIGHT);
break;
}
}
}

return frames;
return output;
}

bool GCMemcard::Format(u8* card_data, bool shift_jis, u16 SizeMb)
Expand Down
17 changes: 12 additions & 5 deletions Source/Core/Core/HW/GCMemcard/GCMemcard.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ enum
GCI = 0,
SAV = 0x80,
GCS = 0x110,

CI8SHARED = 1,
RGB5A3,
CI8,
};

enum class GCMemcardGetSaveDataRetVal
Expand Down Expand Up @@ -106,6 +102,12 @@ class GCMemcardErrorCode
std::bitset<static_cast<size_t>(GCMemcardValidityIssues::COUNT)> m_errors;
};

struct GCMemcardAnimationFrameRGBA8
{
std::vector<u32> image_data;
u8 delay;
};

// size of a single memory card block in bytes
constexpr u32 BLOCK_SIZE = 0x2000;

Expand Down Expand Up @@ -152,6 +154,11 @@ constexpr u32 MEMORY_CARD_ICON_HEIGHT = 32;
// maximum number of frames a save file's icon animation can have
constexpr u32 MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES = 8;

// color format of icon frame as stored in m_icon_format (two bits per frame)
constexpr u8 MEMORY_CARD_ICON_FORMAT_CI8_SHARED_PALETTE = 1;
constexpr u8 MEMORY_CARD_ICON_FORMAT_RGB5A3 = 2;
constexpr u8 MEMORY_CARD_ICON_FORMAT_CI8_UNIQUE_PALETTE = 3;

// number of palette entries in a CI8 palette of a banner or icon
// each palette entry is 16 bits in RGB5A3 format
constexpr u32 MEMORY_CARD_CI8_PALETTE_ENTRIES = 256;
Expand Down Expand Up @@ -507,5 +514,5 @@ class GCMemcard
std::optional<std::vector<u32>> ReadBannerRGBA8(u8 index) const;

// reads the animation frames
u32 ReadAnimRGBA8(u8 index, u32* buffer, u8* delays) const;
std::optional<std::vector<GCMemcardAnimationFrameRGBA8>> ReadAnimRGBA8(u8 index) const;
};
24 changes: 10 additions & 14 deletions Source/Core/DolphinQt/GCMemcardManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -483,40 +483,36 @@ GCMemcardManager::IconAnimationData GCMemcardManager::GetIconFromSaveFile(int fi
{
auto& memcard = m_slot_memcard[slot];

std::vector<u8> anim_delay(MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES);
std::vector<u32> anim_data(MEMORY_CARD_ICON_WIDTH * MEMORY_CARD_ICON_HEIGHT *
MEMORY_CARD_ICON_ANIMATION_MAX_FRAMES);

IconAnimationData frame_data;

const u32 num_frames = memcard->ReadAnimRGBA8(file_index, anim_data.data(), anim_delay.data());
const auto decoded_data = memcard->ReadAnimRGBA8(file_index);

// Decode Save File Animation
if (num_frames > 0)
if (decoded_data && !decoded_data->empty())
{
frame_data.m_frames.reserve(num_frames);
frame_data.m_frames.reserve(decoded_data->size());
const u32 per_frame_offset = MEMORY_CARD_ICON_WIDTH * MEMORY_CARD_ICON_HEIGHT;
for (u32 f = 0; f < num_frames; ++f)
for (size_t f = 0; f < decoded_data->size(); ++f)
{
QImage img(reinterpret_cast<u8*>(&anim_data[f * per_frame_offset]), MEMORY_CARD_ICON_WIDTH,
MEMORY_CARD_ICON_HEIGHT, QImage::Format_ARGB32);
QImage img(reinterpret_cast<const u8*>((*decoded_data)[f].image_data.data()),
MEMORY_CARD_ICON_WIDTH, MEMORY_CARD_ICON_HEIGHT, QImage::Format_ARGB32);
frame_data.m_frames.push_back(QPixmap::fromImage(img));
for (int i = 0; i < anim_delay[f]; ++i)
for (int i = 0; i < (*decoded_data)[f].delay; ++i)
{
frame_data.m_frame_timing.push_back(static_cast<u8>(f));
}
}

const bool is_pingpong = memcard->DEntry_IsPingPong(file_index);
if (is_pingpong && num_frames >= 3)
if (is_pingpong && decoded_data->size() >= 3)
{
// if the animation 'ping-pongs' between start and end then the animation frame order is
// something like 'abcdcbabcdcba' instead of the usual 'abcdabcdabcd'
// to display that correctly just append all except the first and last frame in reverse order
// at the end of the animation
for (u32 f = num_frames - 2; f > 0; --f)
for (size_t f = decoded_data->size() - 2; f > 0; --f)
{
for (int i = 0; i < anim_delay[f]; ++i)
for (int i = 0; i < (*decoded_data)[f].delay; ++i)
{
frame_data.m_frame_timing.push_back(static_cast<u8>(f));
}
Expand Down

0 comments on commit 3b67d0d

Please sign in to comment.