diff --git a/data/resources/gamedb.yaml b/data/resources/gamedb.yaml
index d5d42d0a3..0644f95ed 100644
--- a/data/resources/gamedb.yaml
+++ b/data/resources/gamedb.yaml
@@ -22079,11 +22079,13 @@ SLUS-01476:
SLPS-01567:
name: "Captain Commando (Japan)"
compatibility:
- rating: GraphicalAudioIssues
- versionTested: "0.1-2433-g9089c973"
- upscalingIssues: "There's garbage in right of the screen and the first boss sprite is corrupted."
+ rating: NoIssues
controllers:
- DigitalController
+ traits:
+ - DisablePGXP # 2D, PGXP is not beneficial.
+ - DisableWidescreen # GTE is not used, no effect.
+ - ForceCDROMSubQSkew # Fixes boss sprites.
settings:
displayActiveStartOffset: -62
displayActiveEndOffset: 51
diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp
index 896803f0c..d1ccd496e 100644
--- a/src/core/cdrom.cpp
+++ b/src/core/cdrom.cpp
@@ -3067,6 +3067,32 @@ void CDROM::DoSectorRead()
if (subq_valid)
{
s_state.last_subq = subq;
+ if (g_settings.cdrom_subq_skew) [[unlikely]]
+ {
+ // SubQ Skew Hack. It's horrible. Needed for Captain Commando.
+ // Here's my previous rambling about the game:
+ //
+ // So, there's two Getloc commands on the PS1 to retrieve the most-recent-read sector:
+ // GetlocL, which returns the timecode based on the data sector header, and GetlocP, which gets it from subq.
+ // Captain Commando would always corrupt the first boss sprite.
+ //
+ // What the game does, is repeat the tile/texture data throughout the audio sectors for the background
+ // music when you reach the boss part of the level, it looks for a specific subq timecode coming in (by spamming
+ // GetlocP) then DMA's the data sector interleaved with the audio sectors out at the last possible moment
+ //
+ // So, they hard coded it to look for a sector timecode +2 from the sector they actually wanted, then DMA that
+ // data out they do perform some validation on the data itself, so if you're not offsetting the timecode query,
+ // it never gets the right sector, and just keeps reading forever. Hence why the boss tiles are broken, because
+ // it never gets the data to upload. The most insane part is they should have just done what every other game
+ // does: use the raw read mode (2352 instead of 2048), and look at the data sector header. Instead they do this
+ // nonsense of repeating the data throughout the audio, and racing the DMA at the last possible minute.
+ //
+ // This hack just generates synthetic SubQ with a +2 offset. I'd planned on refactoring the CDImage interface
+ // so that multiple sectors could be read in one back, in which case we could just "look ahead" to grab the
+ // subq, but I haven't got around to it. It'll break libcrypt, but CC doesn't use it. One day I'll get around to
+ // doing the refactor.... but given this is the only game that relies on it, priorities.
+ s_reader.GetMedia()->GenerateSubChannelQ(&s_state.last_subq, s_state.current_lba + 2);
+ }
}
else
{
@@ -3745,7 +3771,7 @@ void CDROM::CreateFileMap(IsoReader& iso, std::string_view dir)
{
DEV_LOG("{}-{} = {}", entry.location_le, entry.location_le + entry.GetSizeInSectors() - 1, path);
s_state.file_map.emplace(entry.location_le, std::make_pair(entry.location_le + entry.GetSizeInSectors() - 1,
- fmt::format("
{}", path)));
+ fmt::format(" {}", path)));
CreateFileMap(iso, path);
continue;
@@ -3753,7 +3779,7 @@ void CDROM::CreateFileMap(IsoReader& iso, std::string_view dir)
DEV_LOG("{}-{} = {}", entry.location_le, entry.location_le + entry.GetSizeInSectors() - 1, path);
s_state.file_map.emplace(entry.location_le,
- std::make_pair(entry.location_le + entry.GetSizeInSectors() - 1, std::move(path)));
+ std::make_pair(entry.location_le + entry.GetSizeInSectors() - 1, std::move(path)));
}
}
diff --git a/src/core/game_database.cpp b/src/core/game_database.cpp
index 22fb040cf..80026b16c 100644
--- a/src/core/game_database.cpp
+++ b/src/core/game_database.cpp
@@ -40,7 +40,7 @@ namespace GameDatabase {
enum : u32
{
GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48,
- GAME_DATABASE_CACHE_VERSION = 15,
+ GAME_DATABASE_CACHE_VERSION = 16,
};
static Entry* GetMutableEntry(std::string_view serial);
@@ -101,6 +101,7 @@ static constexpr const std::array(GameDatabase::Tr
"ForceRecompilerMemoryExceptions",
"ForceRecompilerICache",
"ForceRecompilerLUTFastmem",
+ "ForceCDROMSubQSkew",
"IsLibCryptProtected",
}};
@@ -130,6 +131,7 @@ static constexpr const std::array(GameDatabase::Tr
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Recompiler Memory Exceptions", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Recompiler ICache", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Recompiler LUT Fastmem", "GameDatabase::Trait"),
+ TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force CD-ROM SubQ Skew", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Is LibCrypt Protected", "GameDatabase::Trait"),
}};
@@ -610,6 +612,12 @@ void GameDatabase::Entry::ApplySettings(Settings& settings, bool display_osd_mes
settings.cpu_fastmem_mode = CPUFastmemMode::LUT;
}
+ if (HasTrait(Trait::ForceCDROMSubQSkew))
+ {
+ WARNING_LOG("CD-ROM SubQ Skew forced by compatibility settings.");
+ settings.cdrom_subq_skew = true;
+ }
+
if (!messages.empty())
{
Host::AddIconOSDMessage(
diff --git a/src/core/game_database.h b/src/core/game_database.h
index f46232fc8..2c1945c30 100644
--- a/src/core/game_database.h
+++ b/src/core/game_database.h
@@ -53,6 +53,7 @@ enum class Trait : u32
ForceRecompilerMemoryExceptions,
ForceRecompilerICache,
ForceRecompilerLUTFastmem,
+ ForceCDROMSubQSkew,
IsLibCryptProtected,
Count
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 1f7bb59be..fef18e32f 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -329,6 +329,7 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si)
si.GetStringValue("CDROM", "MechaconVersion", GetCDROMMechVersionName(DEFAULT_CDROM_MECHACON_VERSION)).c_str())
.value_or(DEFAULT_CDROM_MECHACON_VERSION);
cdrom_region_check = si.GetBoolValue("CDROM", "RegionCheck", false);
+ cdrom_subq_skew = si.GetBoolValue("CDROM", "SubQSkew", false);
cdrom_load_image_to_ram = si.GetBoolValue("CDROM", "LoadImageToRAM", false);
cdrom_load_image_patches = si.GetBoolValue("CDROM", "LoadImagePatches", false);
cdrom_mute_cd_audio = si.GetBoolValue("CDROM", "MuteCDAudio", false);
@@ -615,6 +616,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetIntValue("CDROM", "ReadaheadSectors", cdrom_readahead_sectors);
si.SetStringValue("CDROM", "MechaconVersion", GetCDROMMechVersionName(cdrom_mechacon_version));
si.SetBoolValue("CDROM", "RegionCheck", cdrom_region_check);
+ si.SetBoolValue("CDROM", "SubQSkew", cdrom_subq_skew);
si.SetBoolValue("CDROM", "LoadImageToRAM", cdrom_load_image_to_ram);
si.SetBoolValue("CDROM", "LoadImagePatches", cdrom_load_image_patches);
si.SetBoolValue("CDROM", "MuteCDAudio", cdrom_mute_cd_audio);
diff --git a/src/core/settings.h b/src/core/settings.h
index d1c9568b4..c333e9eba 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -180,6 +180,7 @@ struct Settings
u8 cdrom_readahead_sectors = DEFAULT_CDROM_READAHEAD_SECTORS;
CDROMMechaconVersion cdrom_mechacon_version = DEFAULT_CDROM_MECHACON_VERSION;
bool cdrom_region_check : 1 = false;
+ bool cdrom_subq_skew : 1 = false;
bool cdrom_load_image_to_ram : 1 = false;
bool cdrom_load_image_patches : 1 = false;
bool cdrom_mute_cd_audio : 1 = false;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 4bb2f300c..64276cccc 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -4697,6 +4697,9 @@ void System::WarnAboutUnsafeSettings()
TRANSLATE_STR("System", "Compatibility settings are not enabled. Some games may not function correctly."));
}
+ if (g_settings.cdrom_subq_skew)
+ append(ICON_EMOJI_WARNING, TRANSLATE_SV("System", "CD-ROM SubQ Skew is enabled. This will break games."));
+
if (!messages.empty())
{
if (messages.back() == '\n')
diff --git a/src/duckstation-qt/advancedsettingswidget.cpp b/src/duckstation-qt/advancedsettingswidget.cpp
index a9557564f..fe4dc99a4 100644
--- a/src/duckstation-qt/advancedsettingswidget.cpp
+++ b/src/duckstation-qt/advancedsettingswidget.cpp
@@ -261,6 +261,7 @@ void AdvancedSettingsWidget::addTweakOptions()
Settings::GetCDROMMechVersionDisplayName, static_cast(CDROMMechaconVersion::Count),
Settings::DEFAULT_CDROM_MECHACON_VERSION);
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("CD-ROM Region Check"), "CDROM", "RegionCheck", false);
+ addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("CD-ROM SubQ Skew"), "CDROM", "SubQSkew", false);
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Allow Booting Without SBI File"), "CDROM",
"AllowBootingWithoutSBIFile", false);
@@ -297,6 +298,7 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
setChoiceTweakOption(m_ui.tweakOptionTable, i++,
Settings::DEFAULT_CDROM_MECHACON_VERSION); // CDROM Mechacon Version
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // CDROM Region Check
+ setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // CDROM SubQ Skew
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Allow booting without SBI file
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Export Shared Memory
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Enable PCDRV
@@ -325,6 +327,7 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
sif->DeleteValue("CPU", "FastmemMode");
sif->DeleteValue("CDROM", "MechaconVersion");
sif->DeleteValue("CDROM", "RegionCheck");
+ sif->DeleteValue("CDROM", "SubQSkew");
sif->DeleteValue("CDROM", "AllowBootingWithoutSBIFile");
sif->DeleteValue("PCDrv", "Enabled");
sif->DeleteValue("PCDrv", "EnableWrites");
diff --git a/src/util/cd_image.cpp b/src/util/cd_image.cpp
index b8527ba63..6e5d227c1 100644
--- a/src/util/cd_image.cpp
+++ b/src/util/cd_image.cpp
@@ -456,7 +456,7 @@ void CDImage::CopyTOC(const CDImage* image)
m_position_on_disc = 0;
}
-const CDImage::Index* CDImage::GetIndexForDiscPosition(LBA pos)
+const CDImage::Index* CDImage::GetIndexForDiscPosition(LBA pos) const
{
for (const Index& index : m_indices)
{
@@ -473,7 +473,7 @@ const CDImage::Index* CDImage::GetIndexForDiscPosition(LBA pos)
return nullptr;
}
-const CDImage::Index* CDImage::GetIndexForTrackPosition(u32 track_number, LBA track_pos)
+const CDImage::Index* CDImage::GetIndexForTrackPosition(u32 track_number, LBA track_pos) const
{
if (track_number < 1 || track_number > m_tracks.size())
return nullptr;
@@ -485,18 +485,18 @@ const CDImage::Index* CDImage::GetIndexForTrackPosition(u32 track_number, LBA tr
return GetIndexForDiscPosition(track.start_lba + track_pos);
}
-bool CDImage::GenerateSubChannelQ(SubChannelQ* subq, LBA lba)
+bool CDImage::GenerateSubChannelQ(SubChannelQ* subq, LBA lba) const
{
const Index* index = GetIndexForDiscPosition(lba);
if (!index)
return false;
- const u32 index_offset = index->start_lba_on_disc - lba;
+ const u32 index_offset = lba - index->start_lba_on_disc;
GenerateSubChannelQ(subq, *index, index_offset);
return true;
}
-void CDImage::GenerateSubChannelQ(SubChannelQ* subq, const Index& index, u32 index_offset)
+void CDImage::GenerateSubChannelQ(SubChannelQ* subq, const Index& index, u32 index_offset) const
{
subq->control_bits = index.control.bits;
subq->track_number_bcd = (index.track_number <= m_tracks.size() ? BinaryToBCD(static_cast(index.track_number)) :
diff --git a/src/util/cd_image.h b/src/util/cd_image.h
index 0471439bb..9003ff121 100644
--- a/src/util/cd_image.h
+++ b/src/util/cd_image.h
@@ -301,6 +301,12 @@ public:
// Read a single raw sector, and subchannel from the current LBA.
bool ReadRawSector(void* buffer, SubChannelQ* subq);
+ /// Generates sub-channel Q given the specified position.
+ bool GenerateSubChannelQ(SubChannelQ* subq, LBA lba) const;
+
+ /// Generates sub-channel Q from the given index and index-offset.
+ void GenerateSubChannelQ(SubChannelQ* subq, const Index& index, u32 index_offset) const;
+
// Reads sub-channel Q for the specified index+LBA.
virtual bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index);
@@ -340,14 +346,8 @@ protected:
void ClearTOC();
void CopyTOC(const CDImage* image);
- const Index* GetIndexForDiscPosition(LBA pos);
- const Index* GetIndexForTrackPosition(u32 track_number, LBA track_pos);
-
- /// Generates sub-channel Q given the specified position.
- bool GenerateSubChannelQ(SubChannelQ* subq, LBA lba);
-
- /// Generates sub-channel Q from the given index and index-offset.
- void GenerateSubChannelQ(SubChannelQ* subq, const Index& index, u32 index_offset);
+ const Index* GetIndexForDiscPosition(LBA pos) const;
+ const Index* GetIndexForTrackPosition(u32 track_number, LBA track_pos) const;
/// Synthesis of lead-out data.
void AddLeadOutIndex();