Files

582 lines
16 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/HideObjectEngine.h"
#include <algorithm>
#include <fstream>
#include <map>
#include <set>
#include <sstream>
#include <tuple>
#include <utility>
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Core/ConfigLoaders/GameConfigLoader.h"
namespace HideObjectEngine
{
// ============================================================================
// Type name mapping
// ============================================================================
static const char* s_type_names[] = {"8bits", "16bits", "24bits", "32bits",
"40bits", "48bits", "56bits", "64bits",
"72bits", "80bits", "88bits", "96bits",
"104bits", "112bits", "120bits", "128bits"};
const char* GetTypeName(HideObjectType type)
{
const int idx = static_cast<int>(type);
if (idx >= 0 && idx < 16)
return s_type_names[idx];
return "16bits";
}
HideObjectType ParseTypeName(const std::string& name)
{
for (int i = 0; i < 16; i++)
{
if (name == s_type_names[i])
return static_cast<HideObjectType>(i);
}
return HideObjectType::Bits16; // Default
}
std::optional<HideObjectEntry> BuildEntryFromPrefix(const u8* data, size_t available_bytes)
{
if (!data || available_bytes == 0)
return std::nullopt;
const size_t byte_count = std::min<size_t>(available_bytes, GetByteCount(HideObjectType::Bits128));
HideObjectEntry entry;
entry.type = static_cast<HideObjectType>(byte_count - 1);
if (byte_count > 8)
{
const size_t upper_bytes = byte_count - 8;
for (size_t i = 0; i < upper_bytes; ++i)
entry.value_upper = (entry.value_upper << 8) | data[i];
for (size_t i = upper_bytes; i < byte_count; ++i)
entry.value_lower = (entry.value_lower << 8) | data[i];
}
else
{
for (size_t i = 0; i < byte_count; ++i)
entry.value_lower = (entry.value_lower << 8) | data[i];
}
return entry;
}
// ============================================================================
// Static INI helpers — direct file I/O (same pattern as ShaderHunter)
// ============================================================================
// Strip [HideObjectCodes_Enabled] and [HideObjectCodes] sections from file content.
static std::string ReadFileWithoutHideObjectSections(const std::string& path)
{
std::ifstream file(path);
if (!file.is_open())
return {};
std::ostringstream out;
bool skipping = false;
std::string line;
while (std::getline(file, line))
{
std::string trimmed = line;
if (!trimmed.empty() && trimmed.back() == '\r')
trimmed.pop_back();
if (trimmed == "[HideObjectCodes_Enabled]" || trimmed == "[HideObjectCodes]")
{
skipping = true;
continue;
}
if (skipping && !trimmed.empty() && trimmed[0] == '[')
skipping = false;
if (!skipping)
out << line << "\n";
}
return out.str();
}
// Parse key=value with optional whitespace around '='.
static bool ParseKeyValue(const std::string& line, std::string& key, std::string& value)
{
const auto eq = line.find('=');
if (eq == std::string::npos)
return false;
key = line.substr(0, eq);
value = line.substr(eq + 1);
while (!key.empty() && key.back() == ' ')
key.pop_back();
while (!value.empty() && value.front() == ' ')
value.erase(value.begin());
return true;
}
static std::string GetVRGameSettingsPath(const std::string& game_id)
{
return File::GetUserPath(D_GAMESETTINGSVR_IDX) + game_id + ".ini";
}
static std::string GetSysVRGameSettingsPath(const std::string& filename)
{
return File::GetSysDirectory() + GAMESETTINGSVR_DIR DIR_SEP + filename;
}
struct ParsedHideObjectFile
{
std::vector<HideObject> entries;
bool has_enable_section = false;
std::set<std::string> enabled_names;
};
static ParsedHideObjectFile LoadFromINIFile(const std::string& path)
{
ParsedHideObjectFile parsed;
std::ifstream file(path);
if (!file.is_open())
return parsed;
// First pass: read [HideObjectCodes_Enabled] for enabled names
{
bool in_section = false;
std::string line;
while (std::getline(file, line))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (line == "[HideObjectCodes_Enabled]")
{
in_section = true;
parsed.has_enable_section = true;
continue;
}
if (in_section && !line.empty() && line[0] == '[')
break;
if (!in_section || line.empty())
continue;
if (line[0] == '$')
parsed.enabled_names.insert(line.substr(1));
}
}
// Second pass: read [HideObjectCodes] for code data
file.clear();
file.seekg(0);
bool in_section = false;
HideObject current{};
HideObjectEntry current_entry{};
bool has_code = false;
bool has_entry = false;
auto commit_entry = [&]() {
if (has_entry)
current.entries.push_back(current_entry);
has_entry = false;
current_entry = {};
};
auto commit_code = [&]() {
commit_entry();
if (!has_code || current.name.empty())
return;
current.active = parsed.enabled_names.count(current.name) > 0;
if (!current.entries.empty())
parsed.entries.push_back(current);
};
std::string line;
while (std::getline(file, line))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (line == "[HideObjectCodes]")
{
in_section = true;
continue;
}
if (in_section && !line.empty() && line[0] == '[')
break;
if (!in_section || line.empty())
continue;
if (line[0] == '$')
{
commit_code();
current = {};
current.name = line.substr(1);
current.user_defined = true;
has_code = true;
}
else if (has_code)
{
// Backward compatibility: accept legacy '+' prefixes from older files.
std::string data_line = line;
if (!data_line.empty() && data_line[0] == '+')
data_line = data_line.substr(1);
std::string key, value;
if (!ParseKeyValue(data_line, key, value))
{
// Legacy Hydra format: "type:upper:lower" (colon-separated, no key=value prefix)
// e.g. "128bits:0x0000000000000000:0x000000000C0C0CFF"
// Each such line is a complete self-contained entry — add it directly.
std::vector<std::string> parts;
{
std::istringstream ss(data_line);
std::string part;
while (std::getline(ss, part, ':'))
parts.push_back(part);
}
if (parts.size() >= 3)
{
HideObjectEntry legacy_entry;
legacy_entry.type = ParseTypeName(parts[0]);
legacy_entry.value_upper = std::strtoull(parts[1].c_str(), nullptr, 16);
legacy_entry.value_lower = std::strtoull(parts[2].c_str(), nullptr, 16);
current.entries.push_back(legacy_entry);
}
continue;
}
if (key == "Type")
{
// If we already have a pending entry, commit it first (new entry starts with Type=)
if (has_entry)
{
current.entries.push_back(current_entry);
current_entry = {};
}
current_entry.type = ParseTypeName(value);
has_entry = true;
}
else if (key == "Upper")
{
current_entry.value_upper = std::strtoull(value.c_str(), nullptr, 16);
}
else if (key == "Lower")
{
current_entry.value_lower = std::strtoull(value.c_str(), nullptr, 16);
}
}
}
commit_code();
return parsed;
}
static void MergeParsedHideObjectFile(std::vector<HideObject>* result,
std::map<std::string, size_t>* index_by_name,
ParsedHideObjectFile parsed)
{
for (auto& entry : parsed.entries)
{
const auto it = index_by_name->find(entry.name);
if (it != index_by_name->end())
(*result)[it->second] = std::move(entry);
else
{
const size_t index = result->size();
index_by_name->emplace(entry.name, index);
result->push_back(std::move(entry));
}
}
if (parsed.has_enable_section)
{
for (auto& entry : *result)
entry.active = parsed.enabled_names.count(entry.name) > 0;
}
}
std::vector<HideObject> LoadFromINI(const std::string& game_id, std::optional<u16> revision)
{
if (game_id.empty())
return {};
std::vector<HideObject> result;
std::map<std::string, size_t> index_by_name;
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
MergeParsedHideObjectFile(&result, &index_by_name,
LoadFromINIFile(GetSysVRGameSettingsPath(filename)));
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
MergeParsedHideObjectFile(&result, &index_by_name,
LoadFromINIFile(File::GetUserPath(D_GAMESETTINGSVR_IDX) + filename));
return result;
}
void SaveToINI(const std::string& game_id, const std::vector<HideObject>& codes)
{
if (game_id.empty())
return;
const std::string path = GetVRGameSettingsPath(game_id);
File::CreateFullPath(path);
std::string base = ReadFileWithoutHideObjectSections(path);
// Strip trailing whitespace/newlines, then add one newline separator
while (!base.empty() && (base.back() == '\n' || base.back() == '\r' || base.back() == ' '))
base.pop_back();
if (!base.empty())
base += "\n";
std::ostringstream out;
out << base;
// [HideObjectCodes_Enabled] — lists enabled code names
out << "[HideObjectCodes_Enabled]\n";
for (const auto& code : codes)
{
if (code.active)
out << "$" << code.name << "\n";
}
// [HideObjectCodes] — full code data (all codes, enabled or not)
// Data lines are written as plain key=value in GameSettingsVR.
out << "[HideObjectCodes]\n";
for (const auto& code : codes)
{
out << "$" << code.name << "\n";
for (const auto& entry : code.entries)
{
out << "Type=" << GetTypeName(entry.type) << "\n";
out << "Upper=0x" << fmt::format("{:016X}", entry.value_upper) << "\n";
out << "Lower=0x" << fmt::format("{:016X}", entry.value_lower) << "\n";
}
out << "\n";
}
std::ofstream outfile(path, std::ios::trunc);
outfile << out.str();
}
// ============================================================================
// Runtime Engine singleton
// ============================================================================
Engine& Engine::GetInstance()
{
static Engine instance;
return instance;
}
static SkipEntry BuildSkipEntry(const HideObjectEntry& entry)
{
SkipEntry skip_entry;
const int byte_count = GetByteCount(entry.type);
if (byte_count > 8)
{
// Upper bytes first (big-endian), for the portion above 8 bytes
const int upper_bytes = byte_count - 8;
for (int j = upper_bytes; j > 0; --j)
skip_entry.push_back(static_cast<u8>((entry.value_upper >> ((j - 1) * 8)) & 0xFF));
// Then all 8 lower bytes
for (int j = 8; j > 0; --j)
skip_entry.push_back(static_cast<u8>((entry.value_lower >> ((j - 1) * 8)) & 0xFF));
}
else
{
// Only lower bytes, big-endian
for (int j = byte_count; j > 0; --j)
skip_entry.push_back(static_cast<u8>((entry.value_lower >> ((j - 1) * 8)) & 0xFF));
}
return skip_entry;
}
void Engine::ApplyCodes(const std::vector<HideObject>& codes)
{
m_updating.store(true, std::memory_order_release);
std::vector<SkipEntry> new_entries;
for (const auto& code : codes)
{
if (!code.active)
continue;
for (const auto& entry : code.entries)
new_entries.push_back(BuildSkipEntry(entry));
}
{
std::lock_guard lock(m_mutex);
m_active_entries = std::move(new_entries);
}
m_updating.store(false, std::memory_order_release);
}
void Engine::SetCaptureEnabled(bool enabled)
{
m_capture_enabled.store(enabled, std::memory_order_release);
std::lock_guard lock(m_capture_mutex);
m_pending_capture_prefixes.clear();
m_collecting_capture_prefixes.clear();
m_display_capture_prefixes.clear();
}
bool Engine::IsCaptureEnabled() const
{
return m_capture_enabled.load(std::memory_order_acquire);
}
void Engine::CaptureVertexPrefix(const u8* src, size_t available_bytes)
{
if (!m_capture_enabled.load(std::memory_order_acquire) || !src || available_bytes == 0)
return;
const size_t byte_count = std::min<size_t>(available_bytes, GetByteCount(HideObjectType::Bits128));
SkipEntry prefix(src, src + byte_count);
std::lock_guard lock(m_capture_mutex);
if (!m_capture_enabled.load(std::memory_order_relaxed))
return;
m_pending_capture_prefixes.push_back(std::move(prefix));
}
void Engine::CommitCapturedPrefixesForDraw(u32 draw_sequence)
{
if (!m_capture_enabled.load(std::memory_order_acquire))
return;
std::lock_guard lock(m_capture_mutex);
if (!m_capture_enabled.load(std::memory_order_relaxed))
{
m_pending_capture_prefixes.clear();
return;
}
for (auto& prefix : m_pending_capture_prefixes)
m_collecting_capture_prefixes.push_back(CapturedPrefix{draw_sequence, std::move(prefix)});
m_pending_capture_prefixes.clear();
}
void Engine::DiscardPendingCapturedPrefixes()
{
std::lock_guard lock(m_capture_mutex);
m_pending_capture_prefixes.clear();
}
void Engine::OnFrameEnd()
{
std::lock_guard lock(m_capture_mutex);
if (!m_capture_enabled.load(std::memory_order_relaxed))
{
m_pending_capture_prefixes.clear();
m_collecting_capture_prefixes.clear();
m_display_capture_prefixes.clear();
return;
}
m_display_capture_prefixes = std::move(m_collecting_capture_prefixes);
m_collecting_capture_prefixes.clear();
m_pending_capture_prefixes.clear();
}
CapturedHideObjectEntries
Engine::GetCapturedEntriesForDrawSequences(const std::vector<u32>& draw_sequences) const
{
CapturedHideObjectEntries result;
std::set<u32> requested_draws(draw_sequences.begin(), draw_sequences.end());
result.requested_draws = requested_draws.size();
if (requested_draws.empty())
return result;
std::set<u32> matched_draws;
std::set<std::tuple<int, u64, u64>> seen_entries;
std::lock_guard lock(m_capture_mutex);
for (const auto& captured : m_display_capture_prefixes)
{
if (!requested_draws.contains(captured.draw_sequence))
continue;
result.prefixes_seen++;
matched_draws.insert(captured.draw_sequence);
const std::optional<HideObjectEntry> entry =
BuildEntryFromPrefix(captured.prefix.data(), captured.prefix.size());
if (!entry)
continue;
if (GetByteCount(entry->type) < GetByteCount(HideObjectType::Bits128))
result.shorter_than_128bit++;
const auto key =
std::make_tuple(static_cast<int>(entry->type), entry->value_upper, entry->value_lower);
if (seen_entries.insert(key).second)
result.entries.push_back(*entry);
}
result.matched_draws = matched_draws.size();
return result;
}
void Engine::LoadCodes(const std::string& game_id)
{
auto codes = LoadFromINI(game_id);
ApplyCodes(codes);
{
std::lock_guard lock(m_mutex);
m_loaded_game_id = game_id;
}
if (!m_active_entries.empty())
{
INFO_LOG_FMT(VIDEO, "HideObjectEngine: Loaded {} active entries for game {}",
m_active_entries.size(), game_id);
}
}
void Engine::LoadCodesIfNeeded(const std::string& game_id)
{
{
std::lock_guard lock(m_mutex);
if (game_id == m_loaded_game_id)
return;
}
LoadCodes(game_id);
}
bool Engine::ShouldHide(const u8* src) const
{
if (m_updating.load(std::memory_order_acquire))
return false;
// No lock needed — m_active_entries is only modified under m_updating=true
for (const auto& entry : m_active_entries)
{
if (std::memcmp(src, entry.data(), entry.size()) == 0)
return true;
}
return false;
}
bool Engine::HasCodes() const
{
return !m_active_entries.empty();
}
} // namespace HideObjectEngine