backend-infra-engineer: Release v0.3.9-hotfix7 snapshot
This commit is contained in:
@@ -58,6 +58,13 @@ target_include_directories(yaze PUBLIC
|
||||
target_sources(yaze PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/yaze_config.h)
|
||||
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/yaze_config.h PROPERTIES GENERATED TRUE)
|
||||
|
||||
# Add SDL version compile definitions
|
||||
if(YAZE_USE_SDL3)
|
||||
target_compile_definitions(yaze PRIVATE YAZE_SDL3=1)
|
||||
else()
|
||||
target_compile_definitions(yaze PRIVATE YAZE_SDL2=1)
|
||||
endif()
|
||||
|
||||
# Link modular libraries
|
||||
target_link_libraries(yaze PRIVATE
|
||||
yaze_editor
|
||||
|
||||
@@ -17,8 +17,18 @@ set(
|
||||
# because it depends on yaze_editor and yaze_gui, which would create a cycle:
|
||||
# yaze_agent -> yaze_app_core_lib -> yaze_editor -> yaze_agent
|
||||
app/platform/window.cc
|
||||
# Window backend abstraction (SDL2/SDL3 support)
|
||||
app/platform/sdl2_window_backend.cc
|
||||
app/platform/window_backend_factory.cc
|
||||
)
|
||||
|
||||
# SDL3 window backend (only compiled when YAZE_USE_SDL3 is defined)
|
||||
if(YAZE_USE_SDL3)
|
||||
list(APPEND YAZE_APP_CORE_SRC
|
||||
app/platform/sdl3_window_backend.cc
|
||||
)
|
||||
endif()
|
||||
|
||||
# Platform-specific sources
|
||||
if (WIN32 OR MINGW OR (UNIX AND NOT APPLE))
|
||||
list(APPEND YAZE_APP_CORE_SRC
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "controller.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/editor/editor_manager.h"
|
||||
#include "app/gfx/backend/sdl2_renderer.h" // Add include for new renderer
|
||||
#include "app/gfx/backend/renderer_factory.h" // Use renderer factory for SDL2/SDL3 selection
|
||||
#include "app/gfx/resource/arena.h" // Add include for Arena
|
||||
#include "app/gui/automation/widget_id_registry.h"
|
||||
#include "app/gui/core/background_renderer.h"
|
||||
@@ -20,8 +20,8 @@
|
||||
namespace yaze {
|
||||
|
||||
absl::Status Controller::OnEntry(std::string filename) {
|
||||
// Create renderer FIRST
|
||||
renderer_ = std::make_unique<gfx::SDL2Renderer>();
|
||||
// Create renderer FIRST (uses factory for SDL2/SDL3 selection)
|
||||
renderer_ = gfx::RendererFactory::Create();
|
||||
|
||||
// Call CreateWindow with our renderer
|
||||
RETURN_IF_ERROR(CreateWindow(window_, renderer_.get(), SDL_WINDOW_RESIZABLE));
|
||||
@@ -74,7 +74,8 @@ absl::Status Controller::OnLoad() {
|
||||
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
|
||||
window_flags |=
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus |
|
||||
ImGuiWindowFlags_NoBackground;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
@@ -89,6 +90,7 @@ absl::Status Controller::OnLoad() {
|
||||
|
||||
editor_manager_.DrawMenuBar(); // Draw the fixed menu bar at the top
|
||||
|
||||
gui::DockSpaceRenderer::EndEnhancedDockSpace();
|
||||
ImGui::End();
|
||||
#endif
|
||||
gui::WidgetIdRegistry::Instance().BeginFrame();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_CORE_CONTROLLER_H
|
||||
#define YAZE_APP_CORE_CONTROLLER_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "app/editor/agent/agent_chat_widget.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
@@ -478,21 +478,46 @@ void AgentChatWidget::RenderMessage(const ChatMessage& msg, int index) {
|
||||
const auto& theme = AgentUI::GetTheme();
|
||||
|
||||
const bool from_user = (msg.sender == ChatMessage::Sender::kUser);
|
||||
const ImVec4 header_color =
|
||||
from_user ? theme.user_message_color : theme.agent_message_color;
|
||||
|
||||
// Message Bubble Styling
|
||||
float window_width = ImGui::GetContentRegionAvail().x;
|
||||
float bubble_max_width = window_width * 0.85f;
|
||||
|
||||
// Align user messages to right, agent to left
|
||||
if (from_user) {
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (window_width - bubble_max_width) - 20.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
|
||||
}
|
||||
|
||||
ImVec4 bg_color = from_user ? ImVec4(0.2f, 0.4f, 0.8f, 0.2f) : ImVec4(0.3f, 0.3f, 0.3f, 0.2f);
|
||||
ImVec4 border_color = from_user ? ImVec4(0.3f, 0.5f, 0.9f, 0.5f) : ImVec4(0.4f, 0.4f, 0.4f, 0.5f);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, bg_color);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, border_color);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12, 8));
|
||||
|
||||
// Calculate height based on content (approximate)
|
||||
// For a real robust solution we'd need to calculate text size, but auto-resize child is tricky.
|
||||
// We'll use a group and a background rect instead of a child for dynamic height.
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
// Using Group + Rect approach for dynamic height bubbles
|
||||
ImGui::BeginGroup();
|
||||
|
||||
// Header
|
||||
const ImVec4 header_color = from_user ? theme.user_message_color : theme.agent_message_color;
|
||||
const char* header_label = from_user ? "You" : "Agent";
|
||||
|
||||
|
||||
ImGui::TextColored(header_color, "%s", header_label);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(
|
||||
"%s", absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone())
|
||||
.c_str());
|
||||
ImGui::TextDisabled("%s", absl::FormatTime("%H:%M", msg.timestamp, absl::LocalTimeZone()).c_str());
|
||||
|
||||
// Add copy button for all messages
|
||||
// Copy Button (small and subtle)
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, theme.button_copy);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, theme.button_copy_hover);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
||||
if (ImGui::SmallButton(ICON_MD_CONTENT_COPY)) {
|
||||
std::string copy_text = msg.message;
|
||||
if (copy_text.empty() && msg.json_pretty.has_value()) {
|
||||
@@ -500,23 +525,17 @@ void AgentChatWidget::RenderMessage(const ChatMessage& msg, int index) {
|
||||
}
|
||||
ImGui::SetClipboardText(copy_text.c_str());
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Message copied", ToastType::kSuccess, 2.0f);
|
||||
toast_manager_->Show("Copied", ToastType::kSuccess, 1.0f);
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Copy to clipboard");
|
||||
}
|
||||
|
||||
ImGui::Indent();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Content
|
||||
if (msg.table_data.has_value()) {
|
||||
RenderTable(*msg.table_data);
|
||||
} else if (msg.json_pretty.has_value()) {
|
||||
// Don't show JSON as a message - it's internal structure
|
||||
const auto& theme = AgentUI::GetTheme();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, theme.json_text_color);
|
||||
ImGui::TextDisabled(ICON_MD_DATA_OBJECT " (Structured response)");
|
||||
ImGui::TextDisabled(ICON_MD_DATA_OBJECT " (Structured Data)");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::TextWrapped("%s", msg.message.c_str());
|
||||
@@ -526,9 +545,19 @@ void AgentChatWidget::RenderMessage(const ChatMessage& msg, int index) {
|
||||
RenderProposalQuickActions(msg, index);
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
ImGui::EndGroup();
|
||||
|
||||
// Draw background rect
|
||||
ImVec2 p_min = ImGui::GetItemRectMin();
|
||||
ImVec2 p_max = ImGui::GetItemRectMax();
|
||||
p_min.x -= 8; p_min.y -= 4;
|
||||
p_max.x += 8; p_max.y += 4;
|
||||
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, ImGui::GetColorU32(bg_color), 8.0f);
|
||||
ImGui::GetWindowDrawList()->AddRect(p_min, p_max, ImGui::GetColorU32(border_color), 8.0f);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing(); // Extra spacing between messages
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
@@ -2136,11 +2165,17 @@ void AgentChatWidget::RenderAgentConfigPanel() {
|
||||
}
|
||||
|
||||
void AgentChatWidget::RenderModelConfigControls() {
|
||||
const auto& theme = AgentUI::GetTheme();
|
||||
|
||||
// Provider selection buttons using theme colors
|
||||
auto provider_button = [&](const char* label, const char* value,
|
||||
const ImVec4& color) {
|
||||
bool active = agent_config_.ai_provider == value;
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, color);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(color.x * 1.15f, color.y * 1.15f,
|
||||
color.z * 1.15f, color.w));
|
||||
}
|
||||
if (ImGui::Button(label, ImVec2(90, 28))) {
|
||||
agent_config_.ai_provider = value;
|
||||
@@ -2148,51 +2183,54 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
sizeof(agent_config_.provider_buffer), "%s", value);
|
||||
}
|
||||
if (active) {
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
};
|
||||
|
||||
const auto& theme = AgentUI::GetTheme();
|
||||
provider_button(ICON_MD_SETTINGS " Mock", "mock", theme.provider_mock);
|
||||
provider_button(ICON_MD_CLOUD " Ollama", "ollama", theme.provider_ollama);
|
||||
provider_button(ICON_MD_SMART_TOY " Gemini", "gemini", theme.provider_gemini);
|
||||
ImGui::NewLine();
|
||||
ImGui::NewLine();
|
||||
|
||||
// Provider-specific configuration
|
||||
if (agent_config_.ai_provider == "ollama") {
|
||||
if (ImGui::InputTextWithHint(
|
||||
"##ollama_host", "http://localhost:11434",
|
||||
agent_config_.ollama_host_buffer,
|
||||
IM_ARRAYSIZE(agent_config_.ollama_host_buffer))) {
|
||||
agent_config_.ollama_host = agent_config_.ollama_host_buffer;
|
||||
}
|
||||
} else if (agent_config_.ai_provider == "gemini") {
|
||||
if (ImGui::InputTextWithHint("##gemini_key", "API key...",
|
||||
agent_config_.gemini_key_buffer,
|
||||
IM_ARRAYSIZE(agent_config_.gemini_key_buffer),
|
||||
ImGuiInputTextFlags_Password)) {
|
||||
agent_config_.gemini_api_key = agent_config_.gemini_key_buffer;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(ICON_MD_SYNC " Env")) {
|
||||
const char* env_key = std::getenv("GEMINI_API_KEY");
|
||||
if (env_key) {
|
||||
std::snprintf(agent_config_.gemini_key_buffer,
|
||||
sizeof(agent_config_.gemini_key_buffer), "%s", env_key);
|
||||
agent_config_.gemini_api_key = env_key;
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Loaded GEMINI_API_KEY from environment",
|
||||
ToastType::kInfo, 2.0f);
|
||||
}
|
||||
} else if (toast_manager_) {
|
||||
toast_manager_->Show("GEMINI_API_KEY not set", ToastType::kWarning,
|
||||
2.0f);
|
||||
// Provider-specific configuration (always show both for unified access)
|
||||
ImGui::Text("Ollama Host:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (ImGui::InputTextWithHint("##ollama_host", "http://localhost:11434",
|
||||
agent_config_.ollama_host_buffer,
|
||||
IM_ARRAYSIZE(agent_config_.ollama_host_buffer))) {
|
||||
agent_config_.ollama_host = agent_config_.ollama_host_buffer;
|
||||
}
|
||||
|
||||
ImGui::Text("Gemini Key:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
|
||||
if (ImGui::InputTextWithHint("##gemini_key", "API key...",
|
||||
agent_config_.gemini_key_buffer,
|
||||
IM_ARRAYSIZE(agent_config_.gemini_key_buffer),
|
||||
ImGuiInputTextFlags_Password)) {
|
||||
agent_config_.gemini_api_key = agent_config_.gemini_key_buffer;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(ICON_MD_SYNC " Env")) {
|
||||
const char* env_key = std::getenv("GEMINI_API_KEY");
|
||||
if (env_key) {
|
||||
std::snprintf(agent_config_.gemini_key_buffer,
|
||||
sizeof(agent_config_.gemini_key_buffer), "%s", env_key);
|
||||
agent_config_.gemini_api_key = env_key;
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Loaded GEMINI_API_KEY from environment",
|
||||
ToastType::kInfo, 2.0f);
|
||||
}
|
||||
} else if (toast_manager_) {
|
||||
toast_manager_->Show("GEMINI_API_KEY not set", ToastType::kWarning, 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Unified Model Selection
|
||||
if (ImGui::InputTextWithHint("##ai_model", "Model name...",
|
||||
agent_config_.model_buffer,
|
||||
@@ -2200,6 +2238,13 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
agent_config_.ai_model = agent_config_.model_buffer;
|
||||
}
|
||||
|
||||
// Provider filter checkbox for unified model list
|
||||
static bool filter_by_provider = false;
|
||||
ImGui::Checkbox("Filter by selected provider", &filter_by_provider);
|
||||
ImGui::SameLine();
|
||||
AgentUI::HorizontalSpacing(8.0f);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
|
||||
ImGui::InputTextWithHint("##model_search", "Search all models...",
|
||||
model_search_buffer_,
|
||||
@@ -2209,19 +2254,38 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
RefreshModels();
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.14f, 0.9f));
|
||||
// Use theme color for model list background
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
|
||||
ImGui::BeginChild("UnifiedModelList", ImVec2(0, 140), true);
|
||||
std::string filter = absl::AsciiStrToLower(model_search_buffer_);
|
||||
|
||||
if (model_info_cache_.empty() && model_name_cache_.empty()) {
|
||||
ImGui::TextDisabled("No cached models. Refresh to discover.");
|
||||
} else {
|
||||
// Helper lambda to get provider color
|
||||
auto get_provider_color = [&theme](const std::string& provider) -> ImVec4 {
|
||||
if (provider == "ollama") {
|
||||
return theme.provider_ollama;
|
||||
} else if (provider == "gemini") {
|
||||
return theme.provider_gemini;
|
||||
}
|
||||
return theme.provider_mock;
|
||||
};
|
||||
|
||||
// Prefer rich metadata if available
|
||||
if (!model_info_cache_.empty()) {
|
||||
int model_index = 0;
|
||||
for (const auto& info : model_info_cache_) {
|
||||
std::string lower_name = absl::AsciiStrToLower(info.name);
|
||||
std::string lower_provider = absl::AsciiStrToLower(info.provider);
|
||||
|
||||
// Provider filtering
|
||||
if (filter_by_provider &&
|
||||
info.provider != agent_config_.ai_provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Text search filtering
|
||||
if (!filter.empty()) {
|
||||
bool match = lower_name.find(filter) != std::string::npos ||
|
||||
lower_provider.find(filter) != std::string::npos;
|
||||
@@ -2229,16 +2293,32 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
match = absl::AsciiStrToLower(info.parameter_size).find(filter) !=
|
||||
std::string::npos;
|
||||
}
|
||||
if (!match && !info.family.empty()) {
|
||||
match = absl::AsciiStrToLower(info.family).find(filter) !=
|
||||
std::string::npos;
|
||||
}
|
||||
if (!match)
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_selected = agent_config_.ai_model == info.name;
|
||||
// Display provider badge
|
||||
std::string label =
|
||||
absl::StrFormat("%s [%s]", info.name, info.provider);
|
||||
ImGui::PushID(model_index++);
|
||||
|
||||
if (ImGui::Selectable(label.c_str(), is_selected)) {
|
||||
bool is_selected = agent_config_.ai_model == info.name;
|
||||
|
||||
// Colored provider badge
|
||||
ImVec4 provider_color = get_provider_color(info.provider);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, provider_color);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2));
|
||||
ImGui::SmallButton(info.provider.c_str());
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
|
||||
// Model name as selectable
|
||||
if (ImGui::Selectable(info.name.c_str(), is_selected,
|
||||
ImGuiSelectableFlags_None,
|
||||
ImVec2(ImGui::GetContentRegionAvail().x - 60, 0))) {
|
||||
agent_config_.ai_model = info.name;
|
||||
agent_config_.ai_provider = info.provider;
|
||||
std::snprintf(agent_config_.model_buffer,
|
||||
@@ -2255,6 +2335,9 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
std::find(agent_config_.favorite_models.begin(),
|
||||
agent_config_.favorite_models.end(),
|
||||
info.name) != agent_config_.favorite_models.end();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
is_favorite ? theme.status_warning
|
||||
: theme.text_secondary_color);
|
||||
if (ImGui::SmallButton(is_favorite ? ICON_MD_STAR
|
||||
: ICON_MD_STAR_BORDER)) {
|
||||
if (is_favorite) {
|
||||
@@ -2270,6 +2353,7 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
agent_config_.favorite_models.push_back(info.name);
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(is_favorite ? "Remove from favorites"
|
||||
: "Favorite model");
|
||||
@@ -2294,26 +2378,42 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
ImGui::SetTooltip("Capture preset from this model");
|
||||
}
|
||||
|
||||
// Metadata
|
||||
// Metadata display with theme colors
|
||||
std::string size_label = info.parameter_size.empty()
|
||||
? FormatByteSize(info.size_bytes)
|
||||
: info.parameter_size;
|
||||
ImGui::TextDisabled("%s • %s", size_label.c_str(),
|
||||
info.quantization.c_str());
|
||||
if (!info.family.empty()) {
|
||||
ImGui::TextDisabled("Family: %s", info.family.c_str());
|
||||
ImGui::TextColored(theme.text_secondary_color, " %s",
|
||||
size_label.c_str());
|
||||
if (!info.quantization.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(theme.text_info, " %s", info.quantization.c_str());
|
||||
}
|
||||
if (!info.family.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(theme.text_secondary_gray, " Family: %s",
|
||||
info.family.c_str());
|
||||
}
|
||||
if (info.is_local) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(theme.status_success, " " ICON_MD_COMPUTER);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Running locally");
|
||||
}
|
||||
}
|
||||
// ModifiedAt not available in ModelInfo yet
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
} else {
|
||||
// Fallback to just names
|
||||
// Fallback to just names (no rich metadata)
|
||||
int model_index = 0;
|
||||
for (const auto& model_name : model_name_cache_) {
|
||||
std::string lower = absl::AsciiStrToLower(model_name);
|
||||
if (!filter.empty() && lower.find(filter) == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::PushID(model_index++);
|
||||
|
||||
bool is_selected = agent_config_.ai_model == model_name;
|
||||
if (ImGui::Selectable(model_name.c_str(), is_selected)) {
|
||||
agent_config_.ai_model = model_name;
|
||||
@@ -2327,6 +2427,9 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
std::find(agent_config_.favorite_models.begin(),
|
||||
agent_config_.favorite_models.end(),
|
||||
model_name) != agent_config_.favorite_models.end();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
is_favorite ? theme.status_warning
|
||||
: theme.text_secondary_color);
|
||||
if (ImGui::SmallButton(is_favorite ? ICON_MD_STAR
|
||||
: ICON_MD_STAR_BORDER)) {
|
||||
if (is_favorite) {
|
||||
@@ -2338,7 +2441,9 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
agent_config_.favorite_models.push_back(model_name);
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2358,19 +2463,53 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
|
||||
if (!agent_config_.favorite_models.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f),
|
||||
ICON_MD_STAR " Favorites");
|
||||
ImGui::TextColored(theme.status_warning, ICON_MD_STAR " Favorites");
|
||||
for (size_t i = 0; i < agent_config_.favorite_models.size(); ++i) {
|
||||
auto& favorite = agent_config_.favorite_models[i];
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
bool active = agent_config_.ai_model == favorite;
|
||||
|
||||
// Find provider info for this favorite if available
|
||||
std::string provider_name;
|
||||
for (const auto& info : model_info_cache_) {
|
||||
if (info.name == favorite) {
|
||||
provider_name = info.provider;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show provider badge if known
|
||||
if (!provider_name.empty()) {
|
||||
ImVec4 badge_color = theme.provider_mock;
|
||||
if (provider_name == "ollama") {
|
||||
badge_color = theme.provider_ollama;
|
||||
} else if (provider_name == "gemini") {
|
||||
badge_color = theme.provider_gemini;
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, badge_color);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3, 1));
|
||||
ImGui::SmallButton(provider_name.c_str());
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (ImGui::Selectable(favorite.c_str(), active)) {
|
||||
agent_config_.ai_model = favorite;
|
||||
std::snprintf(agent_config_.model_buffer,
|
||||
sizeof(agent_config_.model_buffer), "%s",
|
||||
favorite.c_str());
|
||||
// Also set provider if known
|
||||
if (!provider_name.empty()) {
|
||||
agent_config_.ai_provider = provider_name;
|
||||
std::snprintf(agent_config_.provider_buffer,
|
||||
sizeof(agent_config_.provider_buffer), "%s",
|
||||
provider_name.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, theme.status_error);
|
||||
if (ImGui::SmallButton(ICON_MD_CLOSE)) {
|
||||
agent_config_.model_chain.erase(
|
||||
std::remove(agent_config_.model_chain.begin(),
|
||||
@@ -2378,15 +2517,19 @@ void AgentChatWidget::RenderModelConfigControls() {
|
||||
agent_config_.model_chain.end());
|
||||
agent_config_.favorite_models.erase(
|
||||
agent_config_.favorite_models.begin() + i);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgentChatWidget::RenderModelDeck() {
|
||||
const auto& theme = AgentUI::GetTheme();
|
||||
|
||||
ImGui::TextDisabled("Model Deck");
|
||||
if (agent_config_.model_presets.empty()) {
|
||||
ImGui::TextWrapped(
|
||||
@@ -2402,7 +2545,7 @@ void AgentChatWidget::RenderModelDeck() {
|
||||
: agent_config_.ai_model;
|
||||
preset.model = agent_config_.ai_model;
|
||||
preset.host = agent_config_.ollama_host;
|
||||
preset.tags = {"current"};
|
||||
preset.tags = {agent_config_.ai_provider}; // Use current provider as tag
|
||||
preset.last_used = absl::Now();
|
||||
agent_config_.model_presets.push_back(std::move(preset));
|
||||
new_preset_name_[0] = '\0';
|
||||
@@ -2411,7 +2554,8 @@ void AgentChatWidget::RenderModelDeck() {
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.09f, 0.09f, 0.11f, 0.9f));
|
||||
// Use theme color for preset list background
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
|
||||
ImGui::BeginChild("PresetList", ImVec2(0, 110), true);
|
||||
if (agent_config_.model_presets.empty()) {
|
||||
ImGui::TextDisabled("No presets yet");
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
// Centralized UI theme
|
||||
#include "app/gui/style/theme.h"
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/clock.h"
|
||||
@@ -163,9 +166,14 @@ void AgentEditor::DrawDashboard() {
|
||||
|
||||
// Pulsing glow for window
|
||||
float pulse = 0.5f + 0.5f * std::sin(pulse_animation_);
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBgActive,
|
||||
ImVec4(0.1f + 0.1f * pulse, 0.2f + 0.15f * pulse,
|
||||
0.3f + 0.2f * pulse, 1.0f));
|
||||
// Apply theme primary color with pulsing effect
|
||||
const auto& theme = yaze::gui::style::DefaultTheme();
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBgActive,
|
||||
ImVec4(theme.primary.x + 0.1f * pulse,
|
||||
theme.primary.y + 0.15f * pulse,
|
||||
theme.primary.z + 0.2f * pulse,
|
||||
1.0f));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(1200, 800), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin(ICON_MD_SMART_TOY " AI AGENT PLATFORM [v0.4.x]", &active_,
|
||||
@@ -328,6 +336,7 @@ void AgentEditor::DrawDashboard() {
|
||||
}
|
||||
|
||||
void AgentEditor::DrawConfigurationPanel() {
|
||||
const auto& theme = yaze::gui::style::DefaultTheme();
|
||||
// AI Provider Configuration
|
||||
if (ImGui::CollapsingHeader(ICON_MD_SETTINGS " AI Provider",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
@@ -343,7 +352,7 @@ void AgentEditor::DrawConfigurationPanel() {
|
||||
bool is_gemini = (current_profile_.provider == "gemini");
|
||||
|
||||
if (is_mock)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.6f, 0.6f, 0.8f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, theme.secondary);
|
||||
if (ImGui::Button(ICON_MD_SETTINGS " Mock", button_size)) {
|
||||
current_profile_.provider = "mock";
|
||||
}
|
||||
@@ -352,7 +361,9 @@ void AgentEditor::DrawConfigurationPanel() {
|
||||
|
||||
ImGui::SameLine();
|
||||
if (is_ollama)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.4f, 0.8f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(theme.secondary.x * 1.2f,
|
||||
theme.secondary.y * 1.2f,
|
||||
theme.secondary.z * 1.2f, 1.0f));
|
||||
if (ImGui::Button(ICON_MD_CLOUD " Ollama", button_size)) {
|
||||
current_profile_.provider = "ollama";
|
||||
}
|
||||
@@ -361,7 +372,7 @@ void AgentEditor::DrawConfigurationPanel() {
|
||||
|
||||
ImGui::SameLine();
|
||||
if (is_gemini)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.196f, 0.6f, 0.8f, 0.8f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, theme.primary);
|
||||
if (ImGui::Button(ICON_MD_SMART_TOY " Gemini", button_size)) {
|
||||
current_profile_.provider = "gemini";
|
||||
}
|
||||
@@ -443,7 +454,7 @@ void AgentEditor::DrawConfigurationPanel() {
|
||||
current_profile_.gemini_api_key = key_buf;
|
||||
}
|
||||
if (!current_profile_.gemini_api_key.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.133f, 0.545f, 0.133f, 1.0f),
|
||||
ImGui::TextColored(theme.success,
|
||||
ICON_MD_CHECK_CIRCLE " API key configured");
|
||||
}
|
||||
} else {
|
||||
@@ -520,9 +531,9 @@ void AgentEditor::DrawConfigurationPanel() {
|
||||
|
||||
// Apply button
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.133f, 0.545f, 0.133f, 0.8f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, theme.success);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(0.133f, 0.545f, 0.133f, 1.0f));
|
||||
ImVec4(theme.success.x * 1.2f, theme.success.y * 1.2f, theme.success.z * 1.2f, 1.0f));
|
||||
if (ImGui::Button(ICON_MD_CHECK " Apply & Save Configuration",
|
||||
ImVec2(-1, 40))) {
|
||||
// Update legacy config
|
||||
@@ -637,7 +648,7 @@ void AgentEditor::DrawPromptEditorPanel() {
|
||||
prompt_editor_initialized_ = false;
|
||||
}
|
||||
if (ImGui::Selectable("system_prompt_v3.txt",
|
||||
active_prompt_file_ == "system_prompt_v3.txt")) {
|
||||
active_prompt_file_ == "system_prompt_v3.2.txt")) {
|
||||
active_prompt_file_ = "system_prompt_v3.txt";
|
||||
prompt_editor_initialized_ = false;
|
||||
}
|
||||
@@ -715,6 +726,7 @@ void AgentEditor::DrawPromptEditorPanel() {
|
||||
}
|
||||
|
||||
void AgentEditor::DrawBotProfilesPanel() {
|
||||
const auto& theme = yaze::gui::style::DefaultTheme();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f),
|
||||
ICON_MD_FOLDER " Bot Profile Manager");
|
||||
ImGui::Separator();
|
||||
@@ -771,8 +783,7 @@ void AgentEditor::DrawBotProfilesPanel() {
|
||||
|
||||
bool is_current = (profile.name == current_profile_.name);
|
||||
if (is_current) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
ImVec4(0.196f, 0.6f, 0.8f, 0.6f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, theme.primary); // Use theme.primary for current
|
||||
}
|
||||
|
||||
if (ImGui::Button(profile.name.c_str(),
|
||||
@@ -790,7 +801,7 @@ void AgentEditor::DrawBotProfilesPanel() {
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 0.6f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, theme.warning);
|
||||
if (ImGui::SmallButton(ICON_MD_DELETE)) {
|
||||
DeleteBotProfile(profile.name);
|
||||
if (toast_manager_) {
|
||||
@@ -1287,7 +1298,7 @@ void AgentEditor::DrawAgentBuilderPanel() {
|
||||
toast_manager_->Show("Builder blueprint saved", ToastType::kSuccess,
|
||||
2.0f);
|
||||
} else {
|
||||
toast_manager_->Show(std::string(status.message()), ToastType::kError,
|
||||
toast_manager_->Show(std::string(status.message().data(), status.message().size()), ToastType::kError,
|
||||
3.5f);
|
||||
}
|
||||
}
|
||||
@@ -1300,7 +1311,7 @@ void AgentEditor::DrawAgentBuilderPanel() {
|
||||
toast_manager_->Show("Builder blueprint loaded", ToastType::kSuccess,
|
||||
2.0f);
|
||||
} else {
|
||||
toast_manager_->Show(std::string(status.message()), ToastType::kError,
|
||||
toast_manager_->Show(std::string(status.message().data(), status.message().size()), ToastType::kError,
|
||||
3.5f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ void ProjectFileEditor::Draw() {
|
||||
if (!file.empty()) {
|
||||
auto status = LoadFile(file);
|
||||
if (!status.ok() && toast_manager_) {
|
||||
toast_manager_->Show(std::string(status.message()),
|
||||
toast_manager_->Show(std::string(status.message().data(), status.message().size()),
|
||||
ToastType::kError);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ void ProjectFileEditor::Draw() {
|
||||
if (status.ok() && toast_manager_) {
|
||||
toast_manager_->Show("Project file saved", ToastType::kSuccess);
|
||||
} else if (!status.ok() && toast_manager_) {
|
||||
toast_manager_->Show(std::string(status.message()), ToastType::kError);
|
||||
toast_manager_->Show(std::string(status.message().data(), status.message().size()), ToastType::kError);
|
||||
}
|
||||
}
|
||||
if (!can_save)
|
||||
@@ -79,7 +79,7 @@ void ProjectFileEditor::Draw() {
|
||||
if (status.ok() && toast_manager_) {
|
||||
toast_manager_->Show("Project file saved", ToastType::kSuccess);
|
||||
} else if (!status.ok() && toast_manager_) {
|
||||
toast_manager_->Show(std::string(status.message()),
|
||||
toast_manager_->Show(std::string(status.message().data(), status.message().size()),
|
||||
ToastType::kError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,9 +263,18 @@ absl::Status DungeonEditorV2::Save() {
|
||||
auto status = room.SaveObjects();
|
||||
if (!status.ok()) {
|
||||
// Log error but continue with other rooms
|
||||
LOG_ERROR("DungeonEditorV2", "Failed to save room: %s",
|
||||
LOG_ERROR("DungeonEditorV2", "Failed to save room objects: %s",
|
||||
status.message().data());
|
||||
}
|
||||
|
||||
// Save sprites and other entities via system
|
||||
if (dungeon_editor_system_) {
|
||||
auto sys_status = dungeon_editor_system_->SaveRoom(room.id());
|
||||
if (!sys_status.ok()) {
|
||||
LOG_ERROR("DungeonEditorV2", "Failed to save room system data: %s",
|
||||
sys_status.message().data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save additional dungeon state (stubbed) via DungeonEditorSystem when present
|
||||
@@ -398,6 +407,15 @@ void DungeonEditorV2::DrawRoomTab(int room_id) {
|
||||
status.message().data());
|
||||
return;
|
||||
}
|
||||
|
||||
// Load system data for this room (sprites, etc.)
|
||||
if (dungeon_editor_system_) {
|
||||
auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
|
||||
if (!sys_status.ok()) {
|
||||
LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
|
||||
sys_status.message().data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize room graphics and objects in CORRECT ORDER
|
||||
|
||||
@@ -144,23 +144,13 @@ if(YAZE_BUILD_TESTS)
|
||||
target_link_libraries(yaze_editor PUBLIC ImGuiTestEngine)
|
||||
message(STATUS "✓ yaze_editor linked to ImGuiTestEngine")
|
||||
endif()
|
||||
|
||||
if(TARGET yaze_test_support)
|
||||
# Use whole-archive on Unix to ensure test symbols are included
|
||||
# This is needed because editor_manager.cc calls test functions conditionally
|
||||
if(APPLE)
|
||||
target_link_options(yaze_editor PUBLIC
|
||||
"LINKER:-force_load,$<TARGET_FILE:yaze_test_support>")
|
||||
target_link_libraries(yaze_editor PUBLIC yaze_test_support)
|
||||
elseif(UNIX)
|
||||
target_link_libraries(yaze_editor PUBLIC
|
||||
-Wl,--whole-archive yaze_test_support -Wl,--no-whole-archive)
|
||||
else()
|
||||
# Windows: Normal linking (no whole-archive needed, symbols resolve correctly)
|
||||
target_link_libraries(yaze_editor PUBLIC yaze_test_support)
|
||||
endif()
|
||||
message(STATUS "✓ yaze_editor linked to yaze_test_support")
|
||||
endif()
|
||||
|
||||
# NOTE: yaze_editor should NOT force-load yaze_test_support to avoid circular dependency.
|
||||
# The chain yaze_editor -> force_load(yaze_test_support) -> yaze_editor causes SIGSEGV
|
||||
# during static initialization.
|
||||
#
|
||||
# Test executables should link yaze_test_support directly, which provides all needed
|
||||
# symbols through its own dependencies (including yaze_editor via regular linking).
|
||||
endif()
|
||||
|
||||
# Conditionally link gRPC if enabled
|
||||
|
||||
@@ -1235,6 +1235,8 @@ absl::Status Tile16Editor::LoadTile8() {
|
||||
std::vector<uint8_t> tile_data(64); // 8x8 = 64 pixels
|
||||
|
||||
// Extract tile data from the main graphics bitmap
|
||||
// Keep raw 4-bit pixel values (0-15); palette offset is applied in
|
||||
// RefreshAllPalettes() via SetPaletteWithTransparent
|
||||
for (int py = 0; py < 8; ++py) {
|
||||
for (int px = 0; px < 8; ++px) {
|
||||
int src_x = tile_x * 8 + px;
|
||||
@@ -1246,10 +1248,9 @@ absl::Status Tile16Editor::LoadTile8() {
|
||||
dst_index < 64) {
|
||||
uint8_t pixel_value = current_gfx_bmp_.data()[src_index];
|
||||
|
||||
// Apply normalization based on settings
|
||||
if (auto_normalize_pixels_) {
|
||||
pixel_value &= palette_normalization_mask_;
|
||||
}
|
||||
// Normalize to 4-bit range for proper SNES 4bpp graphics
|
||||
// The actual palette offset is applied during palette refresh
|
||||
pixel_value &= 0x0F;
|
||||
|
||||
tile_data[dst_index] = pixel_value;
|
||||
}
|
||||
@@ -1324,7 +1325,8 @@ absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
|
||||
|
||||
tile_data.resize(kTile16PixelCount);
|
||||
|
||||
// Manual extraction without the buggy offset increment
|
||||
// Manual extraction - preserve pixel values for palette-based rendering
|
||||
// The 4-bit mask is applied after extraction to normalize values
|
||||
for (int ty = 0; ty < kTile16Size; ty++) {
|
||||
for (int tx = 0; tx < kTile16Size; tx++) {
|
||||
int pixel_x = tile_x + tx;
|
||||
@@ -1335,36 +1337,59 @@ absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
|
||||
if (src_index < static_cast<int>(tile16_blockset_->atlas.size()) &&
|
||||
dst_index < static_cast<int>(tile_data.size())) {
|
||||
uint8_t pixel_value = tile16_blockset_->atlas.data()[src_index];
|
||||
// Normalize pixel values to valid palette range
|
||||
pixel_value &= 0x0F; // Keep only lower 4 bits for palette index
|
||||
// Normalize pixel values to 4-bit range for sub-palette indexing
|
||||
// The actual palette offset is applied via SetPaletteWithTransparent
|
||||
pixel_value &= 0x0F;
|
||||
tile_data[dst_index] = pixel_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normalize the extracted data based on settings
|
||||
if (auto_normalize_pixels_) {
|
||||
for (auto& pixel : tile_data) {
|
||||
pixel &= palette_normalization_mask_;
|
||||
}
|
||||
// Normalize the extracted data to 4-bit range
|
||||
for (auto& pixel : tile_data) {
|
||||
pixel &= 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the bitmap with the extracted data
|
||||
current_tile16_bmp_.Create(kTile16Size, kTile16Size, 8, tile_data);
|
||||
|
||||
// Use the same palette system as the overworld (complete 256-color palette)
|
||||
// CRITICAL FIX: Use SetPaletteWithTransparent with proper palette offset
|
||||
// based on current_palette_ selection and default sheet (sheet 0 for tile16)
|
||||
gfx::SnesPalette display_palette;
|
||||
if (overworld_palette_.size() >= 256) {
|
||||
// Use complete 256-color palette (same as overworld system)
|
||||
// The pixel data already contains correct color indices for the 256-color
|
||||
// palette
|
||||
current_tile16_bmp_.SetPalette(overworld_palette_);
|
||||
display_palette = overworld_palette_;
|
||||
} else if (palette_.size() >= 256) {
|
||||
current_tile16_bmp_.SetPalette(palette_);
|
||||
display_palette = palette_;
|
||||
} else if (rom()->palette_group().overworld_main.size() > 0) {
|
||||
current_tile16_bmp_.SetPalette(rom()->palette_group().overworld_main[0]);
|
||||
display_palette = rom()->palette_group().overworld_main[0];
|
||||
}
|
||||
|
||||
// Calculate palette offset: use sheet 0 (main blockset) as default for tile16
|
||||
// palette_base * 16 gives the row offset, current_palette_ * 8 gives
|
||||
// sub-palette
|
||||
int palette_base = GetPaletteBaseForSheet(0); // Default to main blockset
|
||||
size_t palette_offset = (palette_base * 16) + (current_palette_ * 8);
|
||||
|
||||
// Defensive checks: ensure palette is present and offset is valid
|
||||
if (display_palette.empty()) {
|
||||
util::logf("Tile16Editor: display palette empty; falling back to offset 0");
|
||||
return absl::FailedPreconditionError("display palette unavailable");
|
||||
}
|
||||
if (palette_offset + 7 >= display_palette.size()) {
|
||||
util::logf("Tile16Editor: palette offset %zu out of range (size=%zu); "
|
||||
"using offset 0",
|
||||
palette_offset, display_palette.size());
|
||||
palette_offset = 0;
|
||||
if (display_palette.size() < 8) {
|
||||
return absl::FailedPreconditionError("display palette too small");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the correct sub-palette with transparency
|
||||
current_tile16_bmp_.SetPaletteWithTransparent(display_palette, palette_offset,
|
||||
7);
|
||||
|
||||
// Queue texture creation via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
|
||||
¤t_tile16_bmp_);
|
||||
@@ -2062,6 +2087,36 @@ int Tile16Editor::GetActualPaletteSlotForCurrentTile16() const {
|
||||
return GetActualPaletteSlot(current_palette_, 0);
|
||||
}
|
||||
|
||||
int Tile16Editor::GetPaletteBaseForSheet(int sheet_index) const {
|
||||
// Based on overworld palette structure and how ProcessGraphicsBuffer assigns
|
||||
// colors: The 256-color palette is organized as 16 rows of 16 colors each.
|
||||
// Different graphics sheets map to different palette regions:
|
||||
//
|
||||
// Row 0: Transparent/system colors
|
||||
// Row 1: HUD colors (palette index 0x10-0x1F)
|
||||
// Rows 2-4: MAIN/AUX1 palette region for main graphics
|
||||
// Rows 5-7: AUX2 palette region for area-specific graphics
|
||||
// Row 7: ANIMATED palette for animated tiles
|
||||
//
|
||||
// The palette_button (0-7) selects within the region.
|
||||
switch (sheet_index) {
|
||||
case 0: // Main blockset
|
||||
case 3: // Area graphics set 1
|
||||
case 4: // Area graphics set 2
|
||||
return 2; // AUX1 palette region starts at row 2
|
||||
case 5: // Area graphics set 3
|
||||
case 6: // Area graphics set 4
|
||||
return 5; // AUX2 palette region starts at row 5
|
||||
case 1: // Main graphics
|
||||
case 2: // Main graphics
|
||||
return 2; // MAIN palette region starts at row 2
|
||||
case 7: // Animated tiles
|
||||
return 7; // ANIMATED palette region at row 7
|
||||
default:
|
||||
return 2; // Default to MAIN region
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for palette management
|
||||
absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
|
||||
if (tile8_id < 0 ||
|
||||
@@ -2187,13 +2242,29 @@ absl::Status Tile16Editor::RefreshAllPalettes() {
|
||||
gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_);
|
||||
}
|
||||
|
||||
// Update all individual tile8 graphics with complete 256-color palette
|
||||
// CRITICAL FIX: Update individual tile8 graphics with proper palette offsets
|
||||
// Each tile8 belongs to a specific graphics sheet, which maps to a specific
|
||||
// region of the 256-color palette. The current_palette_ (0-7) button selects
|
||||
// within that region.
|
||||
for (size_t i = 0; i < current_gfx_individual_.size(); ++i) {
|
||||
if (current_gfx_individual_[i].is_active()) {
|
||||
// Use complete 256-color palette (same as overworld system)
|
||||
// The pixel data already contains correct color indices for the 256-color
|
||||
// palette
|
||||
current_gfx_individual_[i].SetPalette(display_palette);
|
||||
// Determine which sheet this tile belongs to and get the palette offset
|
||||
int sheet_index = GetSheetIndexForTile8(static_cast<int>(i));
|
||||
int palette_base = GetPaletteBaseForSheet(sheet_index);
|
||||
|
||||
// Calculate the palette offset in the 256-color palette:
|
||||
// - palette_base * 16: row offset in the 16x16 palette grid
|
||||
// - current_palette_: additional offset within the region (0-7 maps to
|
||||
// different sub-palettes)
|
||||
// For 4bpp SNES graphics, we use 8 colors per sub-palette with
|
||||
// transparent index 0
|
||||
size_t palette_offset = (palette_base * 16) + (current_palette_ * 8);
|
||||
|
||||
// Use SetPaletteWithTransparent to apply the correct 8-color sub-palette
|
||||
// This extracts 7 colors starting at palette_offset and creates
|
||||
// transparent index 0
|
||||
current_gfx_individual_[i].SetPaletteWithTransparent(
|
||||
display_palette, palette_offset, 7);
|
||||
current_gfx_individual_[i].set_modified(true);
|
||||
// Queue texture update via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
|
||||
@@ -121,6 +121,10 @@ class Tile16Editor : public gfx::GfxContext {
|
||||
int GetSheetIndexForTile8(int tile8_id) const;
|
||||
int GetActualPaletteSlotForCurrentTile16() const;
|
||||
|
||||
// Get palette base row for a graphics sheet (0-7 range for 256-color palette)
|
||||
// Returns the base row index in the 16-row palette structure
|
||||
int GetPaletteBaseForSheet(int sheet_index) const;
|
||||
|
||||
// ROM data access and modification
|
||||
absl::Status UpdateROMTile16Data();
|
||||
absl::Status RefreshTile16Blockset();
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "app/gui/core/icons.h"
|
||||
#ifdef Z3ED_AI
|
||||
#include "cli/service/rom/rom_sandbox_manager.h"
|
||||
#endif
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
// Policy evaluation support (optional, only in main yaze build)
|
||||
@@ -455,6 +457,7 @@ void ProposalDrawer::FocusProposal(const std::string& proposal_id) {
|
||||
}
|
||||
|
||||
void ProposalDrawer::RefreshProposals() {
|
||||
#ifdef Z3ED_AI
|
||||
auto& registry = cli::ProposalRegistry::Instance();
|
||||
|
||||
std::optional<cli::ProposalRegistry::ProposalStatus> filter;
|
||||
@@ -491,6 +494,7 @@ void ProposalDrawer::RefreshProposals() {
|
||||
log_content_.clear();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ProposalDrawer::SelectProposal(const std::string& proposal_id) {
|
||||
@@ -509,6 +513,7 @@ void ProposalDrawer::SelectProposal(const std::string& proposal_id) {
|
||||
}
|
||||
|
||||
absl::Status ProposalDrawer::AcceptProposal(const std::string& proposal_id) {
|
||||
#ifdef Z3ED_AI
|
||||
auto& registry = cli::ProposalRegistry::Instance();
|
||||
|
||||
// Get proposal metadata to find sandbox
|
||||
@@ -579,18 +584,26 @@ absl::Status ProposalDrawer::AcceptProposal(const std::string& proposal_id) {
|
||||
|
||||
needs_refresh_ = true;
|
||||
return status;
|
||||
#else
|
||||
return absl::UnimplementedError("AI features disabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status ProposalDrawer::RejectProposal(const std::string& proposal_id) {
|
||||
#ifdef Z3ED_AI
|
||||
auto& registry = cli::ProposalRegistry::Instance();
|
||||
auto status = registry.UpdateStatus(
|
||||
proposal_id, cli::ProposalRegistry::ProposalStatus::kRejected);
|
||||
|
||||
needs_refresh_ = true;
|
||||
return status;
|
||||
#else
|
||||
return absl::UnimplementedError("AI features disabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status ProposalDrawer::DeleteProposal(const std::string& proposal_id) {
|
||||
#ifdef Z3ED_AI
|
||||
auto& registry = cli::ProposalRegistry::Instance();
|
||||
auto status = registry.RemoveProposal(proposal_id);
|
||||
|
||||
@@ -603,6 +616,9 @@ absl::Status ProposalDrawer::DeleteProposal(const std::string& proposal_id) {
|
||||
|
||||
needs_refresh_ = true;
|
||||
return status;
|
||||
#else
|
||||
return absl::UnimplementedError("AI features disabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "app/emu/audio/apu.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
#include "app/emu/audio/audio_backend.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
#include "app/emu/audio/sdl3_audio_backend.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace audio {
|
||||
@@ -335,6 +339,14 @@ std::unique_ptr<IAudioBackend> AudioBackendFactory::Create(BackendType type) {
|
||||
case BackendType::SDL2:
|
||||
return std::make_unique<SDL2AudioBackend>();
|
||||
|
||||
case BackendType::SDL3:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return std::make_unique<SDL3AudioBackend>();
|
||||
#else
|
||||
LOG_ERROR("AudioBackend", "SDL3 backend requested but not compiled with SDL3 support");
|
||||
return std::make_unique<SDL2AudioBackend>();
|
||||
#endif
|
||||
|
||||
case BackendType::NULL_BACKEND:
|
||||
// TODO: Implement null backend for testing
|
||||
LOG_WARN("AudioBackend", "NULL backend not yet implemented, using SDL2");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifndef YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H
|
||||
#define YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
458
src/app/emu/audio/sdl3_audio_backend.cc
Normal file
458
src/app/emu/audio/sdl3_audio_backend.cc
Normal file
@@ -0,0 +1,458 @@
|
||||
// sdl3_audio_backend.cc - SDL3 Audio Backend Implementation
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
#include "app/emu/audio/sdl3_audio_backend.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace audio {
|
||||
|
||||
// ============================================================================
|
||||
// SDL3AudioBackend Implementation
|
||||
// ============================================================================
|
||||
|
||||
SDL3AudioBackend::~SDL3AudioBackend() {
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool SDL3AudioBackend::Initialize(const AudioConfig& config) {
|
||||
if (initialized_) {
|
||||
LOG_WARN("AudioBackend", "SDL3 backend already initialized, shutting down first");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
config_ = config;
|
||||
|
||||
// Set up the audio specification for SDL3
|
||||
SDL_AudioSpec spec;
|
||||
spec.format = (config.format == SampleFormat::INT16) ? SDL_AUDIO_S16 : SDL_AUDIO_F32;
|
||||
spec.channels = config.channels;
|
||||
spec.freq = config.sample_rate;
|
||||
|
||||
// SDL3 uses stream-based API - open audio device stream
|
||||
audio_stream_ = SDL_OpenAudioDeviceStream(
|
||||
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, // Use default playback device
|
||||
&spec, // Desired spec
|
||||
nullptr, // Callback (nullptr for stream mode)
|
||||
nullptr // User data
|
||||
);
|
||||
|
||||
if (!audio_stream_) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to open audio stream: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the actual device ID from the stream
|
||||
device_id_ = SDL_GetAudioStreamDevice(audio_stream_);
|
||||
if (!device_id_) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to get audio device from stream");
|
||||
SDL_DestroyAudioStream(audio_stream_);
|
||||
audio_stream_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get actual device format information
|
||||
SDL_AudioSpec obtained_spec;
|
||||
if (SDL_GetAudioDeviceFormat(device_id_, &obtained_spec, nullptr) < 0) {
|
||||
LOG_WARN("AudioBackend", "SDL3: Could not query device format: %s", SDL_GetError());
|
||||
// Use requested values as fallback
|
||||
device_format_ = spec.format;
|
||||
device_channels_ = spec.channels;
|
||||
device_freq_ = spec.freq;
|
||||
} else {
|
||||
device_format_ = obtained_spec.format;
|
||||
device_channels_ = obtained_spec.channels;
|
||||
device_freq_ = obtained_spec.freq;
|
||||
|
||||
// Update config if we got different values
|
||||
if (device_freq_ != config_.sample_rate || device_channels_ != config_.channels) {
|
||||
LOG_WARN("AudioBackend",
|
||||
"SDL3: Audio spec mismatch - wanted %dHz %dch, got %dHz %dch",
|
||||
config_.sample_rate, config_.channels, device_freq_, device_channels_);
|
||||
config_.sample_rate = device_freq_;
|
||||
config_.channels = device_channels_;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("AudioBackend",
|
||||
"SDL3 audio initialized: %dHz, %d channels, format=%d",
|
||||
device_freq_, device_channels_, device_format_);
|
||||
|
||||
initialized_ = true;
|
||||
resampling_enabled_ = false;
|
||||
native_rate_ = 0;
|
||||
native_channels_ = 0;
|
||||
resample_buffer_.clear();
|
||||
|
||||
// Start playback immediately
|
||||
if (SDL_ResumeAudioDevice(device_id_) < 0) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to resume audio device: %s", SDL_GetError());
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::Shutdown() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up resampling stream
|
||||
if (resampling_stream_) {
|
||||
SDL_DestroyAudioStream(resampling_stream_);
|
||||
resampling_stream_ = nullptr;
|
||||
}
|
||||
resampling_enabled_ = false;
|
||||
native_rate_ = 0;
|
||||
native_channels_ = 0;
|
||||
resample_buffer_.clear();
|
||||
|
||||
// Pause device before cleanup
|
||||
if (device_id_) {
|
||||
SDL_PauseAudioDevice(device_id_);
|
||||
}
|
||||
|
||||
// Destroy main audio stream
|
||||
if (audio_stream_) {
|
||||
SDL_DestroyAudioStream(audio_stream_);
|
||||
audio_stream_ = nullptr;
|
||||
}
|
||||
|
||||
device_id_ = 0;
|
||||
initialized_ = false;
|
||||
|
||||
LOG_INFO("AudioBackend", "SDL3 audio shut down");
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::Play() {
|
||||
if (!initialized_ || !device_id_) {
|
||||
return;
|
||||
}
|
||||
SDL_ResumeAudioDevice(device_id_);
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::Pause() {
|
||||
if (!initialized_ || !device_id_) {
|
||||
return;
|
||||
}
|
||||
SDL_PauseAudioDevice(device_id_);
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::Stop() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
Clear();
|
||||
if (device_id_) {
|
||||
SDL_PauseAudioDevice(device_id_);
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::Clear() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (audio_stream_) {
|
||||
SDL_ClearAudioStream(audio_stream_);
|
||||
}
|
||||
|
||||
if (resampling_stream_) {
|
||||
SDL_ClearAudioStream(resampling_stream_);
|
||||
}
|
||||
}
|
||||
|
||||
bool SDL3AudioBackend::QueueSamples(const int16_t* samples, int num_samples) {
|
||||
if (!initialized_ || !audio_stream_ || !samples) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast path: No volume adjustment needed
|
||||
if (volume_ == 1.0f) {
|
||||
int result = SDL_PutAudioStreamData(audio_stream_, samples,
|
||||
num_samples * sizeof(int16_t));
|
||||
if (result < 0) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: SDL_PutAudioStreamData failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Slow path: Volume scaling required
|
||||
thread_local std::vector<int16_t> scaled_samples;
|
||||
|
||||
if (scaled_samples.size() < static_cast<size_t>(num_samples)) {
|
||||
scaled_samples.resize(num_samples);
|
||||
}
|
||||
|
||||
// Apply volume scaling
|
||||
float vol = volume_.load();
|
||||
for (int i = 0; i < num_samples; ++i) {
|
||||
int32_t scaled = static_cast<int32_t>(samples[i] * vol);
|
||||
scaled_samples[i] = static_cast<int16_t>(std::clamp(scaled, -32768, 32767));
|
||||
}
|
||||
|
||||
int result = SDL_PutAudioStreamData(audio_stream_, scaled_samples.data(),
|
||||
num_samples * sizeof(int16_t));
|
||||
if (result < 0) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: SDL_PutAudioStreamData failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDL3AudioBackend::QueueSamples(const float* samples, int num_samples) {
|
||||
if (!initialized_ || !audio_stream_ || !samples) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert float to int16 with volume scaling
|
||||
thread_local std::vector<int16_t> int_samples;
|
||||
if (int_samples.size() < static_cast<size_t>(num_samples)) {
|
||||
int_samples.resize(num_samples);
|
||||
}
|
||||
|
||||
float vol = volume_.load();
|
||||
for (int i = 0; i < num_samples; ++i) {
|
||||
float scaled = std::clamp(samples[i] * vol, -1.0f, 1.0f);
|
||||
int_samples[i] = static_cast<int16_t>(scaled * 32767.0f);
|
||||
}
|
||||
|
||||
return QueueSamples(int_samples.data(), num_samples);
|
||||
}
|
||||
|
||||
bool SDL3AudioBackend::QueueSamplesNative(const int16_t* samples,
|
||||
int frames_per_channel, int channels,
|
||||
int native_rate) {
|
||||
if (!initialized_ || !samples) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we need to set up resampling
|
||||
if (!resampling_enabled_ || !resampling_stream_) {
|
||||
LOG_WARN("AudioBackend", "SDL3: Native rate resampling not enabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the resampling configuration matches
|
||||
if (native_rate != native_rate_ || channels != native_channels_) {
|
||||
SetAudioStreamResampling(true, native_rate, channels);
|
||||
if (!resampling_stream_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const int bytes_in = frames_per_channel * channels * static_cast<int>(sizeof(int16_t));
|
||||
|
||||
// Put data into resampling stream
|
||||
if (SDL_PutAudioStreamData(resampling_stream_, samples, bytes_in) < 0) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to put data in resampling stream: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get available resampled data
|
||||
int available_bytes = SDL_GetAudioStreamAvailable(resampling_stream_);
|
||||
if (available_bytes < 0) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to get available stream data: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (available_bytes == 0) {
|
||||
return true; // No data ready yet
|
||||
}
|
||||
|
||||
// Resize buffer if needed
|
||||
int available_samples = available_bytes / static_cast<int>(sizeof(int16_t));
|
||||
if (static_cast<int>(resample_buffer_.size()) < available_samples) {
|
||||
resample_buffer_.resize(available_samples);
|
||||
}
|
||||
|
||||
// Get resampled data
|
||||
int bytes_read = SDL_GetAudioStreamData(resampling_stream_,
|
||||
resample_buffer_.data(),
|
||||
available_bytes);
|
||||
if (bytes_read < 0) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to get resampled data: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Queue the resampled data
|
||||
int samples_read = bytes_read / static_cast<int>(sizeof(int16_t));
|
||||
return QueueSamples(resample_buffer_.data(), samples_read);
|
||||
}
|
||||
|
||||
AudioStatus SDL3AudioBackend::GetStatus() const {
|
||||
AudioStatus status;
|
||||
|
||||
if (!initialized_) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check if device is playing
|
||||
status.is_playing = device_id_ && !SDL_IsAudioDevicePaused(device_id_);
|
||||
|
||||
// Get queued audio size from stream
|
||||
if (audio_stream_) {
|
||||
int queued_bytes = SDL_GetAudioStreamQueued(audio_stream_);
|
||||
if (queued_bytes >= 0) {
|
||||
status.queued_bytes = static_cast<uint32_t>(queued_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate queued frames
|
||||
int bytes_per_frame = config_.channels *
|
||||
(config_.format == SampleFormat::INT16 ? 2 : 4);
|
||||
if (bytes_per_frame > 0) {
|
||||
status.queued_frames = status.queued_bytes / bytes_per_frame;
|
||||
}
|
||||
|
||||
// Check for underrun (queue too low while playing)
|
||||
if (status.is_playing && status.queued_frames < 100) {
|
||||
status.has_underrun = true;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
bool SDL3AudioBackend::IsInitialized() const {
|
||||
return initialized_;
|
||||
}
|
||||
|
||||
AudioConfig SDL3AudioBackend::GetConfig() const {
|
||||
return config_;
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::SetVolume(float volume) {
|
||||
volume_ = std::clamp(volume, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float SDL3AudioBackend::GetVolume() const {
|
||||
return volume_;
|
||||
}
|
||||
|
||||
void SDL3AudioBackend::SetAudioStreamResampling(bool enable, int native_rate,
|
||||
int channels) {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enable) {
|
||||
// Disable resampling
|
||||
if (resampling_stream_) {
|
||||
SDL_DestroyAudioStream(resampling_stream_);
|
||||
resampling_stream_ = nullptr;
|
||||
}
|
||||
resampling_enabled_ = false;
|
||||
native_rate_ = 0;
|
||||
native_channels_ = 0;
|
||||
resample_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we need to recreate the resampling stream
|
||||
const bool needs_recreate = (resampling_stream_ == nullptr) ||
|
||||
(native_rate_ != native_rate) ||
|
||||
(native_channels_ != channels);
|
||||
|
||||
if (!needs_recreate) {
|
||||
resampling_enabled_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up existing stream
|
||||
if (resampling_stream_) {
|
||||
SDL_DestroyAudioStream(resampling_stream_);
|
||||
resampling_stream_ = nullptr;
|
||||
}
|
||||
|
||||
// Create new resampling stream
|
||||
// Source spec (native rate)
|
||||
SDL_AudioSpec src_spec;
|
||||
src_spec.format = SDL_AUDIO_S16;
|
||||
src_spec.channels = channels;
|
||||
src_spec.freq = native_rate;
|
||||
|
||||
// Destination spec (device rate)
|
||||
SDL_AudioSpec dst_spec;
|
||||
dst_spec.format = device_format_;
|
||||
dst_spec.channels = device_channels_;
|
||||
dst_spec.freq = device_freq_;
|
||||
|
||||
// Create audio stream for resampling
|
||||
resampling_stream_ = SDL_CreateAudioStream(&src_spec, &dst_spec);
|
||||
if (!resampling_stream_) {
|
||||
LOG_ERROR("AudioBackend", "SDL3: Failed to create resampling stream: %s",
|
||||
SDL_GetError());
|
||||
resampling_enabled_ = false;
|
||||
native_rate_ = 0;
|
||||
native_channels_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing data
|
||||
SDL_ClearAudioStream(resampling_stream_);
|
||||
|
||||
// Update state
|
||||
resampling_enabled_ = true;
|
||||
native_rate_ = native_rate;
|
||||
native_channels_ = channels;
|
||||
resample_buffer_.clear();
|
||||
|
||||
LOG_INFO("AudioBackend",
|
||||
"SDL3: Resampling enabled: %dHz %dch -> %dHz %dch",
|
||||
native_rate, channels, device_freq_, device_channels_);
|
||||
}
|
||||
|
||||
// Helper functions for volume application
|
||||
bool SDL3AudioBackend::ApplyVolume(int16_t* samples, int num_samples) const {
|
||||
if (!samples) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float vol = volume_.load();
|
||||
if (vol == 1.0f) {
|
||||
return true; // No change needed
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_samples; ++i) {
|
||||
int32_t scaled = static_cast<int32_t>(samples[i] * vol);
|
||||
samples[i] = static_cast<int16_t>(std::clamp(scaled, -32768, 32767));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDL3AudioBackend::ApplyVolume(float* samples, int num_samples) const {
|
||||
if (!samples) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float vol = volume_.load();
|
||||
if (vol == 1.0f) {
|
||||
return true; // No change needed
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_samples; ++i) {
|
||||
samples[i] = std::clamp(samples[i] * vol, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
110
src/app/emu/audio/sdl3_audio_backend.h
Normal file
110
src/app/emu/audio/sdl3_audio_backend.h
Normal file
@@ -0,0 +1,110 @@
|
||||
// sdl3_audio_backend.h - SDL3 Audio Backend Implementation
|
||||
// Stream-based audio implementation for SDL3
|
||||
|
||||
#ifndef YAZE_APP_EMU_AUDIO_SDL3_AUDIO_BACKEND_H
|
||||
#define YAZE_APP_EMU_AUDIO_SDL3_AUDIO_BACKEND_H
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_audio.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app/emu/audio/audio_backend.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace audio {
|
||||
|
||||
/**
|
||||
* @brief SDL3 audio backend implementation using SDL_AudioStream API
|
||||
*
|
||||
* SDL3 introduces a stream-based audio API replacing the queue-based approach.
|
||||
* This implementation provides compatibility with the IAudioBackend interface
|
||||
* while leveraging SDL3's improved audio pipeline.
|
||||
*/
|
||||
class SDL3AudioBackend : public IAudioBackend {
|
||||
public:
|
||||
SDL3AudioBackend() = default;
|
||||
~SDL3AudioBackend() override;
|
||||
|
||||
// Initialization
|
||||
bool Initialize(const AudioConfig& config) override;
|
||||
void Shutdown() override;
|
||||
|
||||
// Playback control
|
||||
void Play() override;
|
||||
void Pause() override;
|
||||
void Stop() override;
|
||||
void Clear() override;
|
||||
|
||||
// Audio data
|
||||
bool QueueSamples(const int16_t* samples, int num_samples) override;
|
||||
bool QueueSamples(const float* samples, int num_samples) override;
|
||||
bool QueueSamplesNative(const int16_t* samples, int frames_per_channel,
|
||||
int channels, int native_rate) override;
|
||||
|
||||
// Status queries
|
||||
AudioStatus GetStatus() const override;
|
||||
bool IsInitialized() const override;
|
||||
AudioConfig GetConfig() const override;
|
||||
|
||||
// Volume control (0.0 to 1.0)
|
||||
void SetVolume(float volume) override;
|
||||
float GetVolume() const override;
|
||||
|
||||
// SDL3 supports audio stream resampling natively
|
||||
void SetAudioStreamResampling(bool enable, int native_rate,
|
||||
int channels) override;
|
||||
bool SupportsAudioStream() const override { return true; }
|
||||
|
||||
// Backend identification
|
||||
std::string GetBackendName() const override { return "SDL3"; }
|
||||
|
||||
private:
|
||||
// Helper functions
|
||||
bool ApplyVolume(int16_t* samples, int num_samples) const;
|
||||
bool ApplyVolume(float* samples, int num_samples) const;
|
||||
|
||||
// SDL3 audio stream - primary interface for audio output
|
||||
SDL_AudioStream* audio_stream_ = nullptr;
|
||||
|
||||
// Resampling stream for native rate support
|
||||
SDL_AudioStream* resampling_stream_ = nullptr;
|
||||
|
||||
// Audio device ID
|
||||
SDL_AudioDeviceID device_id_ = 0;
|
||||
|
||||
// Configuration
|
||||
AudioConfig config_;
|
||||
|
||||
// State
|
||||
std::atomic<bool> initialized_{false};
|
||||
std::atomic<float> volume_{1.0f};
|
||||
|
||||
// Resampling configuration
|
||||
bool resampling_enabled_ = false;
|
||||
int native_rate_ = 0;
|
||||
int native_channels_ = 0;
|
||||
|
||||
// Buffer for resampling operations
|
||||
std::vector<int16_t> resample_buffer_;
|
||||
|
||||
// Format information
|
||||
SDL_AudioFormat device_format_ = SDL_AUDIO_S16;
|
||||
int device_channels_ = 2;
|
||||
int device_freq_ = 48000;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
|
||||
#endif // YAZE_APP_EMU_AUDIO_SDL3_AUDIO_BACKEND_H
|
||||
668
src/app/emu/debug/disassembler.cc
Normal file
668
src/app/emu/debug/disassembler.cc
Normal file
@@ -0,0 +1,668 @@
|
||||
#include "app/emu/debug/disassembler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
Disassembler65816::Disassembler65816() { InitializeOpcodeTable(); }
|
||||
|
||||
void Disassembler65816::InitializeOpcodeTable() {
|
||||
// Initialize all opcodes with their mnemonics and addressing modes
|
||||
// Format: opcode_table_[opcode] = {mnemonic, addressing_mode, base_size}
|
||||
|
||||
using AM = AddressingMode65816;
|
||||
|
||||
// Row 0x00-0x0F
|
||||
opcode_table_[0x00] = {"BRK", AM::kImmediate8, 2};
|
||||
opcode_table_[0x01] = {"ORA", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0x02] = {"COP", AM::kImmediate8, 2};
|
||||
opcode_table_[0x03] = {"ORA", AM::kStackRelative, 2};
|
||||
opcode_table_[0x04] = {"TSB", AM::kDirectPage, 2};
|
||||
opcode_table_[0x05] = {"ORA", AM::kDirectPage, 2};
|
||||
opcode_table_[0x06] = {"ASL", AM::kDirectPage, 2};
|
||||
opcode_table_[0x07] = {"ORA", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0x08] = {"PHP", AM::kImplied, 1};
|
||||
opcode_table_[0x09] = {"ORA", AM::kImmediateM, 2}; // Size depends on M flag
|
||||
opcode_table_[0x0A] = {"ASL", AM::kAccumulator, 1};
|
||||
opcode_table_[0x0B] = {"PHD", AM::kImplied, 1};
|
||||
opcode_table_[0x0C] = {"TSB", AM::kAbsolute, 3};
|
||||
opcode_table_[0x0D] = {"ORA", AM::kAbsolute, 3};
|
||||
opcode_table_[0x0E] = {"ASL", AM::kAbsolute, 3};
|
||||
opcode_table_[0x0F] = {"ORA", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0x10-0x1F
|
||||
opcode_table_[0x10] = {"BPL", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0x11] = {"ORA", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0x12] = {"ORA", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0x13] = {"ORA", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0x14] = {"TRB", AM::kDirectPage, 2};
|
||||
opcode_table_[0x15] = {"ORA", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x16] = {"ASL", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x17] = {"ORA", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0x18] = {"CLC", AM::kImplied, 1};
|
||||
opcode_table_[0x19] = {"ORA", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0x1A] = {"INC", AM::kAccumulator, 1};
|
||||
opcode_table_[0x1B] = {"TCS", AM::kImplied, 1};
|
||||
opcode_table_[0x1C] = {"TRB", AM::kAbsolute, 3};
|
||||
opcode_table_[0x1D] = {"ORA", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x1E] = {"ASL", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x1F] = {"ORA", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0x20-0x2F
|
||||
opcode_table_[0x20] = {"JSR", AM::kAbsolute, 3};
|
||||
opcode_table_[0x21] = {"AND", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0x22] = {"JSL", AM::kAbsoluteLong, 4};
|
||||
opcode_table_[0x23] = {"AND", AM::kStackRelative, 2};
|
||||
opcode_table_[0x24] = {"BIT", AM::kDirectPage, 2};
|
||||
opcode_table_[0x25] = {"AND", AM::kDirectPage, 2};
|
||||
opcode_table_[0x26] = {"ROL", AM::kDirectPage, 2};
|
||||
opcode_table_[0x27] = {"AND", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0x28] = {"PLP", AM::kImplied, 1};
|
||||
opcode_table_[0x29] = {"AND", AM::kImmediateM, 2};
|
||||
opcode_table_[0x2A] = {"ROL", AM::kAccumulator, 1};
|
||||
opcode_table_[0x2B] = {"PLD", AM::kImplied, 1};
|
||||
opcode_table_[0x2C] = {"BIT", AM::kAbsolute, 3};
|
||||
opcode_table_[0x2D] = {"AND", AM::kAbsolute, 3};
|
||||
opcode_table_[0x2E] = {"ROL", AM::kAbsolute, 3};
|
||||
opcode_table_[0x2F] = {"AND", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0x30-0x3F
|
||||
opcode_table_[0x30] = {"BMI", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0x31] = {"AND", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0x32] = {"AND", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0x33] = {"AND", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0x34] = {"BIT", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x35] = {"AND", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x36] = {"ROL", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x37] = {"AND", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0x38] = {"SEC", AM::kImplied, 1};
|
||||
opcode_table_[0x39] = {"AND", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0x3A] = {"DEC", AM::kAccumulator, 1};
|
||||
opcode_table_[0x3B] = {"TSC", AM::kImplied, 1};
|
||||
opcode_table_[0x3C] = {"BIT", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x3D] = {"AND", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x3E] = {"ROL", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x3F] = {"AND", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0x40-0x4F
|
||||
opcode_table_[0x40] = {"RTI", AM::kImplied, 1};
|
||||
opcode_table_[0x41] = {"EOR", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0x42] = {"WDM", AM::kImmediate8, 2};
|
||||
opcode_table_[0x43] = {"EOR", AM::kStackRelative, 2};
|
||||
opcode_table_[0x44] = {"MVP", AM::kBlockMove, 3};
|
||||
opcode_table_[0x45] = {"EOR", AM::kDirectPage, 2};
|
||||
opcode_table_[0x46] = {"LSR", AM::kDirectPage, 2};
|
||||
opcode_table_[0x47] = {"EOR", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0x48] = {"PHA", AM::kImplied, 1};
|
||||
opcode_table_[0x49] = {"EOR", AM::kImmediateM, 2};
|
||||
opcode_table_[0x4A] = {"LSR", AM::kAccumulator, 1};
|
||||
opcode_table_[0x4B] = {"PHK", AM::kImplied, 1};
|
||||
opcode_table_[0x4C] = {"JMP", AM::kAbsolute, 3};
|
||||
opcode_table_[0x4D] = {"EOR", AM::kAbsolute, 3};
|
||||
opcode_table_[0x4E] = {"LSR", AM::kAbsolute, 3};
|
||||
opcode_table_[0x4F] = {"EOR", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0x50-0x5F
|
||||
opcode_table_[0x50] = {"BVC", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0x51] = {"EOR", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0x52] = {"EOR", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0x53] = {"EOR", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0x54] = {"MVN", AM::kBlockMove, 3};
|
||||
opcode_table_[0x55] = {"EOR", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x56] = {"LSR", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x57] = {"EOR", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0x58] = {"CLI", AM::kImplied, 1};
|
||||
opcode_table_[0x59] = {"EOR", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0x5A] = {"PHY", AM::kImplied, 1};
|
||||
opcode_table_[0x5B] = {"TCD", AM::kImplied, 1};
|
||||
opcode_table_[0x5C] = {"JMP", AM::kAbsoluteLong, 4};
|
||||
opcode_table_[0x5D] = {"EOR", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x5E] = {"LSR", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x5F] = {"EOR", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0x60-0x6F
|
||||
opcode_table_[0x60] = {"RTS", AM::kImplied, 1};
|
||||
opcode_table_[0x61] = {"ADC", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0x62] = {"PER", AM::kProgramCounterRelativeLong, 3};
|
||||
opcode_table_[0x63] = {"ADC", AM::kStackRelative, 2};
|
||||
opcode_table_[0x64] = {"STZ", AM::kDirectPage, 2};
|
||||
opcode_table_[0x65] = {"ADC", AM::kDirectPage, 2};
|
||||
opcode_table_[0x66] = {"ROR", AM::kDirectPage, 2};
|
||||
opcode_table_[0x67] = {"ADC", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0x68] = {"PLA", AM::kImplied, 1};
|
||||
opcode_table_[0x69] = {"ADC", AM::kImmediateM, 2};
|
||||
opcode_table_[0x6A] = {"ROR", AM::kAccumulator, 1};
|
||||
opcode_table_[0x6B] = {"RTL", AM::kImplied, 1};
|
||||
opcode_table_[0x6C] = {"JMP", AM::kAbsoluteIndirect, 3};
|
||||
opcode_table_[0x6D] = {"ADC", AM::kAbsolute, 3};
|
||||
opcode_table_[0x6E] = {"ROR", AM::kAbsolute, 3};
|
||||
opcode_table_[0x6F] = {"ADC", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0x70-0x7F
|
||||
opcode_table_[0x70] = {"BVS", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0x71] = {"ADC", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0x72] = {"ADC", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0x73] = {"ADC", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0x74] = {"STZ", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x75] = {"ADC", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x76] = {"ROR", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x77] = {"ADC", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0x78] = {"SEI", AM::kImplied, 1};
|
||||
opcode_table_[0x79] = {"ADC", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0x7A] = {"PLY", AM::kImplied, 1};
|
||||
opcode_table_[0x7B] = {"TDC", AM::kImplied, 1};
|
||||
opcode_table_[0x7C] = {"JMP", AM::kAbsoluteIndexedIndirect, 3};
|
||||
opcode_table_[0x7D] = {"ADC", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x7E] = {"ROR", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x7F] = {"ADC", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0x80-0x8F
|
||||
opcode_table_[0x80] = {"BRA", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0x81] = {"STA", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0x82] = {"BRL", AM::kProgramCounterRelativeLong, 3};
|
||||
opcode_table_[0x83] = {"STA", AM::kStackRelative, 2};
|
||||
opcode_table_[0x84] = {"STY", AM::kDirectPage, 2};
|
||||
opcode_table_[0x85] = {"STA", AM::kDirectPage, 2};
|
||||
opcode_table_[0x86] = {"STX", AM::kDirectPage, 2};
|
||||
opcode_table_[0x87] = {"STA", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0x88] = {"DEY", AM::kImplied, 1};
|
||||
opcode_table_[0x89] = {"BIT", AM::kImmediateM, 2};
|
||||
opcode_table_[0x8A] = {"TXA", AM::kImplied, 1};
|
||||
opcode_table_[0x8B] = {"PHB", AM::kImplied, 1};
|
||||
opcode_table_[0x8C] = {"STY", AM::kAbsolute, 3};
|
||||
opcode_table_[0x8D] = {"STA", AM::kAbsolute, 3};
|
||||
opcode_table_[0x8E] = {"STX", AM::kAbsolute, 3};
|
||||
opcode_table_[0x8F] = {"STA", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0x90-0x9F
|
||||
opcode_table_[0x90] = {"BCC", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0x91] = {"STA", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0x92] = {"STA", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0x93] = {"STA", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0x94] = {"STY", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x95] = {"STA", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0x96] = {"STX", AM::kDirectPageIndexedY, 2};
|
||||
opcode_table_[0x97] = {"STA", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0x98] = {"TYA", AM::kImplied, 1};
|
||||
opcode_table_[0x99] = {"STA", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0x9A] = {"TXS", AM::kImplied, 1};
|
||||
opcode_table_[0x9B] = {"TXY", AM::kImplied, 1};
|
||||
opcode_table_[0x9C] = {"STZ", AM::kAbsolute, 3};
|
||||
opcode_table_[0x9D] = {"STA", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x9E] = {"STZ", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0x9F] = {"STA", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0xA0-0xAF
|
||||
opcode_table_[0xA0] = {"LDY", AM::kImmediateX, 2};
|
||||
opcode_table_[0xA1] = {"LDA", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0xA2] = {"LDX", AM::kImmediateX, 2};
|
||||
opcode_table_[0xA3] = {"LDA", AM::kStackRelative, 2};
|
||||
opcode_table_[0xA4] = {"LDY", AM::kDirectPage, 2};
|
||||
opcode_table_[0xA5] = {"LDA", AM::kDirectPage, 2};
|
||||
opcode_table_[0xA6] = {"LDX", AM::kDirectPage, 2};
|
||||
opcode_table_[0xA7] = {"LDA", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0xA8] = {"TAY", AM::kImplied, 1};
|
||||
opcode_table_[0xA9] = {"LDA", AM::kImmediateM, 2};
|
||||
opcode_table_[0xAA] = {"TAX", AM::kImplied, 1};
|
||||
opcode_table_[0xAB] = {"PLB", AM::kImplied, 1};
|
||||
opcode_table_[0xAC] = {"LDY", AM::kAbsolute, 3};
|
||||
opcode_table_[0xAD] = {"LDA", AM::kAbsolute, 3};
|
||||
opcode_table_[0xAE] = {"LDX", AM::kAbsolute, 3};
|
||||
opcode_table_[0xAF] = {"LDA", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0xB0-0xBF
|
||||
opcode_table_[0xB0] = {"BCS", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0xB1] = {"LDA", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0xB2] = {"LDA", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0xB3] = {"LDA", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0xB4] = {"LDY", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0xB5] = {"LDA", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0xB6] = {"LDX", AM::kDirectPageIndexedY, 2};
|
||||
opcode_table_[0xB7] = {"LDA", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0xB8] = {"CLV", AM::kImplied, 1};
|
||||
opcode_table_[0xB9] = {"LDA", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0xBA] = {"TSX", AM::kImplied, 1};
|
||||
opcode_table_[0xBB] = {"TYX", AM::kImplied, 1};
|
||||
opcode_table_[0xBC] = {"LDY", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0xBD] = {"LDA", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0xBE] = {"LDX", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0xBF] = {"LDA", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0xC0-0xCF
|
||||
opcode_table_[0xC0] = {"CPY", AM::kImmediateX, 2};
|
||||
opcode_table_[0xC1] = {"CMP", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0xC2] = {"REP", AM::kImmediate8, 2};
|
||||
opcode_table_[0xC3] = {"CMP", AM::kStackRelative, 2};
|
||||
opcode_table_[0xC4] = {"CPY", AM::kDirectPage, 2};
|
||||
opcode_table_[0xC5] = {"CMP", AM::kDirectPage, 2};
|
||||
opcode_table_[0xC6] = {"DEC", AM::kDirectPage, 2};
|
||||
opcode_table_[0xC7] = {"CMP", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0xC8] = {"INY", AM::kImplied, 1};
|
||||
opcode_table_[0xC9] = {"CMP", AM::kImmediateM, 2};
|
||||
opcode_table_[0xCA] = {"DEX", AM::kImplied, 1};
|
||||
opcode_table_[0xCB] = {"WAI", AM::kImplied, 1};
|
||||
opcode_table_[0xCC] = {"CPY", AM::kAbsolute, 3};
|
||||
opcode_table_[0xCD] = {"CMP", AM::kAbsolute, 3};
|
||||
opcode_table_[0xCE] = {"DEC", AM::kAbsolute, 3};
|
||||
opcode_table_[0xCF] = {"CMP", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0xD0-0xDF
|
||||
opcode_table_[0xD0] = {"BNE", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0xD1] = {"CMP", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0xD2] = {"CMP", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0xD3] = {"CMP", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0xD4] = {"PEI", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0xD5] = {"CMP", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0xD6] = {"DEC", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0xD7] = {"CMP", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0xD8] = {"CLD", AM::kImplied, 1};
|
||||
opcode_table_[0xD9] = {"CMP", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0xDA] = {"PHX", AM::kImplied, 1};
|
||||
opcode_table_[0xDB] = {"STP", AM::kImplied, 1};
|
||||
opcode_table_[0xDC] = {"JMP", AM::kAbsoluteIndirectLong, 3};
|
||||
opcode_table_[0xDD] = {"CMP", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0xDE] = {"DEC", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0xDF] = {"CMP", AM::kAbsoluteLongIndexedX, 4};
|
||||
|
||||
// Row 0xE0-0xEF
|
||||
opcode_table_[0xE0] = {"CPX", AM::kImmediateX, 2};
|
||||
opcode_table_[0xE1] = {"SBC", AM::kDirectPageIndexedIndirectX, 2};
|
||||
opcode_table_[0xE2] = {"SEP", AM::kImmediate8, 2};
|
||||
opcode_table_[0xE3] = {"SBC", AM::kStackRelative, 2};
|
||||
opcode_table_[0xE4] = {"CPX", AM::kDirectPage, 2};
|
||||
opcode_table_[0xE5] = {"SBC", AM::kDirectPage, 2};
|
||||
opcode_table_[0xE6] = {"INC", AM::kDirectPage, 2};
|
||||
opcode_table_[0xE7] = {"SBC", AM::kDirectPageIndirectLong, 2};
|
||||
opcode_table_[0xE8] = {"INX", AM::kImplied, 1};
|
||||
opcode_table_[0xE9] = {"SBC", AM::kImmediateM, 2};
|
||||
opcode_table_[0xEA] = {"NOP", AM::kImplied, 1};
|
||||
opcode_table_[0xEB] = {"XBA", AM::kImplied, 1};
|
||||
opcode_table_[0xEC] = {"CPX", AM::kAbsolute, 3};
|
||||
opcode_table_[0xED] = {"SBC", AM::kAbsolute, 3};
|
||||
opcode_table_[0xEE] = {"INC", AM::kAbsolute, 3};
|
||||
opcode_table_[0xEF] = {"SBC", AM::kAbsoluteLong, 4};
|
||||
|
||||
// Row 0xF0-0xFF
|
||||
opcode_table_[0xF0] = {"BEQ", AM::kProgramCounterRelative, 2};
|
||||
opcode_table_[0xF1] = {"SBC", AM::kDirectPageIndirectIndexedY, 2};
|
||||
opcode_table_[0xF2] = {"SBC", AM::kDirectPageIndirect, 2};
|
||||
opcode_table_[0xF3] = {"SBC", AM::kStackRelativeIndirectIndexedY, 2};
|
||||
opcode_table_[0xF4] = {"PEA", AM::kAbsolute, 3};
|
||||
opcode_table_[0xF5] = {"SBC", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0xF6] = {"INC", AM::kDirectPageIndexedX, 2};
|
||||
opcode_table_[0xF7] = {"SBC", AM::kDirectPageIndirectLongIndexedY, 2};
|
||||
opcode_table_[0xF8] = {"SED", AM::kImplied, 1};
|
||||
opcode_table_[0xF9] = {"SBC", AM::kAbsoluteIndexedY, 3};
|
||||
opcode_table_[0xFA] = {"PLX", AM::kImplied, 1};
|
||||
opcode_table_[0xFB] = {"XCE", AM::kImplied, 1};
|
||||
opcode_table_[0xFC] = {"JSR", AM::kAbsoluteIndexedIndirect, 3};
|
||||
opcode_table_[0xFD] = {"SBC", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0xFE] = {"INC", AM::kAbsoluteIndexedX, 3};
|
||||
opcode_table_[0xFF] = {"SBC", AM::kAbsoluteLongIndexedX, 4};
|
||||
}
|
||||
|
||||
const InstructionInfo& Disassembler65816::GetInstructionInfo(
|
||||
uint8_t opcode) const {
|
||||
return opcode_table_[opcode];
|
||||
}
|
||||
|
||||
uint8_t Disassembler65816::GetInstructionSize(uint8_t opcode, bool m_flag,
|
||||
bool x_flag) const {
|
||||
const auto& info = opcode_table_[opcode];
|
||||
uint8_t size = info.base_size;
|
||||
|
||||
// Adjust size for M-flag dependent immediate modes
|
||||
if (info.mode == AddressingMode65816::kImmediateM && !m_flag) {
|
||||
size++; // 16-bit accumulator mode adds 1 byte
|
||||
}
|
||||
// Adjust size for X-flag dependent immediate modes
|
||||
if (info.mode == AddressingMode65816::kImmediateX && !x_flag) {
|
||||
size++; // 16-bit index mode adds 1 byte
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
DisassembledInstruction Disassembler65816::Disassemble(
|
||||
uint32_t address, MemoryReader read_byte, bool m_flag, bool x_flag) const {
|
||||
DisassembledInstruction result;
|
||||
result.address = address;
|
||||
|
||||
// Read opcode
|
||||
result.opcode = read_byte(address);
|
||||
const auto& info = opcode_table_[result.opcode];
|
||||
result.mnemonic = info.mnemonic;
|
||||
result.size = GetInstructionSize(result.opcode, m_flag, x_flag);
|
||||
|
||||
// Read operand bytes
|
||||
for (uint8_t i = 1; i < result.size; i++) {
|
||||
result.operands.push_back(read_byte(address + i));
|
||||
}
|
||||
|
||||
// Format operand string
|
||||
result.operand_str =
|
||||
FormatOperand(info.mode, result.operands, address, m_flag, x_flag);
|
||||
|
||||
// Determine instruction type
|
||||
const std::string& mn = result.mnemonic;
|
||||
result.is_branch = (mn == "BRA" || mn == "BRL" || mn == "BPL" ||
|
||||
mn == "BMI" || mn == "BVC" || mn == "BVS" ||
|
||||
mn == "BCC" || mn == "BCS" || mn == "BNE" ||
|
||||
mn == "BEQ" || mn == "JMP");
|
||||
result.is_call = (mn == "JSR" || mn == "JSL");
|
||||
result.is_return = (mn == "RTS" || mn == "RTL" || mn == "RTI");
|
||||
|
||||
// Calculate branch target if applicable
|
||||
if (result.is_branch || result.is_call) {
|
||||
result.branch_target =
|
||||
CalculateBranchTarget(address, result.operands, info.mode, result.size);
|
||||
}
|
||||
|
||||
// Build full text representation
|
||||
std::ostringstream ss;
|
||||
ss << absl::StrFormat("$%06X: ", address);
|
||||
|
||||
// Hex dump of bytes
|
||||
ss << absl::StrFormat("%02X ", result.opcode);
|
||||
for (const auto& byte : result.operands) {
|
||||
ss << absl::StrFormat("%02X ", byte);
|
||||
}
|
||||
// Pad to align mnemonics
|
||||
for (int i = result.size; i < 4; i++) {
|
||||
ss << " ";
|
||||
}
|
||||
|
||||
ss << " " << result.mnemonic;
|
||||
if (!result.operand_str.empty()) {
|
||||
ss << " " << result.operand_str;
|
||||
}
|
||||
|
||||
// Add branch target comment if applicable
|
||||
if ((result.is_branch || result.is_call) &&
|
||||
info.mode != AddressingMode65816::kAbsoluteIndirect &&
|
||||
info.mode != AddressingMode65816::kAbsoluteIndirectLong &&
|
||||
info.mode != AddressingMode65816::kAbsoluteIndexedIndirect) {
|
||||
// Try to resolve symbol
|
||||
if (symbol_resolver_) {
|
||||
std::string symbol = symbol_resolver_(result.branch_target);
|
||||
if (!symbol.empty()) {
|
||||
ss << " ; -> " << symbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.full_text = ss.str();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<DisassembledInstruction> Disassembler65816::DisassembleRange(
|
||||
uint32_t start_address, size_t count, MemoryReader read_byte, bool m_flag,
|
||||
bool x_flag) const {
|
||||
std::vector<DisassembledInstruction> results;
|
||||
results.reserve(count);
|
||||
|
||||
uint32_t current_address = start_address;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
auto instruction = Disassemble(current_address, read_byte, m_flag, x_flag);
|
||||
results.push_back(instruction);
|
||||
current_address += instruction.size;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
std::string Disassembler65816::FormatOperand(AddressingMode65816 mode,
|
||||
const std::vector<uint8_t>& ops,
|
||||
uint32_t address, bool m_flag,
|
||||
bool x_flag) const {
|
||||
using AM = AddressingMode65816;
|
||||
|
||||
switch (mode) {
|
||||
case AM::kImplied:
|
||||
case AM::kAccumulator:
|
||||
return "";
|
||||
|
||||
case AM::kImmediate8:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("#$%02X", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kImmediate16:
|
||||
if (ops.size() >= 2) {
|
||||
return absl::StrFormat("#$%04X", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kImmediateM:
|
||||
if (m_flag && ops.size() >= 1) {
|
||||
return absl::StrFormat("#$%02X", ops[0]);
|
||||
} else if (!m_flag && ops.size() >= 2) {
|
||||
return absl::StrFormat("#$%04X", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kImmediateX:
|
||||
if (x_flag && ops.size() >= 1) {
|
||||
return absl::StrFormat("#$%02X", ops[0]);
|
||||
} else if (!x_flag && ops.size() >= 2) {
|
||||
return absl::StrFormat("#$%04X", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPage:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("$%02X", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndexedX:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("$%02X,X", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndexedY:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("$%02X,Y", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndirect:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("($%02X)", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndirectLong:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("[$%02X]", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndexedIndirectX:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("($%02X,X)", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndirectIndexedY:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("($%02X),Y", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kDirectPageIndirectLongIndexedY:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("[$%02X],Y", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsolute:
|
||||
if (ops.size() >= 2) {
|
||||
uint16_t addr = ops[0] | (ops[1] << 8);
|
||||
// Try symbol resolution
|
||||
if (symbol_resolver_) {
|
||||
std::string symbol = symbol_resolver_(addr);
|
||||
if (!symbol.empty()) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
return absl::StrFormat("$%04X", addr);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteIndexedX:
|
||||
if (ops.size() >= 2) {
|
||||
return absl::StrFormat("$%04X,X", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteIndexedY:
|
||||
if (ops.size() >= 2) {
|
||||
return absl::StrFormat("$%04X,Y", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteLong:
|
||||
if (ops.size() >= 3) {
|
||||
uint32_t addr = ops[0] | (ops[1] << 8) | (ops[2] << 16);
|
||||
if (symbol_resolver_) {
|
||||
std::string symbol = symbol_resolver_(addr);
|
||||
if (!symbol.empty()) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
return absl::StrFormat("$%06X", addr);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteLongIndexedX:
|
||||
if (ops.size() >= 3) {
|
||||
return absl::StrFormat("$%06X,X",
|
||||
ops[0] | (ops[1] << 8) | (ops[2] << 16));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteIndirect:
|
||||
if (ops.size() >= 2) {
|
||||
return absl::StrFormat("($%04X)", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteIndirectLong:
|
||||
if (ops.size() >= 2) {
|
||||
return absl::StrFormat("[$%04X]", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteIndexedIndirect:
|
||||
if (ops.size() >= 2) {
|
||||
return absl::StrFormat("($%04X,X)", ops[0] | (ops[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kProgramCounterRelative:
|
||||
if (ops.size() >= 1) {
|
||||
// 8-bit signed offset
|
||||
int8_t offset = static_cast<int8_t>(ops[0]);
|
||||
uint32_t target = (address + 2 + offset) & 0xFFFF;
|
||||
// Preserve bank
|
||||
target |= (address & 0xFF0000);
|
||||
if (symbol_resolver_) {
|
||||
std::string symbol = symbol_resolver_(target);
|
||||
if (!symbol.empty()) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
return absl::StrFormat("$%04X", target & 0xFFFF);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kProgramCounterRelativeLong:
|
||||
if (ops.size() >= 2) {
|
||||
// 16-bit signed offset
|
||||
int16_t offset = static_cast<int16_t>(ops[0] | (ops[1] << 8));
|
||||
uint32_t target = (address + 3 + offset) & 0xFFFF;
|
||||
target |= (address & 0xFF0000);
|
||||
if (symbol_resolver_) {
|
||||
std::string symbol = symbol_resolver_(target);
|
||||
if (!symbol.empty()) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
return absl::StrFormat("$%04X", target & 0xFFFF);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kStackRelative:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("$%02X,S", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kStackRelativeIndirectIndexedY:
|
||||
if (ops.size() >= 1) {
|
||||
return absl::StrFormat("($%02X,S),Y", ops[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kBlockMove:
|
||||
if (ops.size() >= 2) {
|
||||
// MVN/MVP: srcBank, dstBank
|
||||
return absl::StrFormat("$%02X,$%02X", ops[0], ops[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return "???";
|
||||
}
|
||||
|
||||
uint32_t Disassembler65816::CalculateBranchTarget(
|
||||
uint32_t address, const std::vector<uint8_t>& operands,
|
||||
AddressingMode65816 mode, uint8_t instruction_size) const {
|
||||
using AM = AddressingMode65816;
|
||||
|
||||
switch (mode) {
|
||||
case AM::kProgramCounterRelative:
|
||||
if (operands.size() >= 1) {
|
||||
int8_t offset = static_cast<int8_t>(operands[0]);
|
||||
return (address + instruction_size + offset) & 0xFFFFFF;
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kProgramCounterRelativeLong:
|
||||
if (operands.size() >= 2) {
|
||||
int16_t offset =
|
||||
static_cast<int16_t>(operands[0] | (operands[1] << 8));
|
||||
return (address + instruction_size + offset) & 0xFFFFFF;
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsolute:
|
||||
if (operands.size() >= 2) {
|
||||
// For JMP/JSR absolute, use current bank
|
||||
return (address & 0xFF0000) | (operands[0] | (operands[1] << 8));
|
||||
}
|
||||
break;
|
||||
|
||||
case AM::kAbsoluteLong:
|
||||
if (operands.size() >= 3) {
|
||||
return operands[0] | (operands[1] << 8) | (operands[2] << 16);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
182
src/app/emu/debug/disassembler.h
Normal file
182
src/app/emu/debug/disassembler.h
Normal file
@@ -0,0 +1,182 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_DISASSEMBLER_H_
|
||||
#define YAZE_APP_EMU_DEBUG_DISASSEMBLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
/**
|
||||
* @brief Addressing modes for the 65816 CPU
|
||||
*/
|
||||
enum class AddressingMode65816 {
|
||||
kImplied, // No operand
|
||||
kAccumulator, // A
|
||||
kImmediate8, // #$xx (8-bit)
|
||||
kImmediate16, // #$xxxx (16-bit, depends on M/X flags)
|
||||
kImmediateM, // #$xx or #$xxxx (depends on M flag)
|
||||
kImmediateX, // #$xx or #$xxxx (depends on X flag)
|
||||
kDirectPage, // $xx
|
||||
kDirectPageIndexedX, // $xx,X
|
||||
kDirectPageIndexedY, // $xx,Y
|
||||
kDirectPageIndirect, // ($xx)
|
||||
kDirectPageIndirectLong, // [$xx]
|
||||
kDirectPageIndexedIndirectX, // ($xx,X)
|
||||
kDirectPageIndirectIndexedY, // ($xx),Y
|
||||
kDirectPageIndirectLongIndexedY, // [$xx],Y
|
||||
kAbsolute, // $xxxx
|
||||
kAbsoluteIndexedX, // $xxxx,X
|
||||
kAbsoluteIndexedY, // $xxxx,Y
|
||||
kAbsoluteLong, // $xxxxxx
|
||||
kAbsoluteLongIndexedX, // $xxxxxx,X
|
||||
kAbsoluteIndirect, // ($xxxx)
|
||||
kAbsoluteIndirectLong, // [$xxxx]
|
||||
kAbsoluteIndexedIndirect, // ($xxxx,X)
|
||||
kProgramCounterRelative, // 8-bit relative branch
|
||||
kProgramCounterRelativeLong, // 16-bit relative branch
|
||||
kStackRelative, // $xx,S
|
||||
kStackRelativeIndirectIndexedY, // ($xx,S),Y
|
||||
kBlockMove, // src,dst (MVN/MVP)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Information about a single 65816 instruction
|
||||
*/
|
||||
struct InstructionInfo {
|
||||
std::string mnemonic; // e.g., "LDA", "STA", "JSR"
|
||||
AddressingMode65816 mode; // Addressing mode
|
||||
uint8_t base_size; // Base size in bytes (1 for opcode alone)
|
||||
|
||||
InstructionInfo() : mnemonic("???"), mode(AddressingMode65816::kImplied), base_size(1) {}
|
||||
InstructionInfo(const std::string& m, AddressingMode65816 am, uint8_t size)
|
||||
: mnemonic(m), mode(am), base_size(size) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Result of disassembling a single instruction
|
||||
*/
|
||||
struct DisassembledInstruction {
|
||||
uint32_t address; // Full 24-bit address
|
||||
uint8_t opcode; // The opcode byte
|
||||
std::vector<uint8_t> operands; // Operand bytes
|
||||
std::string mnemonic; // Instruction mnemonic
|
||||
std::string operand_str; // Formatted operand (e.g., "#$FF", "$1234,X")
|
||||
std::string full_text; // Complete disassembly line
|
||||
uint8_t size; // Total instruction size
|
||||
bool is_branch; // Is this a branch instruction?
|
||||
bool is_call; // Is this JSR/JSL?
|
||||
bool is_return; // Is this RTS/RTL/RTI?
|
||||
uint32_t branch_target; // Target address for branches/jumps
|
||||
|
||||
DisassembledInstruction()
|
||||
: address(0), opcode(0), size(1), is_branch(false),
|
||||
is_call(false), is_return(false), branch_target(0) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 65816 CPU disassembler for debugging and ROM hacking
|
||||
*
|
||||
* This disassembler converts raw ROM/memory bytes into human-readable
|
||||
* assembly instructions. It handles:
|
||||
* - All 256 opcodes
|
||||
* - All addressing modes including 65816-specific ones
|
||||
* - Variable-size immediate operands based on M/X flags
|
||||
* - Branch target calculation
|
||||
* - Symbol resolution (optional)
|
||||
*
|
||||
* Usage:
|
||||
* Disassembler65816 dis;
|
||||
* auto result = dis.Disassemble(address, [](uint32_t addr) {
|
||||
* return memory.ReadByte(addr);
|
||||
* });
|
||||
* std::cout << result.full_text << std::endl;
|
||||
*/
|
||||
class Disassembler65816 {
|
||||
public:
|
||||
using MemoryReader = std::function<uint8_t(uint32_t)>;
|
||||
using SymbolResolver = std::function<std::string(uint32_t)>;
|
||||
|
||||
Disassembler65816();
|
||||
|
||||
/**
|
||||
* @brief Disassemble a single instruction
|
||||
* @param address Starting address (24-bit)
|
||||
* @param read_byte Function to read bytes from memory
|
||||
* @param m_flag Accumulator/memory size flag (true = 8-bit)
|
||||
* @param x_flag Index register size flag (true = 8-bit)
|
||||
* @return Disassembled instruction
|
||||
*/
|
||||
DisassembledInstruction Disassemble(uint32_t address,
|
||||
MemoryReader read_byte,
|
||||
bool m_flag = true,
|
||||
bool x_flag = true) const;
|
||||
|
||||
/**
|
||||
* @brief Disassemble multiple instructions
|
||||
* @param start_address Starting address
|
||||
* @param count Number of instructions to disassemble
|
||||
* @param read_byte Function to read bytes from memory
|
||||
* @param m_flag Accumulator/memory size flag
|
||||
* @param x_flag Index register size flag
|
||||
* @return Vector of disassembled instructions
|
||||
*/
|
||||
std::vector<DisassembledInstruction> DisassembleRange(
|
||||
uint32_t start_address,
|
||||
size_t count,
|
||||
MemoryReader read_byte,
|
||||
bool m_flag = true,
|
||||
bool x_flag = true) const;
|
||||
|
||||
/**
|
||||
* @brief Set optional symbol resolver for address lookups
|
||||
*/
|
||||
void SetSymbolResolver(SymbolResolver resolver) {
|
||||
symbol_resolver_ = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get instruction info for an opcode
|
||||
*/
|
||||
const InstructionInfo& GetInstructionInfo(uint8_t opcode) const;
|
||||
|
||||
/**
|
||||
* @brief Calculate actual instruction size based on flags
|
||||
*/
|
||||
uint8_t GetInstructionSize(uint8_t opcode, bool m_flag, bool x_flag) const;
|
||||
|
||||
private:
|
||||
// Initialize opcode tables
|
||||
void InitializeOpcodeTable();
|
||||
|
||||
// Format operand based on addressing mode
|
||||
std::string FormatOperand(AddressingMode65816 mode,
|
||||
const std::vector<uint8_t>& operands,
|
||||
uint32_t address,
|
||||
bool m_flag,
|
||||
bool x_flag) const;
|
||||
|
||||
// Calculate branch target
|
||||
uint32_t CalculateBranchTarget(uint32_t address,
|
||||
const std::vector<uint8_t>& operands,
|
||||
AddressingMode65816 mode,
|
||||
uint8_t instruction_size) const;
|
||||
|
||||
// Opcode to instruction info mapping
|
||||
InstructionInfo opcode_table_[256];
|
||||
|
||||
// Optional symbol resolver
|
||||
SymbolResolver symbol_resolver_;
|
||||
};
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_DEBUG_DISASSEMBLER_H_
|
||||
710
src/app/emu/debug/semantic_introspection.cc
Normal file
710
src/app/emu/debug/semantic_introspection.cc
Normal file
@@ -0,0 +1,710 @@
|
||||
#include "app/emu/debug/semantic_introspection.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
SemanticIntrospectionEngine::SemanticIntrospectionEngine(Memory* memory)
|
||||
: memory_(memory) {
|
||||
if (!memory_) {
|
||||
// Handle null pointer gracefully - this should be caught at construction
|
||||
}
|
||||
}
|
||||
|
||||
absl::StatusOr<SemanticGameState> SemanticIntrospectionEngine::GetSemanticState() {
|
||||
if (!memory_) {
|
||||
return absl::InvalidArgumentError("Memory pointer is null");
|
||||
}
|
||||
|
||||
SemanticGameState state;
|
||||
|
||||
// Get game mode state
|
||||
auto game_mode = GetGameModeState();
|
||||
if (!game_mode.ok()) {
|
||||
return game_mode.status();
|
||||
}
|
||||
state.game_mode = *game_mode;
|
||||
|
||||
// Get player state
|
||||
auto player = GetPlayerState();
|
||||
if (!player.ok()) {
|
||||
return player.status();
|
||||
}
|
||||
state.player = *player;
|
||||
|
||||
// Get location context
|
||||
auto location = GetLocationContext();
|
||||
if (!location.ok()) {
|
||||
return location.status();
|
||||
}
|
||||
state.location = *location;
|
||||
|
||||
// Get sprite states
|
||||
auto sprites = GetSpriteStates();
|
||||
if (!sprites.ok()) {
|
||||
return sprites.status();
|
||||
}
|
||||
state.sprites = *sprites;
|
||||
|
||||
// Get frame info
|
||||
state.frame.frame_counter = memory_->ReadByte(alttp::kFrameCounter);
|
||||
state.frame.is_lag_frame = false; // TODO: Implement lag frame detection
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> SemanticIntrospectionEngine::GetStateAsJson() {
|
||||
auto state = GetSemanticState();
|
||||
if (!state.ok()) {
|
||||
return state.status();
|
||||
}
|
||||
|
||||
json j;
|
||||
|
||||
// Game mode
|
||||
j["game_mode"]["main_mode"] = state->game_mode.main_mode;
|
||||
j["game_mode"]["submodule"] = state->game_mode.submodule;
|
||||
j["game_mode"]["mode_name"] = state->game_mode.mode_name;
|
||||
j["game_mode"]["in_game"] = state->game_mode.in_game;
|
||||
j["game_mode"]["in_transition"] = state->game_mode.in_transition;
|
||||
|
||||
// Player
|
||||
j["player"]["x"] = state->player.x;
|
||||
j["player"]["y"] = state->player.y;
|
||||
j["player"]["state"] = state->player.state_name;
|
||||
j["player"]["direction"] = state->player.direction_name;
|
||||
j["player"]["layer"] = state->player.layer;
|
||||
j["player"]["health"] = state->player.health;
|
||||
j["player"]["max_health"] = state->player.max_health;
|
||||
|
||||
// Location
|
||||
j["location"]["indoors"] = state->location.indoors;
|
||||
if (state->location.indoors) {
|
||||
j["location"]["dungeon_room"] = state->location.dungeon_room;
|
||||
j["location"]["room_name"] = state->location.room_name;
|
||||
} else {
|
||||
j["location"]["overworld_area"] = state->location.overworld_area;
|
||||
j["location"]["area_name"] = state->location.area_name;
|
||||
}
|
||||
|
||||
// Sprites
|
||||
j["sprites"] = json::array();
|
||||
for (const auto& sprite : state->sprites) {
|
||||
json sprite_json;
|
||||
sprite_json["id"] = sprite.id;
|
||||
sprite_json["type"] = sprite.type_name;
|
||||
sprite_json["x"] = sprite.x;
|
||||
sprite_json["y"] = sprite.y;
|
||||
sprite_json["state"] = sprite.state_name;
|
||||
j["sprites"].push_back(sprite_json);
|
||||
}
|
||||
|
||||
// Frame
|
||||
j["frame"]["counter"] = state->frame.frame_counter;
|
||||
j["frame"]["is_lag"] = state->frame.is_lag_frame;
|
||||
|
||||
return j.dump(2); // Pretty print with 2-space indentation
|
||||
}
|
||||
|
||||
absl::StatusOr<PlayerState> SemanticIntrospectionEngine::GetPlayerState() {
|
||||
if (!memory_) {
|
||||
return absl::InvalidArgumentError("Memory pointer is null");
|
||||
}
|
||||
|
||||
PlayerState player;
|
||||
|
||||
// Read player coordinates
|
||||
uint8_t x_low = memory_->ReadByte(alttp::kLinkXLow);
|
||||
uint8_t x_high = memory_->ReadByte(alttp::kLinkXHigh);
|
||||
player.x = (x_high << 8) | x_low;
|
||||
|
||||
uint8_t y_low = memory_->ReadByte(alttp::kLinkYLow);
|
||||
uint8_t y_high = memory_->ReadByte(alttp::kLinkYHigh);
|
||||
player.y = (y_high << 8) | y_low;
|
||||
|
||||
// Read player state
|
||||
player.state = memory_->ReadByte(alttp::kLinkState);
|
||||
player.state_name = GetPlayerStateName(player.state);
|
||||
|
||||
// Read direction
|
||||
player.direction = memory_->ReadByte(alttp::kLinkDirection);
|
||||
player.direction_name = GetPlayerDirectionName(player.direction);
|
||||
|
||||
// Read layer
|
||||
player.layer = memory_->ReadByte(alttp::kLinkLayer);
|
||||
|
||||
// Read health
|
||||
player.health = memory_->ReadByte(alttp::kLinkHealth);
|
||||
player.max_health = memory_->ReadByte(alttp::kLinkMaxHealth);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::vector<SpriteState>> SemanticIntrospectionEngine::GetSpriteStates() {
|
||||
if (!memory_) {
|
||||
return absl::InvalidArgumentError("Memory pointer is null");
|
||||
}
|
||||
|
||||
std::vector<SpriteState> sprites;
|
||||
|
||||
// Check up to 16 sprite slots
|
||||
for (uint8_t i = 0; i < 16; ++i) {
|
||||
uint8_t state = memory_->ReadByte(alttp::kSpriteState + i);
|
||||
|
||||
// Skip inactive sprites (state 0 typically means inactive)
|
||||
if (state == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SpriteState sprite;
|
||||
sprite.id = i;
|
||||
|
||||
// Read sprite coordinates
|
||||
uint8_t x_low = memory_->ReadByte(alttp::kSpriteXLow + i);
|
||||
uint8_t x_high = memory_->ReadByte(alttp::kSpriteXHigh + i);
|
||||
sprite.x = (x_high << 8) | x_low;
|
||||
|
||||
uint8_t y_low = memory_->ReadByte(alttp::kSpriteYLow + i);
|
||||
uint8_t y_high = memory_->ReadByte(alttp::kSpriteYHigh + i);
|
||||
sprite.y = (y_high << 8) | y_low;
|
||||
|
||||
// Read sprite type and state
|
||||
sprite.type = memory_->ReadByte(alttp::kSpriteType + i);
|
||||
sprite.type_name = GetSpriteTypeName(sprite.type);
|
||||
sprite.state = state;
|
||||
sprite.state_name = GetSpriteStateName(state);
|
||||
|
||||
sprites.push_back(sprite);
|
||||
}
|
||||
|
||||
return sprites;
|
||||
}
|
||||
|
||||
absl::StatusOr<LocationContext> SemanticIntrospectionEngine::GetLocationContext() {
|
||||
if (!memory_) {
|
||||
return absl::InvalidArgumentError("Memory pointer is null");
|
||||
}
|
||||
|
||||
LocationContext location;
|
||||
|
||||
// Check if indoors
|
||||
location.indoors = memory_->ReadByte(alttp::kIndoorFlag) != 0;
|
||||
|
||||
if (location.indoors) {
|
||||
// Read dungeon room (16-bit)
|
||||
uint8_t room_low = memory_->ReadByte(alttp::kDungeonRoomLow);
|
||||
uint8_t room_high = memory_->ReadByte(alttp::kDungeonRoomHigh);
|
||||
location.dungeon_room = (room_high << 8) | room_low;
|
||||
location.room_name = GetDungeonRoomName(location.dungeon_room);
|
||||
location.area_name = ""; // Not applicable for dungeons
|
||||
} else {
|
||||
// Read overworld area
|
||||
location.overworld_area = memory_->ReadByte(alttp::kOverworldArea);
|
||||
location.area_name = GetOverworldAreaName(location.overworld_area);
|
||||
location.room_name = ""; // Not applicable for overworld
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
absl::StatusOr<GameModeState> SemanticIntrospectionEngine::GetGameModeState() {
|
||||
if (!memory_) {
|
||||
return absl::InvalidArgumentError("Memory pointer is null");
|
||||
}
|
||||
|
||||
GameModeState mode;
|
||||
|
||||
mode.main_mode = memory_->ReadByte(alttp::kGameMode);
|
||||
mode.submodule = memory_->ReadByte(alttp::kSubmodule);
|
||||
mode.mode_name = GetGameModeName(mode.main_mode, mode.submodule);
|
||||
|
||||
// Determine if in-game (modes 0x07-0x18 are generally gameplay)
|
||||
mode.in_game = (mode.main_mode >= 0x07 && mode.main_mode <= 0x18);
|
||||
|
||||
// Check for transition states (modes that involve screen transitions)
|
||||
mode.in_transition = (mode.main_mode == 0x0F || mode.main_mode == 0x10 ||
|
||||
mode.main_mode == 0x11 || mode.main_mode == 0x12);
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
// Helper method implementations
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetGameModeName(uint8_t mode, uint8_t submodule) {
|
||||
switch (mode) {
|
||||
case 0x00: return "Startup/Initial";
|
||||
case 0x01: return "Title Screen";
|
||||
case 0x02: return "File Select";
|
||||
case 0x03: return "Name Entry";
|
||||
case 0x04: return "Delete Save";
|
||||
case 0x05: return "Load Game";
|
||||
case 0x06: return "Pre-Dungeon";
|
||||
case 0x07: return "Dungeon";
|
||||
case 0x08: return "Pre-Overworld";
|
||||
case 0x09: return "Overworld";
|
||||
case 0x0A: return "Pre-Overworld (Special)";
|
||||
case 0x0B: return "Overworld (Special)";
|
||||
case 0x0C: return "Unknown Mode";
|
||||
case 0x0D: return "Blank Screen";
|
||||
case 0x0E: return "Text/Dialog";
|
||||
case 0x0F: return "Screen Transition";
|
||||
case 0x10: return "Room Transition";
|
||||
case 0x11: return "Overworld Transition";
|
||||
case 0x12: return "Message";
|
||||
case 0x13: return "Death Sequence";
|
||||
case 0x14: return "Attract Mode";
|
||||
case 0x15: return "Mirror Warp";
|
||||
case 0x16: return "Refill Stats";
|
||||
case 0x17: return "Game Over";
|
||||
case 0x18: return "Triforce Room";
|
||||
case 0x19: return "Victory";
|
||||
case 0x1A: return "Ending Sequence";
|
||||
case 0x1B: return "Credits";
|
||||
default: return "Unknown (" + std::to_string(mode) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetPlayerStateName(uint8_t state) {
|
||||
switch (state) {
|
||||
case 0x00: return "Standing";
|
||||
case 0x01: return "Walking";
|
||||
case 0x02: return "Turning";
|
||||
case 0x03: return "Pushing";
|
||||
case 0x04: return "Swimming";
|
||||
case 0x05: return "Attacking";
|
||||
case 0x06: return "Spin Attack";
|
||||
case 0x07: return "Item Use";
|
||||
case 0x08: return "Lifting";
|
||||
case 0x09: return "Throwing";
|
||||
case 0x0A: return "Stunned";
|
||||
case 0x0B: return "Jumping";
|
||||
case 0x0C: return "Falling";
|
||||
case 0x0D: return "Dashing";
|
||||
case 0x0E: return "Hookshot";
|
||||
case 0x0F: return "Carrying";
|
||||
case 0x10: return "Sitting";
|
||||
case 0x11: return "Telepathy";
|
||||
case 0x12: return "Bunny";
|
||||
case 0x13: return "Sleep";
|
||||
case 0x14: return "Cape";
|
||||
case 0x15: return "Dying";
|
||||
case 0x16: return "Tree Pull";
|
||||
case 0x17: return "Spin Jump";
|
||||
default: return "Unknown (" + std::to_string(state) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetPlayerDirectionName(uint8_t direction) {
|
||||
switch (direction) {
|
||||
case 0: return "North";
|
||||
case 2: return "South";
|
||||
case 4: return "West";
|
||||
case 6: return "East";
|
||||
default: return "Unknown (" + std::to_string(direction) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetSpriteTypeName(uint8_t type) {
|
||||
// Common ALTTP sprite types (subset for demonstration)
|
||||
switch (type) {
|
||||
case 0x00: return "Raven";
|
||||
case 0x01: return "Vulture";
|
||||
case 0x02: return "Flying Stalfos Head";
|
||||
case 0x03: return "Empty";
|
||||
case 0x04: return "Pull Switch";
|
||||
case 0x05: return "Pull Switch (unused)";
|
||||
case 0x06: return "Pull Switch (wrong)";
|
||||
case 0x07: return "Pull Switch (unused)";
|
||||
case 0x08: return "Octorok (one way)";
|
||||
case 0x09: return "Moldorm (boss)";
|
||||
case 0x0A: return "Octorok (four way)";
|
||||
case 0x0B: return "Chicken";
|
||||
case 0x0C: return "Octorok (stone)";
|
||||
case 0x0D: return "Buzzblob";
|
||||
case 0x0E: return "Snapdragon";
|
||||
case 0x0F: return "Octoballoon";
|
||||
case 0x10: return "Octoballoon Hatchlings";
|
||||
case 0x11: return "Hinox";
|
||||
case 0x12: return "Moblin";
|
||||
case 0x13: return "Mini Helmasaur";
|
||||
case 0x14: return "Thieves' Town Grate";
|
||||
case 0x15: return "Antifairy";
|
||||
case 0x16: return "Sahasrahla";
|
||||
case 0x17: return "Bush Hoarder";
|
||||
case 0x18: return "Mini Moldorm";
|
||||
case 0x19: return "Poe";
|
||||
case 0x1A: return "Smithy";
|
||||
case 0x1B: return "Arrow";
|
||||
case 0x1C: return "Statue";
|
||||
case 0x1D: return "Flutequest";
|
||||
case 0x1E: return "Crystal Switch";
|
||||
case 0x1F: return "Sick Kid";
|
||||
case 0x20: return "Sluggula";
|
||||
case 0x21: return "Water Switch";
|
||||
case 0x22: return "Ropa";
|
||||
case 0x23: return "Red Bari";
|
||||
case 0x24: return "Blue Bari";
|
||||
case 0x25: return "Talking Tree";
|
||||
case 0x26: return "Hardhat Beetle";
|
||||
case 0x27: return "Deadrock";
|
||||
case 0x28: return "Dark World Hint NPC";
|
||||
case 0x29: return "Adult";
|
||||
case 0x2A: return "Sweeping Lady";
|
||||
case 0x2B: return "Hobo";
|
||||
case 0x2C: return "Lumberjacks";
|
||||
case 0x2D: return "Neckless Man";
|
||||
case 0x2E: return "Flute Kid";
|
||||
case 0x2F: return "Race Game Lady";
|
||||
case 0x30: return "Race Game Guy";
|
||||
case 0x31: return "Fortune Teller";
|
||||
case 0x32: return "Angry Brothers";
|
||||
case 0x33: return "Pull For Rupees";
|
||||
case 0x34: return "Young Snitch";
|
||||
case 0x35: return "Innkeeper";
|
||||
case 0x36: return "Witch";
|
||||
case 0x37: return "Waterfall";
|
||||
case 0x38: return "Eye Statue";
|
||||
case 0x39: return "Locksmith";
|
||||
case 0x3A: return "Magic Bat";
|
||||
case 0x3B: return "Bonk Item";
|
||||
case 0x3C: return "Kid In KakTree";
|
||||
case 0x3D: return "Old Snitch Lady";
|
||||
case 0x3E: return "Hoarder";
|
||||
case 0x3F: return "Tutorial Guard";
|
||||
case 0x40: return "Lightning Lock";
|
||||
case 0x41: return "Blue Guard";
|
||||
case 0x42: return "Green Guard";
|
||||
case 0x43: return "Red Spear Guard";
|
||||
case 0x44: return "Bluesain Bolt";
|
||||
case 0x45: return "Usain Bolt";
|
||||
case 0x46: return "Blue Archer";
|
||||
case 0x47: return "Green Bush Guard";
|
||||
case 0x48: return "Red Javelin Guard";
|
||||
case 0x49: return "Red Bush Guard";
|
||||
case 0x4A: return "Bomb Guard";
|
||||
case 0x4B: return "Green Knife Guard";
|
||||
case 0x4C: return "Geldman";
|
||||
case 0x4D: return "Toppo";
|
||||
case 0x4E: return "Popo";
|
||||
case 0x4F: return "Popo2";
|
||||
case 0x50: return "Cannonball";
|
||||
case 0x51: return "Armos";
|
||||
case 0x52: return "King Zora";
|
||||
case 0x53: return "Armos Knight (boss)";
|
||||
case 0x54: return "Lanmolas (boss)";
|
||||
case 0x55: return "Fireball Zora";
|
||||
case 0x56: return "Walking Zora";
|
||||
case 0x57: return "Desert Statue";
|
||||
case 0x58: return "Crab";
|
||||
case 0x59: return "Lost Woods Bird";
|
||||
case 0x5A: return "Lost Woods Squirrel";
|
||||
case 0x5B: return "Spark (Left to Right)";
|
||||
case 0x5C: return "Spark (Right to Left)";
|
||||
case 0x5D: return "Roller (vertical moving)";
|
||||
case 0x5E: return "Roller (vertical moving)";
|
||||
case 0x5F: return "Roller";
|
||||
case 0x60: return "Roller (horizontal moving)";
|
||||
case 0x61: return "Beamos";
|
||||
case 0x62: return "Master Sword";
|
||||
case 0x63: return "Debirando Pit";
|
||||
case 0x64: return "Debirando";
|
||||
case 0x65: return "Archery Guy";
|
||||
case 0x66: return "Wall Cannon (vertical left)";
|
||||
case 0x67: return "Wall Cannon (vertical right)";
|
||||
case 0x68: return "Wall Cannon (horizontal top)";
|
||||
case 0x69: return "Wall Cannon (horizontal bottom)";
|
||||
case 0x6A: return "Ball N' Chain";
|
||||
case 0x6B: return "Cannon Soldier";
|
||||
case 0x6C: return "Cannon Soldier";
|
||||
case 0x6D: return "Mirror Portal";
|
||||
case 0x6E: return "Rat";
|
||||
case 0x6F: return "Rope";
|
||||
case 0x70: return "Keese";
|
||||
case 0x71: return "Helmasaur King Fireball";
|
||||
case 0x72: return "Leever";
|
||||
case 0x73: return "Pond Trigger";
|
||||
case 0x74: return "Uncle Priest";
|
||||
case 0x75: return "Running Man";
|
||||
case 0x76: return "Bottle Salesman";
|
||||
case 0x77: return "Princess Zelda";
|
||||
case 0x78: return "Antifairy (alternate)";
|
||||
case 0x79: return "Village Elder";
|
||||
case 0x7A: return "Bee";
|
||||
case 0x7B: return "Agahnim";
|
||||
case 0x7C: return "Agahnim Ball";
|
||||
case 0x7D: return "Green Stalfos";
|
||||
case 0x7E: return "Big Spike";
|
||||
case 0x7F: return "Firebar (clockwise)";
|
||||
case 0x80: return "Firebar (counterclockwise)";
|
||||
case 0x81: return "Firesnake";
|
||||
case 0x82: return "Hover";
|
||||
case 0x83: return "Green Eyegore";
|
||||
case 0x84: return "Red Eyegore";
|
||||
case 0x85: return "Yellow Stalfos";
|
||||
case 0x86: return "Kodongo";
|
||||
case 0x87: return "Flames";
|
||||
case 0x88: return "Mothula (boss)";
|
||||
case 0x89: return "Mothula Beam";
|
||||
case 0x8A: return "Spike Block";
|
||||
case 0x8B: return "Gibdo";
|
||||
case 0x8C: return "Arrghus (boss)";
|
||||
case 0x8D: return "Arrghus spawn";
|
||||
case 0x8E: return "Terrorpin";
|
||||
case 0x8F: return "Slime";
|
||||
case 0x90: return "Wallmaster";
|
||||
case 0x91: return "Stalfos Knight";
|
||||
case 0x92: return "Helmasaur King";
|
||||
case 0x93: return "Bumper";
|
||||
case 0x94: return "Pirogusu";
|
||||
case 0x95: return "Laser Eye (left)";
|
||||
case 0x96: return "Laser Eye (right)";
|
||||
case 0x97: return "Laser Eye (top)";
|
||||
case 0x98: return "Laser Eye (bottom)";
|
||||
case 0x99: return "Pengator";
|
||||
case 0x9A: return "Kyameron";
|
||||
case 0x9B: return "Wizzrobe";
|
||||
case 0x9C: return "Zoro";
|
||||
case 0x9D: return "Babasu";
|
||||
case 0x9E: return "Haunted Grove Ostritch";
|
||||
case 0x9F: return "Haunted Grove Rabbit";
|
||||
case 0xA0: return "Haunted Grove Bird";
|
||||
case 0xA1: return "Freezor";
|
||||
case 0xA2: return "Kholdstare";
|
||||
case 0xA3: return "Kholdstare Shell";
|
||||
case 0xA4: return "Falling Ice";
|
||||
case 0xA5: return "Zazak (blue)";
|
||||
case 0xA6: return "Zazak (red)";
|
||||
case 0xA7: return "Stalfos";
|
||||
case 0xA8: return "Bomber Flying Creatures from Darkworld";
|
||||
case 0xA9: return "Bomber Flying Creatures from Darkworld";
|
||||
case 0xAA: return "Pikit";
|
||||
case 0xAB: return "Maiden";
|
||||
case 0xAC: return "Apple";
|
||||
case 0xAD: return "Lost Old Man";
|
||||
case 0xAE: return "Down Pipe";
|
||||
case 0xAF: return "Up Pipe";
|
||||
case 0xB0: return "Right Pip";
|
||||
case 0xB1: return "Left Pipe";
|
||||
case 0xB2: return "Good Bee Again";
|
||||
case 0xB3: return "Hylian Inscription";
|
||||
case 0xB4: return "Thief's chest";
|
||||
case 0xB5: return "Bomb Salesman";
|
||||
case 0xB6: return "Kiki";
|
||||
case 0xB7: return "Blind Maiden";
|
||||
case 0xB8: return "Dialogue Tester";
|
||||
case 0xB9: return "Bully / Pink Ball";
|
||||
case 0xBA: return "Whirlpool";
|
||||
case 0xBB: return "Shopkeeper";
|
||||
case 0xBC: return "Drunk in the Inn";
|
||||
case 0xBD: return "Vitreous (boss)";
|
||||
case 0xBE: return "Vitreous small eye";
|
||||
case 0xBF: return "Vitreous' lightning";
|
||||
case 0xC0: return "Monster in Lake of Ill Omen";
|
||||
case 0xC1: return "Quicksand";
|
||||
case 0xC2: return "Gibo";
|
||||
case 0xC3: return "Thief";
|
||||
case 0xC4: return "Medusa";
|
||||
case 0xC5: return "4-Way Shooter";
|
||||
case 0xC6: return "Pokey";
|
||||
case 0xC7: return "Big Fairy";
|
||||
case 0xC8: return "Tektite";
|
||||
case 0xC9: return "Chain Chomp";
|
||||
case 0xCA: return "Trinexx Rock Head";
|
||||
case 0xCB: return "Trinexx Fire Head";
|
||||
case 0xCC: return "Trinexx Ice Head";
|
||||
case 0xCD: return "Blind (boss)";
|
||||
case 0xCE: return "Blind Laser";
|
||||
case 0xCF: return "Running Stalfos Head";
|
||||
case 0xD0: return "Lynel";
|
||||
case 0xD1: return "Bunny Beam";
|
||||
case 0xD2: return "Flopping Fish";
|
||||
case 0xD3: return "Stal";
|
||||
case 0xD4: return "Landmine";
|
||||
case 0xD5: return "Digging Game Guy";
|
||||
case 0xD6: return "Ganon";
|
||||
case 0xD7: return "Ganon Fire";
|
||||
case 0xD8: return "Heart";
|
||||
case 0xD9: return "Green Rupee";
|
||||
case 0xDA: return "Blue Rupee";
|
||||
case 0xDB: return "Red Rupee";
|
||||
case 0xDC: return "Bomb Refill (1)";
|
||||
case 0xDD: return "Bomb Refill (4)";
|
||||
case 0xDE: return "Bomb Refill (8)";
|
||||
case 0xDF: return "Small Magic Refill";
|
||||
case 0xE0: return "Full Magic Refill";
|
||||
case 0xE1: return "Arrow Refill (5)";
|
||||
case 0xE2: return "Arrow Refill (10)";
|
||||
case 0xE3: return "Fairy";
|
||||
case 0xE4: return "Small Key";
|
||||
case 0xE5: return "Big Key";
|
||||
case 0xE6: return "Shield";
|
||||
case 0xE7: return "Mushroom";
|
||||
case 0xE8: return "Fake Master Sword";
|
||||
case 0xE9: return "Magic Shop Assistant";
|
||||
case 0xEA: return "Heart Container";
|
||||
case 0xEB: return "Heart Piece";
|
||||
case 0xEC: return "Thrown Item";
|
||||
case 0xED: return "Somaria Platform";
|
||||
case 0xEE: return "Castle Mantle";
|
||||
case 0xEF: return "Somaria Platform (unused)";
|
||||
case 0xF0: return "Somaria Platform (unused)";
|
||||
case 0xF1: return "Somaria Platform (unused)";
|
||||
case 0xF2: return "Medallion Tablet";
|
||||
default: return "Unknown Sprite (" + std::to_string(type) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetSpriteStateName(uint8_t state) {
|
||||
// Generic sprite state names (actual states vary by sprite type)
|
||||
switch (state) {
|
||||
case 0x00: return "Inactive";
|
||||
case 0x01: return "Spawning";
|
||||
case 0x02: return "Normal";
|
||||
case 0x03: return "Held";
|
||||
case 0x04: return "Stunned";
|
||||
case 0x05: return "Falling";
|
||||
case 0x06: return "Dead";
|
||||
case 0x07: return "Unused1";
|
||||
case 0x08: return "Active";
|
||||
case 0x09: return "Recoil";
|
||||
case 0x0A: return "Carried";
|
||||
case 0x0B: return "Frozen";
|
||||
default: return "State " + std::to_string(state);
|
||||
}
|
||||
}
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetOverworldAreaName(uint8_t area) {
|
||||
// ALTTP overworld areas
|
||||
switch (area) {
|
||||
case 0x00: return "Lost Woods";
|
||||
case 0x02: return "Lumberjack Tree";
|
||||
case 0x03: case 0x04: case 0x05: case 0x06:
|
||||
return "West Death Mountain";
|
||||
case 0x07: return "East Death Mountain";
|
||||
case 0x0A: return "Mountain Entry";
|
||||
case 0x0F: return "Waterfall of Wishing";
|
||||
case 0x10: return "Lost Woods Alcove";
|
||||
case 0x11: return "North of Kakariko";
|
||||
case 0x12: case 0x13: case 0x14: return "Northwest Pond";
|
||||
case 0x15: return "Desert Area";
|
||||
case 0x16: case 0x17: return "Desert Palace Entrance";
|
||||
case 0x18: return "Kakariko Village";
|
||||
case 0x1A: return "Pond of Happiness";
|
||||
case 0x1B: case 0x1C: return "West Hyrule";
|
||||
case 0x1D: return "Link's House";
|
||||
case 0x1E: return "East Hyrule";
|
||||
case 0x22: return "Smithy House";
|
||||
case 0x25: return "Zora's Domain";
|
||||
case 0x28: return "Haunted Grove Entrance";
|
||||
case 0x29: case 0x2A: return "West Hyrule";
|
||||
case 0x2B: return "Hyrule Castle";
|
||||
case 0x2C: return "East Hyrule";
|
||||
case 0x2D: case 0x2E: return "Eastern Palace";
|
||||
case 0x2F: return "Marsh";
|
||||
case 0x30: return "Desert of Mystery";
|
||||
case 0x32: return "Haunted Grove";
|
||||
case 0x33: case 0x34: return "West Hyrule";
|
||||
case 0x35: return "Graveyard";
|
||||
case 0x37: return "Waterfall Lake";
|
||||
case 0x39: case 0x3A: return "South Hyrule";
|
||||
case 0x3B: return "Pyramid";
|
||||
case 0x3C: return "East Dark World";
|
||||
case 0x3F: return "Marsh";
|
||||
case 0x40: return "Skull Woods";
|
||||
case 0x42: return "Dark Lumberjack Tree";
|
||||
case 0x43: case 0x44: case 0x45: return "West Death Mountain";
|
||||
case 0x47: return "Turtle Rock";
|
||||
case 0x4A: return "Bumper Cave Entry";
|
||||
case 0x4F: return "Dark Waterfall";
|
||||
case 0x50: return "Skull Woods Alcove";
|
||||
case 0x51: return "North of Outcasts";
|
||||
case 0x52: case 0x53: case 0x54: return "Northwest Dark World";
|
||||
case 0x55: return "Dark Desert";
|
||||
case 0x56: case 0x57: return "Misery Mire";
|
||||
case 0x58: return "Village of Outcasts";
|
||||
case 0x5A: return "Dark Pond of Happiness";
|
||||
case 0x5B: return "West Dark World";
|
||||
case 0x5D: return "Dark Link's House";
|
||||
case 0x5E: return "East Dark World";
|
||||
case 0x62: return "Haunted Grove";
|
||||
case 0x65: return "Dig Game";
|
||||
case 0x68: return "Dark Haunted Grove Entrance";
|
||||
case 0x69: case 0x6A: return "West Dark World";
|
||||
case 0x6B: return "Pyramid of Power";
|
||||
case 0x6C: return "East Dark World";
|
||||
case 0x6D: case 0x6E: return "Shield Shop";
|
||||
case 0x6F: return "Dark Marsh";
|
||||
case 0x70: return "Misery Mire";
|
||||
case 0x72: return "Dark Haunted Grove";
|
||||
case 0x73: case 0x74: return "West Dark World";
|
||||
case 0x75: return "Dark Graveyard";
|
||||
case 0x77: return "Palace of Darkness";
|
||||
case 0x7A: return "South Dark World";
|
||||
case 0x7B: return "Pyramid of Power";
|
||||
case 0x7C: return "East Dark World";
|
||||
case 0x7F: return "Swamp Palace";
|
||||
case 0x80: return "Master Sword Grove";
|
||||
case 0x81: return "Zora's Domain";
|
||||
default: return "Area " + std::to_string(area);
|
||||
}
|
||||
}
|
||||
|
||||
std::string SemanticIntrospectionEngine::GetDungeonRoomName(uint16_t room) {
|
||||
// Simplified dungeon room naming - actual names depend on extensive lookup
|
||||
// This is a small subset for demonstration
|
||||
if (room < 0x100) {
|
||||
// Light World dungeons
|
||||
if (room >= 0x00 && room <= 0x0F) {
|
||||
return "Sewer/Escape Room " + std::to_string(room);
|
||||
} else if (room >= 0x20 && room <= 0x3F) {
|
||||
return "Hyrule Castle Room " + std::to_string(room - 0x20);
|
||||
} else if (room >= 0x50 && room <= 0x5F) {
|
||||
return "Castle Tower Room " + std::to_string(room - 0x50);
|
||||
} else if (room >= 0x60 && room <= 0x6F) {
|
||||
return "Agahnim Tower Room " + std::to_string(room - 0x60);
|
||||
} else if (room >= 0x70 && room <= 0x7F) {
|
||||
return "Swamp Palace Room " + std::to_string(room - 0x70);
|
||||
} else if (room >= 0x80 && room <= 0x8F) {
|
||||
return "Skull Woods Room " + std::to_string(room - 0x80);
|
||||
} else if (room >= 0x90 && room <= 0x9F) {
|
||||
return "Thieves' Town Room " + std::to_string(room - 0x90);
|
||||
} else if (room >= 0xA0 && room <= 0xAF) {
|
||||
return "Ice Palace Room " + std::to_string(room - 0xA0);
|
||||
} else if (room >= 0xB0 && room <= 0xBF) {
|
||||
return "Misery Mire Room " + std::to_string(room - 0xB0);
|
||||
} else if (room >= 0xC0 && room <= 0xCF) {
|
||||
return "Turtle Rock Room " + std::to_string(room - 0xC0);
|
||||
} else if (room >= 0xD0 && room <= 0xDF) {
|
||||
return "Palace of Darkness Room " + std::to_string(room - 0xD0);
|
||||
} else if (room >= 0xE0 && room <= 0xEF) {
|
||||
return "Desert Palace Room " + std::to_string(room - 0xE0);
|
||||
} else if (room >= 0xF0 && room <= 0xFF) {
|
||||
return "Eastern Palace Room " + std::to_string(room - 0xF0);
|
||||
}
|
||||
}
|
||||
|
||||
// Special rooms
|
||||
switch (room) {
|
||||
case 0x00: return "Sewer Entrance";
|
||||
case 0x01: return "Hyrule Castle North Corridor";
|
||||
case 0x02: return "Switch Room (Escape)";
|
||||
case 0x10: return "Ganon Tower Entrance";
|
||||
case 0x11: return "Ganon Tower Stairs";
|
||||
case 0x20: return "Ganon Tower Big Chest";
|
||||
case 0x30: return "Ganon Tower Final Approach";
|
||||
case 0x40: return "Ganon Tower Top";
|
||||
case 0x41: return "Ganon Arena";
|
||||
default: return "Room " + std::to_string(room);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
189
src/app/emu/debug/semantic_introspection.h
Normal file
189
src/app/emu/debug/semantic_introspection.h
Normal file
@@ -0,0 +1,189 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_SEMANTIC_INTROSPECTION_H
|
||||
#define YAZE_APP_EMU_DEBUG_SEMANTIC_INTROSPECTION_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
// ALTTP-specific RAM addresses
|
||||
namespace alttp {
|
||||
// Game Mode
|
||||
constexpr uint32_t kGameMode = 0x7E0010;
|
||||
constexpr uint32_t kSubmodule = 0x7E0011;
|
||||
constexpr uint32_t kIndoorFlag = 0x7E001B;
|
||||
|
||||
// Player
|
||||
constexpr uint32_t kLinkYLow = 0x7E0020;
|
||||
constexpr uint32_t kLinkYHigh = 0x7E0021;
|
||||
constexpr uint32_t kLinkXLow = 0x7E0022;
|
||||
constexpr uint32_t kLinkXHigh = 0x7E0023;
|
||||
constexpr uint32_t kLinkDirection = 0x7E002F;
|
||||
constexpr uint32_t kLinkState = 0x7E005D;
|
||||
constexpr uint32_t kLinkLayer = 0x7E00EE;
|
||||
constexpr uint32_t kLinkHealth = 0x7E00F6;
|
||||
constexpr uint32_t kLinkMaxHealth = 0x7E00F7;
|
||||
|
||||
// Location
|
||||
constexpr uint32_t kOverworldArea = 0x7E008A;
|
||||
constexpr uint32_t kDungeonRoom = 0x7E00A0;
|
||||
constexpr uint32_t kDungeonRoomLow = 0x7E00A0;
|
||||
constexpr uint32_t kDungeonRoomHigh = 0x7E00A1;
|
||||
|
||||
// Sprites (base addresses, add slot offset 0-15)
|
||||
constexpr uint32_t kSpriteYLow = 0x7E0D00;
|
||||
constexpr uint32_t kSpriteYHigh = 0x7E0D20;
|
||||
constexpr uint32_t kSpriteXLow = 0x7E0D10;
|
||||
constexpr uint32_t kSpriteXHigh = 0x7E0D30;
|
||||
constexpr uint32_t kSpriteState = 0x7E0DD0;
|
||||
constexpr uint32_t kSpriteType = 0x7E0E20;
|
||||
|
||||
// Frame timing
|
||||
constexpr uint32_t kFrameCounter = 0x7E001A;
|
||||
} // namespace alttp
|
||||
|
||||
/**
|
||||
* @brief Semantic representation of a sprite entity
|
||||
*/
|
||||
struct SpriteState {
|
||||
uint8_t id; // Sprite slot ID (0-15)
|
||||
uint16_t x; // X coordinate
|
||||
uint16_t y; // Y coordinate
|
||||
uint8_t type; // Sprite type ID
|
||||
std::string type_name; // Human-readable sprite type name
|
||||
uint8_t state; // Sprite state
|
||||
std::string state_name; // Human-readable state (Active, Dead, etc.)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Semantic representation of the player state
|
||||
*/
|
||||
struct PlayerState {
|
||||
uint16_t x; // X coordinate
|
||||
uint16_t y; // Y coordinate
|
||||
uint8_t state; // Action state
|
||||
std::string state_name; // Human-readable state (Walking, Attacking, etc.)
|
||||
uint8_t direction; // Facing direction (0=up, 2=down, 4=left, 6=right)
|
||||
std::string direction_name; // Human-readable direction
|
||||
uint8_t layer; // Z-layer (upper/lower)
|
||||
uint8_t health; // Current health
|
||||
uint8_t max_health; // Maximum health
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Semantic representation of the current location
|
||||
*/
|
||||
struct LocationContext {
|
||||
bool indoors; // True if in dungeon/house, false if overworld
|
||||
uint8_t overworld_area; // Overworld area ID (if outdoors)
|
||||
uint16_t dungeon_room; // Dungeon room ID (if indoors)
|
||||
std::string room_name; // Human-readable location name
|
||||
std::string area_name; // Human-readable area name
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Semantic representation of the game mode
|
||||
*/
|
||||
struct GameModeState {
|
||||
uint8_t main_mode; // Main game mode value
|
||||
uint8_t submodule; // Submodule value
|
||||
std::string mode_name; // Human-readable mode name
|
||||
bool in_game; // True if actively playing (not in menu/title)
|
||||
bool in_transition; // True if transitioning between screens
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Frame timing information
|
||||
*/
|
||||
struct FrameInfo {
|
||||
uint8_t frame_counter; // Current frame counter value
|
||||
bool is_lag_frame; // True if this frame is lagging
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Complete semantic game state
|
||||
*/
|
||||
struct SemanticGameState {
|
||||
GameModeState game_mode;
|
||||
PlayerState player;
|
||||
LocationContext location;
|
||||
std::vector<SpriteState> sprites;
|
||||
FrameInfo frame;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Engine for extracting semantic game state from SNES memory
|
||||
*
|
||||
* This class provides high-level semantic interpretations of raw SNES RAM
|
||||
* values, making it easier for AI agents to understand the game state without
|
||||
* needing to know the specific memory addresses or value encodings.
|
||||
*/
|
||||
class SemanticIntrospectionEngine {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Semantic Introspection Engine
|
||||
* @param memory Pointer to the SNES memory interface
|
||||
*/
|
||||
explicit SemanticIntrospectionEngine(Memory* memory);
|
||||
~SemanticIntrospectionEngine() = default;
|
||||
|
||||
/**
|
||||
* @brief Get the complete semantic game state
|
||||
* @return Current semantic game state
|
||||
*/
|
||||
absl::StatusOr<SemanticGameState> GetSemanticState();
|
||||
|
||||
/**
|
||||
* @brief Get the semantic state as JSON string
|
||||
* @return JSON representation of the current game state
|
||||
*/
|
||||
absl::StatusOr<std::string> GetStateAsJson();
|
||||
|
||||
/**
|
||||
* @brief Get only the player state
|
||||
* @return Current player semantic state
|
||||
*/
|
||||
absl::StatusOr<PlayerState> GetPlayerState();
|
||||
|
||||
/**
|
||||
* @brief Get all active sprite states
|
||||
* @return Vector of active sprite states
|
||||
*/
|
||||
absl::StatusOr<std::vector<SpriteState>> GetSpriteStates();
|
||||
|
||||
/**
|
||||
* @brief Get the current location context
|
||||
* @return Current location semantic state
|
||||
*/
|
||||
absl::StatusOr<LocationContext> GetLocationContext();
|
||||
|
||||
/**
|
||||
* @brief Get the current game mode state
|
||||
* @return Current game mode semantic state
|
||||
*/
|
||||
absl::StatusOr<GameModeState> GetGameModeState();
|
||||
|
||||
private:
|
||||
Memory* memory_; // Non-owning pointer to SNES memory
|
||||
|
||||
// Helper methods for name lookups
|
||||
std::string GetGameModeName(uint8_t mode, uint8_t submodule);
|
||||
std::string GetPlayerStateName(uint8_t state);
|
||||
std::string GetPlayerDirectionName(uint8_t direction);
|
||||
std::string GetSpriteTypeName(uint8_t type);
|
||||
std::string GetSpriteStateName(uint8_t state);
|
||||
std::string GetOverworldAreaName(uint8_t area);
|
||||
std::string GetDungeonRoomName(uint16_t room);
|
||||
};
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_DEBUG_SEMANTIC_INTROSPECTION_H
|
||||
388
src/app/emu/debug/step_controller.cc
Normal file
388
src/app/emu/debug/step_controller.cc
Normal file
@@ -0,0 +1,388 @@
|
||||
#include "app/emu/debug/step_controller.h"
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
bool StepController::IsCallInstruction(uint8_t opcode) {
|
||||
return opcode == opcode::JSR ||
|
||||
opcode == opcode::JSL ||
|
||||
opcode == opcode::JSR_X;
|
||||
}
|
||||
|
||||
bool StepController::IsReturnInstruction(uint8_t opcode) {
|
||||
return opcode == opcode::RTS ||
|
||||
opcode == opcode::RTL ||
|
||||
opcode == opcode::RTI;
|
||||
}
|
||||
|
||||
bool StepController::IsBranchInstruction(uint8_t opcode) {
|
||||
return opcode == opcode::BCC ||
|
||||
opcode == opcode::BCS ||
|
||||
opcode == opcode::BEQ ||
|
||||
opcode == opcode::BMI ||
|
||||
opcode == opcode::BNE ||
|
||||
opcode == opcode::BPL ||
|
||||
opcode == opcode::BVC ||
|
||||
opcode == opcode::BVS ||
|
||||
opcode == opcode::BRA ||
|
||||
opcode == opcode::BRL ||
|
||||
opcode == opcode::JMP_ABS ||
|
||||
opcode == opcode::JMP_IND ||
|
||||
opcode == opcode::JMP_ABS_X ||
|
||||
opcode == opcode::JMP_LONG ||
|
||||
opcode == opcode::JMP_IND_L;
|
||||
}
|
||||
|
||||
uint8_t StepController::GetInstructionSize(uint8_t opcode, bool m_flag,
|
||||
bool x_flag) {
|
||||
// Simplified instruction size calculation
|
||||
// For a full implementation, refer to the Disassembler65816 class
|
||||
|
||||
switch (opcode) {
|
||||
// Implied (1 byte)
|
||||
case 0x00: // BRK
|
||||
case 0x18: // CLC
|
||||
case 0x38: // SEC
|
||||
case 0x58: // CLI
|
||||
case 0x78: // SEI
|
||||
case 0xB8: // CLV
|
||||
case 0xD8: // CLD
|
||||
case 0xF8: // SED
|
||||
case 0x1A: // INC A
|
||||
case 0x3A: // DEC A
|
||||
case 0x1B: // TCS
|
||||
case 0x3B: // TSC
|
||||
case 0x4A: // LSR A
|
||||
case 0x5B: // TCD
|
||||
case 0x6A: // ROR A
|
||||
case 0x7B: // TDC
|
||||
case 0x0A: // ASL A
|
||||
case 0x2A: // ROL A
|
||||
case 0x40: // RTI
|
||||
case 0x60: // RTS
|
||||
case 0x6B: // RTL
|
||||
case 0x8A: // TXA
|
||||
case 0x9A: // TXS
|
||||
case 0x9B: // TXY
|
||||
case 0xAA: // TAX
|
||||
case 0xBA: // TSX
|
||||
case 0xBB: // TYX
|
||||
case 0xCA: // DEX
|
||||
case 0xDA: // PHX
|
||||
case 0xEA: // NOP
|
||||
case 0xFA: // PLX
|
||||
case 0xCB: // WAI
|
||||
case 0xDB: // STP
|
||||
case 0xEB: // XBA
|
||||
case 0xFB: // XCE
|
||||
case 0x08: // PHP
|
||||
case 0x28: // PLP
|
||||
case 0x48: // PHA
|
||||
case 0x68: // PLA
|
||||
case 0x88: // DEY
|
||||
case 0x98: // TYA
|
||||
case 0xA8: // TAY
|
||||
case 0xC8: // INY
|
||||
case 0xE8: // INX
|
||||
case 0x5A: // PHY
|
||||
case 0x7A: // PLY
|
||||
case 0x0B: // PHD
|
||||
case 0x2B: // PLD
|
||||
case 0x4B: // PHK
|
||||
case 0x8B: // PHB
|
||||
case 0xAB: // PLB
|
||||
return 1;
|
||||
|
||||
// Relative branch (2 bytes)
|
||||
case 0x10: // BPL
|
||||
case 0x30: // BMI
|
||||
case 0x50: // BVC
|
||||
case 0x70: // BVS
|
||||
case 0x80: // BRA
|
||||
case 0x90: // BCC
|
||||
case 0xB0: // BCS
|
||||
case 0xD0: // BNE
|
||||
case 0xF0: // BEQ
|
||||
return 2;
|
||||
|
||||
// Relative long (3 bytes)
|
||||
case 0x82: // BRL
|
||||
return 3;
|
||||
|
||||
// JSR absolute (3 bytes)
|
||||
case 0x20: // JSR
|
||||
case 0xFC: // JSR (abs,X)
|
||||
return 3;
|
||||
|
||||
// JSL long (4 bytes)
|
||||
case 0x22: // JSL
|
||||
return 4;
|
||||
|
||||
// Absolute (3 bytes)
|
||||
case 0x4C: // JMP abs
|
||||
case 0x6C: // JMP (abs)
|
||||
case 0x7C: // JMP (abs,X)
|
||||
return 3;
|
||||
|
||||
// Absolute long (4 bytes)
|
||||
case 0x5C: // JMP long
|
||||
case 0xDC: // JMP [abs]
|
||||
return 4;
|
||||
|
||||
default:
|
||||
// For other instructions, use reasonable defaults
|
||||
// This is a simplification - for full accuracy use Disassembler65816
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t StepController::CalculateReturnAddress(uint32_t pc,
|
||||
uint8_t opcode) const {
|
||||
// Return address is pushed onto stack and is the address of the
|
||||
// instruction following the call
|
||||
uint8_t size = GetInstructionSize(opcode, true, true);
|
||||
uint32_t bank = pc & 0xFF0000;
|
||||
|
||||
if (opcode == opcode::JSL) {
|
||||
// JSL pushes PB along with PC+3, so return is full 24-bit
|
||||
return pc + size;
|
||||
} else {
|
||||
// JSR only pushes 16-bit PC, so return stays in same bank
|
||||
return bank | ((pc + size) & 0xFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t StepController::CalculateCallTarget(uint32_t pc,
|
||||
uint8_t opcode) const {
|
||||
if (!read_byte_) return 0;
|
||||
|
||||
uint32_t bank = pc & 0xFF0000;
|
||||
|
||||
switch (opcode) {
|
||||
case opcode::JSR:
|
||||
// JSR abs - 16-bit address in current bank
|
||||
return bank | (read_byte_(pc + 1) | (read_byte_(pc + 2) << 8));
|
||||
|
||||
case opcode::JSL:
|
||||
// JSL long - full 24-bit address
|
||||
return read_byte_(pc + 1) |
|
||||
(read_byte_(pc + 2) << 8) |
|
||||
(read_byte_(pc + 3) << 16);
|
||||
|
||||
case opcode::JSR_X:
|
||||
// JSR (abs,X) - indirect, can't easily determine target
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void StepController::ProcessInstruction(uint32_t pc) {
|
||||
if (!read_byte_) return;
|
||||
|
||||
uint8_t opcode = read_byte_(pc);
|
||||
|
||||
if (IsCallInstruction(opcode)) {
|
||||
// Push call onto stack
|
||||
uint32_t target = CalculateCallTarget(pc, opcode);
|
||||
uint32_t return_addr = CalculateReturnAddress(pc, opcode);
|
||||
bool is_long = (opcode == opcode::JSL);
|
||||
|
||||
call_stack_.emplace_back(pc, target, return_addr, is_long);
|
||||
} else if (IsReturnInstruction(opcode)) {
|
||||
// Pop from call stack if we have entries
|
||||
if (!call_stack_.empty()) {
|
||||
call_stack_.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StepResult StepController::StepInto() {
|
||||
StepResult result;
|
||||
result.success = false;
|
||||
result.instructions_executed = 0;
|
||||
|
||||
if (!step_ || !get_pc_ || !read_byte_) {
|
||||
result.message = "Step controller not properly configured";
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t pc_before = get_pc_();
|
||||
uint8_t opcode = read_byte_(pc_before);
|
||||
|
||||
// Track if this is a call
|
||||
std::optional<CallStackEntry> call_made;
|
||||
if (IsCallInstruction(opcode)) {
|
||||
uint32_t target = CalculateCallTarget(pc_before, opcode);
|
||||
uint32_t return_addr = CalculateReturnAddress(pc_before, opcode);
|
||||
bool is_long = (opcode == opcode::JSL);
|
||||
call_made = CallStackEntry(pc_before, target, return_addr, is_long);
|
||||
call_stack_.push_back(*call_made);
|
||||
}
|
||||
|
||||
// Track if this is a return
|
||||
std::optional<CallStackEntry> return_made;
|
||||
if (IsReturnInstruction(opcode) && !call_stack_.empty()) {
|
||||
return_made = call_stack_.back();
|
||||
call_stack_.pop_back();
|
||||
}
|
||||
|
||||
// Execute the instruction
|
||||
step_();
|
||||
result.instructions_executed = 1;
|
||||
|
||||
uint32_t pc_after = get_pc_();
|
||||
result.new_pc = pc_after;
|
||||
result.success = true;
|
||||
result.call = call_made;
|
||||
result.ret = return_made;
|
||||
|
||||
if (call_made) {
|
||||
result.message = absl::StrFormat("Called $%06X from $%06X",
|
||||
call_made->target_address,
|
||||
call_made->call_address);
|
||||
} else if (return_made) {
|
||||
result.message = absl::StrFormat("Returned to $%06X", pc_after);
|
||||
} else {
|
||||
result.message = absl::StrFormat("Stepped to $%06X", pc_after);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
StepResult StepController::StepOver(uint32_t max_instructions) {
|
||||
StepResult result;
|
||||
result.success = false;
|
||||
result.instructions_executed = 0;
|
||||
|
||||
if (!step_ || !get_pc_ || !read_byte_) {
|
||||
result.message = "Step controller not properly configured";
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t pc = get_pc_();
|
||||
uint8_t opcode = read_byte_(pc);
|
||||
|
||||
// If not a call instruction, just do a single step
|
||||
if (!IsCallInstruction(opcode)) {
|
||||
return StepInto();
|
||||
}
|
||||
|
||||
// It's a call instruction - execute until we return
|
||||
size_t initial_depth = call_stack_.size();
|
||||
uint32_t return_address = CalculateReturnAddress(pc, opcode);
|
||||
|
||||
// Execute the call
|
||||
auto step_result = StepInto();
|
||||
result.instructions_executed = step_result.instructions_executed;
|
||||
result.call = step_result.call;
|
||||
|
||||
if (!step_result.success) {
|
||||
return step_result;
|
||||
}
|
||||
|
||||
// Now run until we return to the expected depth
|
||||
while (result.instructions_executed < max_instructions) {
|
||||
pc = get_pc_();
|
||||
|
||||
// Check if we've returned to our expected depth
|
||||
if (call_stack_.size() <= initial_depth) {
|
||||
result.success = true;
|
||||
result.new_pc = pc;
|
||||
result.message = absl::StrFormat(
|
||||
"Stepped over subroutine, returned to $%06X after %u instructions",
|
||||
pc, result.instructions_executed);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if we hit a breakpoint or error condition
|
||||
uint8_t current_opcode = read_byte_(pc);
|
||||
|
||||
// Step one instruction
|
||||
step_();
|
||||
result.instructions_executed++;
|
||||
|
||||
// Update call stack based on instruction
|
||||
if (IsCallInstruction(current_opcode)) {
|
||||
uint32_t target = CalculateCallTarget(pc, current_opcode);
|
||||
uint32_t ret = CalculateReturnAddress(pc, current_opcode);
|
||||
bool is_long = (current_opcode == opcode::JSL);
|
||||
call_stack_.emplace_back(pc, target, ret, is_long);
|
||||
} else if (IsReturnInstruction(current_opcode) && !call_stack_.empty()) {
|
||||
call_stack_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout
|
||||
result.success = false;
|
||||
result.new_pc = get_pc_();
|
||||
result.message = absl::StrFormat(
|
||||
"Step over timed out after %u instructions", max_instructions);
|
||||
return result;
|
||||
}
|
||||
|
||||
StepResult StepController::StepOut(uint32_t max_instructions) {
|
||||
StepResult result;
|
||||
result.success = false;
|
||||
result.instructions_executed = 0;
|
||||
|
||||
if (!step_ || !get_pc_ || !read_byte_) {
|
||||
result.message = "Step controller not properly configured";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (call_stack_.empty()) {
|
||||
result.message = "Cannot step out - call stack is empty";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Target depth is one less than current
|
||||
size_t target_depth = call_stack_.size() - 1;
|
||||
|
||||
// Run until we return to the target depth
|
||||
while (result.instructions_executed < max_instructions) {
|
||||
uint32_t pc = get_pc_();
|
||||
uint8_t opcode = read_byte_(pc);
|
||||
|
||||
// Step one instruction
|
||||
step_();
|
||||
result.instructions_executed++;
|
||||
|
||||
// Update call stack based on instruction
|
||||
if (IsCallInstruction(opcode)) {
|
||||
uint32_t target = CalculateCallTarget(pc, opcode);
|
||||
uint32_t ret = CalculateReturnAddress(pc, opcode);
|
||||
bool is_long = (opcode == opcode::JSL);
|
||||
call_stack_.emplace_back(pc, target, ret, is_long);
|
||||
} else if (IsReturnInstruction(opcode) && !call_stack_.empty()) {
|
||||
CallStackEntry returned = call_stack_.back();
|
||||
call_stack_.pop_back();
|
||||
result.ret = returned;
|
||||
|
||||
// Check if we've returned to target depth
|
||||
if (call_stack_.size() <= target_depth) {
|
||||
result.success = true;
|
||||
result.new_pc = get_pc_();
|
||||
result.message = absl::StrFormat(
|
||||
"Stepped out to $%06X after %u instructions",
|
||||
result.new_pc, result.instructions_executed);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout
|
||||
result.success = false;
|
||||
result.new_pc = get_pc_();
|
||||
result.message = absl::StrFormat(
|
||||
"Step out timed out after %u instructions", max_instructions);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
200
src/app/emu/debug/step_controller.h
Normal file
200
src/app/emu/debug/step_controller.h
Normal file
@@ -0,0 +1,200 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_STEP_CONTROLLER_H_
|
||||
#define YAZE_APP_EMU_DEBUG_STEP_CONTROLLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
/**
|
||||
* @brief Tracks call stack for intelligent stepping
|
||||
*
|
||||
* The 65816 uses these instructions for subroutine calls:
|
||||
* - JSR (opcode 0x20): Jump to Subroutine (16-bit address, pushes PC+2)
|
||||
* - JSL (opcode 0x22): Jump to Subroutine Long (24-bit address, pushes PB + PC+3)
|
||||
*
|
||||
* And these for returns:
|
||||
* - RTS (opcode 0x60): Return from Subroutine (pulls PC)
|
||||
* - RTL (opcode 0x6B): Return from Subroutine Long (pulls PB + PC)
|
||||
* - RTI (opcode 0x40): Return from Interrupt (pulls status, PC, PB)
|
||||
*/
|
||||
struct CallStackEntry {
|
||||
uint32_t call_address; // Address where the call was made
|
||||
uint32_t target_address; // Target of the call (subroutine start)
|
||||
uint32_t return_address; // Expected return address
|
||||
bool is_long; // True for JSL, false for JSR
|
||||
std::string symbol; // Symbol name at target (if available)
|
||||
|
||||
CallStackEntry(uint32_t call, uint32_t target, uint32_t ret, bool long_call)
|
||||
: call_address(call),
|
||||
target_address(target),
|
||||
return_address(ret),
|
||||
is_long(long_call) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Result of a step operation
|
||||
*/
|
||||
struct StepResult {
|
||||
bool success;
|
||||
uint32_t new_pc; // New program counter
|
||||
uint32_t instructions_executed; // Number of instructions stepped
|
||||
std::string message;
|
||||
std::optional<CallStackEntry> call; // If a call was made
|
||||
std::optional<CallStackEntry> ret; // If a return was made
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Controller for intelligent step operations
|
||||
*
|
||||
* Provides step-over, step-out, and step-into functionality by tracking
|
||||
* the call stack during execution.
|
||||
*
|
||||
* Usage:
|
||||
* StepController controller;
|
||||
* controller.SetMemoryReader([&](uint32_t addr) { return mem.ReadByte(addr); });
|
||||
* controller.SetSingleStepper([&]() { cpu.ExecuteInstruction(); });
|
||||
*
|
||||
* // Step over a JSR - will run until it returns
|
||||
* auto result = controller.StepOver(current_pc);
|
||||
*
|
||||
* // Step out of current subroutine
|
||||
* auto result = controller.StepOut(current_pc, call_depth);
|
||||
*/
|
||||
class StepController {
|
||||
public:
|
||||
using MemoryReader = std::function<uint8_t(uint32_t)>;
|
||||
using SingleStepper = std::function<void()>;
|
||||
using PcGetter = std::function<uint32_t()>;
|
||||
|
||||
StepController() = default;
|
||||
|
||||
void SetMemoryReader(MemoryReader reader) { read_byte_ = reader; }
|
||||
void SetSingleStepper(SingleStepper stepper) { step_ = stepper; }
|
||||
void SetPcGetter(PcGetter getter) { get_pc_ = getter; }
|
||||
|
||||
/**
|
||||
* @brief Step a single instruction and update call stack
|
||||
* @return Step result with call stack info
|
||||
*/
|
||||
StepResult StepInto();
|
||||
|
||||
/**
|
||||
* @brief Step over the current instruction
|
||||
*
|
||||
* If the current instruction is JSR/JSL, this executes until
|
||||
* the subroutine returns. Otherwise, it's equivalent to StepInto.
|
||||
*
|
||||
* @param max_instructions Maximum instructions before timeout
|
||||
* @return Step result
|
||||
*/
|
||||
StepResult StepOver(uint32_t max_instructions = 1000000);
|
||||
|
||||
/**
|
||||
* @brief Step out of the current subroutine
|
||||
*
|
||||
* Executes until RTS/RTL returns to a higher call level.
|
||||
*
|
||||
* @param max_instructions Maximum instructions before timeout
|
||||
* @return Step result
|
||||
*/
|
||||
StepResult StepOut(uint32_t max_instructions = 1000000);
|
||||
|
||||
/**
|
||||
* @brief Get the current call stack
|
||||
*/
|
||||
const std::vector<CallStackEntry>& GetCallStack() const {
|
||||
return call_stack_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the current call depth
|
||||
*/
|
||||
size_t GetCallDepth() const { return call_stack_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Clear the call stack (e.g., on reset)
|
||||
*/
|
||||
void ClearCallStack() { call_stack_.clear(); }
|
||||
|
||||
/**
|
||||
* @brief Check if an opcode is a call instruction (JSR/JSL)
|
||||
*/
|
||||
static bool IsCallInstruction(uint8_t opcode);
|
||||
|
||||
/**
|
||||
* @brief Check if an opcode is a return instruction (RTS/RTL/RTI)
|
||||
*/
|
||||
static bool IsReturnInstruction(uint8_t opcode);
|
||||
|
||||
/**
|
||||
* @brief Check if an opcode is a branch instruction
|
||||
*/
|
||||
static bool IsBranchInstruction(uint8_t opcode);
|
||||
|
||||
/**
|
||||
* @brief Get instruction size for step over calculations
|
||||
*/
|
||||
static uint8_t GetInstructionSize(uint8_t opcode, bool m_flag, bool x_flag);
|
||||
|
||||
private:
|
||||
// Process instruction and update call stack
|
||||
void ProcessInstruction(uint32_t pc);
|
||||
|
||||
// Calculate return address for call
|
||||
uint32_t CalculateReturnAddress(uint32_t pc, uint8_t opcode) const;
|
||||
|
||||
// Calculate target address for call
|
||||
uint32_t CalculateCallTarget(uint32_t pc, uint8_t opcode) const;
|
||||
|
||||
MemoryReader read_byte_;
|
||||
SingleStepper step_;
|
||||
PcGetter get_pc_;
|
||||
std::vector<CallStackEntry> call_stack_;
|
||||
};
|
||||
|
||||
// Static helper functions for opcode classification
|
||||
namespace opcode {
|
||||
|
||||
// Call instructions
|
||||
constexpr uint8_t JSR = 0x20; // Jump to Subroutine (absolute)
|
||||
constexpr uint8_t JSL = 0x22; // Jump to Subroutine Long
|
||||
constexpr uint8_t JSR_X = 0xFC; // Jump to Subroutine (absolute,X)
|
||||
|
||||
// Return instructions
|
||||
constexpr uint8_t RTS = 0x60; // Return from Subroutine
|
||||
constexpr uint8_t RTL = 0x6B; // Return from Subroutine Long
|
||||
constexpr uint8_t RTI = 0x40; // Return from Interrupt
|
||||
|
||||
// Branch instructions (conditional)
|
||||
constexpr uint8_t BCC = 0x90; // Branch if Carry Clear
|
||||
constexpr uint8_t BCS = 0xB0; // Branch if Carry Set
|
||||
constexpr uint8_t BEQ = 0xF0; // Branch if Equal (Z=1)
|
||||
constexpr uint8_t BMI = 0x30; // Branch if Minus (N=1)
|
||||
constexpr uint8_t BNE = 0xD0; // Branch if Not Equal (Z=0)
|
||||
constexpr uint8_t BPL = 0x10; // Branch if Plus (N=0)
|
||||
constexpr uint8_t BVC = 0x50; // Branch if Overflow Clear
|
||||
constexpr uint8_t BVS = 0x70; // Branch if Overflow Set
|
||||
constexpr uint8_t BRA = 0x80; // Branch Always (relative)
|
||||
constexpr uint8_t BRL = 0x82; // Branch Long (relative long)
|
||||
|
||||
// Jump instructions
|
||||
constexpr uint8_t JMP_ABS = 0x4C; // Jump Absolute
|
||||
constexpr uint8_t JMP_IND = 0x6C; // Jump Indirect
|
||||
constexpr uint8_t JMP_ABS_X = 0x7C; // Jump Absolute Indexed Indirect
|
||||
constexpr uint8_t JMP_LONG = 0x5C; // Jump Long
|
||||
constexpr uint8_t JMP_IND_L = 0xDC; // Jump Indirect Long
|
||||
|
||||
} // namespace opcode
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_DEBUG_STEP_CONTROLLER_H_
|
||||
489
src/app/emu/debug/symbol_provider.cc
Normal file
489
src/app/emu/debug/symbol_provider.cc
Normal file
@@ -0,0 +1,489 @@
|
||||
#include "app/emu/debug/symbol_provider.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "absl/strings/strip.h"
|
||||
#include "absl/strings/match.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper to read entire file into string
|
||||
absl::StatusOr<std::string> ReadFileContent(const std::string& path) {
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Failed to open file: %s", path));
|
||||
}
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
// Parse 24-bit hex address from string (e.g., "008034" or "$008034")
|
||||
std::optional<uint32_t> ParseAddress(const std::string& str) {
|
||||
std::string clean = str;
|
||||
// Remove $ prefix if present
|
||||
if (!clean.empty() && clean[0] == '$') {
|
||||
clean = clean.substr(1);
|
||||
}
|
||||
// Remove 0x prefix if present
|
||||
if (clean.size() >= 2 && clean[0] == '0' &&
|
||||
(clean[1] == 'x' || clean[1] == 'X')) {
|
||||
clean = clean.substr(2);
|
||||
}
|
||||
// Remove any trailing colon
|
||||
if (!clean.empty() && clean.back() == ':') {
|
||||
clean.pop_back();
|
||||
}
|
||||
|
||||
if (clean.empty() || clean.size() > 6) return std::nullopt;
|
||||
|
||||
try {
|
||||
size_t pos;
|
||||
uint32_t addr = std::stoul(clean, &pos, 16);
|
||||
if (pos != clean.size()) return std::nullopt;
|
||||
return addr;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a string is a valid label name
|
||||
bool IsValidLabelName(const std::string& name) {
|
||||
if (name.empty()) return false;
|
||||
// First char must be alpha, underscore, or dot (for local labels)
|
||||
char first = name[0];
|
||||
if (!std::isalpha(first) && first != '_' && first != '.') return false;
|
||||
// Rest must be alphanumeric, underscore, or dot
|
||||
for (size_t i = 1; i < name.size(); ++i) {
|
||||
char c = name[i];
|
||||
if (!std::isalnum(c) && c != '_' && c != '.') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Simple wildcard matching (supports * only)
|
||||
bool WildcardMatch(const std::string& pattern, const std::string& str) {
|
||||
size_t p = 0, s = 0;
|
||||
size_t starPos = std::string::npos;
|
||||
size_t matchPos = 0;
|
||||
|
||||
while (s < str.size()) {
|
||||
if (p < pattern.size() && (pattern[p] == str[s] || pattern[p] == '?')) {
|
||||
++p;
|
||||
++s;
|
||||
} else if (p < pattern.size() && pattern[p] == '*') {
|
||||
starPos = p++;
|
||||
matchPos = s;
|
||||
} else if (starPos != std::string::npos) {
|
||||
p = starPos + 1;
|
||||
s = ++matchPos;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (p < pattern.size() && pattern[p] == '*') ++p;
|
||||
return p == pattern.size();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::Status SymbolProvider::LoadAsarAsmFile(const std::string& path) {
|
||||
auto content_or = ReadFileContent(path);
|
||||
if (!content_or.ok()) {
|
||||
return content_or.status();
|
||||
}
|
||||
|
||||
std::filesystem::path file_path(path);
|
||||
return ParseAsarAsmContent(*content_or, file_path.filename().string());
|
||||
}
|
||||
|
||||
absl::Status SymbolProvider::LoadAsarAsmDirectory(
|
||||
const std::string& directory_path) {
|
||||
std::filesystem::path dir(directory_path);
|
||||
if (!std::filesystem::exists(dir)) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Directory not found: %s", directory_path));
|
||||
}
|
||||
|
||||
int files_loaded = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
|
||||
if (entry.is_regular_file()) {
|
||||
auto ext = entry.path().extension().string();
|
||||
if (ext == ".asm" || ext == ".s") {
|
||||
auto status = LoadAsarAsmFile(entry.path().string());
|
||||
if (status.ok()) {
|
||||
++files_loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files_loaded == 0) {
|
||||
return absl::NotFoundError("No ASM files found in directory");
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SymbolProvider::LoadSymbolFile(const std::string& path,
|
||||
SymbolFormat format) {
|
||||
auto content_or = ReadFileContent(path);
|
||||
if (!content_or.ok()) {
|
||||
return content_or.status();
|
||||
}
|
||||
|
||||
const std::string& content = *content_or;
|
||||
std::filesystem::path file_path(path);
|
||||
std::string ext = file_path.extension().string();
|
||||
|
||||
// Auto-detect format if needed
|
||||
if (format == SymbolFormat::kAuto) {
|
||||
format = DetectFormat(content, ext);
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case SymbolFormat::kAsar:
|
||||
return ParseAsarAsmContent(content, file_path.filename().string());
|
||||
case SymbolFormat::kWlaDx:
|
||||
return ParseWlaDxSymFile(content);
|
||||
case SymbolFormat::kMesen:
|
||||
return ParseMesenMlbFile(content);
|
||||
case SymbolFormat::kBsnes:
|
||||
case SymbolFormat::kNo$snes:
|
||||
return ParseBsnesSymFile(content);
|
||||
default:
|
||||
return absl::InvalidArgumentError("Unknown symbol format");
|
||||
}
|
||||
}
|
||||
|
||||
void SymbolProvider::AddSymbol(const Symbol& symbol) {
|
||||
symbols_by_address_.emplace(symbol.address, symbol);
|
||||
symbols_by_name_[symbol.name] = symbol;
|
||||
}
|
||||
|
||||
void SymbolProvider::AddAsarSymbols(const std::vector<Symbol>& symbols) {
|
||||
for (const auto& sym : symbols) {
|
||||
AddSymbol(sym);
|
||||
}
|
||||
}
|
||||
|
||||
void SymbolProvider::Clear() {
|
||||
symbols_by_address_.clear();
|
||||
symbols_by_name_.clear();
|
||||
}
|
||||
|
||||
std::string SymbolProvider::GetSymbolName(uint32_t address) const {
|
||||
auto it = symbols_by_address_.find(address);
|
||||
if (it != symbols_by_address_.end()) {
|
||||
return it->second.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::optional<Symbol> SymbolProvider::GetSymbol(uint32_t address) const {
|
||||
auto it = symbols_by_address_.find(address);
|
||||
if (it != symbols_by_address_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<Symbol> SymbolProvider::GetSymbolsAtAddress(
|
||||
uint32_t address) const {
|
||||
std::vector<Symbol> result;
|
||||
auto range = symbols_by_address_.equal_range(address);
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
result.push_back(it->second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<Symbol> SymbolProvider::FindSymbol(
|
||||
const std::string& name) const {
|
||||
auto it = symbols_by_name_.find(name);
|
||||
if (it != symbols_by_name_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<Symbol> SymbolProvider::FindSymbolsMatching(
|
||||
const std::string& pattern) const {
|
||||
std::vector<Symbol> result;
|
||||
for (const auto& [name, sym] : symbols_by_name_) {
|
||||
if (WildcardMatch(pattern, name)) {
|
||||
result.push_back(sym);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Symbol> SymbolProvider::GetSymbolsInRange(uint32_t start,
|
||||
uint32_t end) const {
|
||||
std::vector<Symbol> result;
|
||||
auto it_start = symbols_by_address_.lower_bound(start);
|
||||
auto it_end = symbols_by_address_.upper_bound(end);
|
||||
for (auto it = it_start; it != it_end; ++it) {
|
||||
result.push_back(it->second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<Symbol> SymbolProvider::GetNearestSymbol(
|
||||
uint32_t address) const {
|
||||
if (symbols_by_address_.empty()) return std::nullopt;
|
||||
|
||||
// Find first symbol > address
|
||||
auto it = symbols_by_address_.upper_bound(address);
|
||||
|
||||
if (it == symbols_by_address_.begin()) {
|
||||
// All symbols are > address, no symbol at or before
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Go back to the symbol at or before address
|
||||
--it;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::string SymbolProvider::FormatAddress(uint32_t address,
|
||||
uint32_t max_offset) const {
|
||||
// Check for exact match first
|
||||
auto exact = GetSymbol(address);
|
||||
if (exact) {
|
||||
return exact->name;
|
||||
}
|
||||
|
||||
// Check for nearest symbol with offset
|
||||
auto nearest = GetNearestSymbol(address);
|
||||
if (nearest) {
|
||||
uint32_t offset = address - nearest->address;
|
||||
if (offset <= max_offset) {
|
||||
return absl::StrFormat("%s+$%X", nearest->name, offset);
|
||||
}
|
||||
}
|
||||
|
||||
// No symbol found, just format as hex
|
||||
return absl::StrFormat("$%06X", address);
|
||||
}
|
||||
|
||||
std::function<std::string(uint32_t)> SymbolProvider::CreateResolver() const {
|
||||
return [this](uint32_t address) -> std::string {
|
||||
return GetSymbolName(address);
|
||||
};
|
||||
}
|
||||
|
||||
absl::Status SymbolProvider::ParseAsarAsmContent(const std::string& content,
|
||||
const std::string& filename) {
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
int line_number = 0;
|
||||
|
||||
std::string current_label; // Current global label (for local label scope)
|
||||
uint32_t last_address = 0;
|
||||
|
||||
// Regex patterns for usdasm format
|
||||
// Label definition: word followed by colon at start of line
|
||||
std::regex label_regex(R"(^([A-Za-z_][A-Za-z0-9_]*):)");
|
||||
// Local label: dot followed by word and colon
|
||||
std::regex local_label_regex(R"(^(\.[A-Za-z_][A-Za-z0-9_]*))");
|
||||
// Address line: #_XXXXXX: instruction
|
||||
std::regex address_regex(R"(^#_([0-9A-Fa-f]{6}):)");
|
||||
|
||||
bool pending_label = false;
|
||||
std::string pending_label_name;
|
||||
bool pending_is_local = false;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
++line_number;
|
||||
|
||||
// Skip empty lines and comment-only lines
|
||||
std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
|
||||
if (trimmed.empty() || trimmed[0] == ';') continue;
|
||||
|
||||
std::smatch match;
|
||||
|
||||
// Check for address line
|
||||
if (std::regex_search(line, match, address_regex)) {
|
||||
auto addr = ParseAddress(match[1].str());
|
||||
if (addr) {
|
||||
last_address = *addr;
|
||||
|
||||
// If we have a pending label, associate it with this address
|
||||
if (pending_label) {
|
||||
Symbol sym;
|
||||
sym.name = pending_label_name;
|
||||
sym.address = *addr;
|
||||
sym.file = filename;
|
||||
sym.line = line_number;
|
||||
sym.is_local = pending_is_local;
|
||||
|
||||
AddSymbol(sym);
|
||||
pending_label = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for global label (at start of line, not indented)
|
||||
if (line[0] != ' ' && line[0] != '\t' && line[0] != '#') {
|
||||
if (std::regex_search(line, match, label_regex)) {
|
||||
current_label = match[1].str();
|
||||
pending_label = true;
|
||||
pending_label_name = current_label;
|
||||
pending_is_local = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for local label
|
||||
if (std::regex_search(trimmed, match, local_label_regex)) {
|
||||
std::string local_name = match[1].str();
|
||||
// Create fully qualified name: GlobalLabel.local_name
|
||||
std::string full_name = current_label.empty()
|
||||
? local_name
|
||||
: current_label + local_name;
|
||||
pending_label = true;
|
||||
pending_label_name = full_name;
|
||||
pending_is_local = true;
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SymbolProvider::ParseWlaDxSymFile(const std::string& content) {
|
||||
// WLA-DX format:
|
||||
// [labels]
|
||||
// 00:8000 Reset
|
||||
// 00:8034 MainGameLoop
|
||||
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
bool in_labels_section = false;
|
||||
|
||||
std::regex label_regex(R"(^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{4})\s+(\S+))");
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
|
||||
|
||||
if (trimmed == "[labels]") {
|
||||
in_labels_section = true;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.empty() || trimmed[0] == '[') {
|
||||
if (trimmed[0] == '[') in_labels_section = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_labels_section) continue;
|
||||
|
||||
std::smatch match;
|
||||
if (std::regex_search(trimmed, match, label_regex)) {
|
||||
uint32_t bank = std::stoul(match[1].str(), nullptr, 16);
|
||||
uint32_t offset = std::stoul(match[2].str(), nullptr, 16);
|
||||
uint32_t address = (bank << 16) | offset;
|
||||
std::string name = match[3].str();
|
||||
|
||||
Symbol sym(name, address);
|
||||
AddSymbol(sym);
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SymbolProvider::ParseMesenMlbFile(const std::string& content) {
|
||||
// Mesen .mlb format:
|
||||
// PRG:address:name
|
||||
// or just
|
||||
// address:name
|
||||
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
|
||||
std::regex label_regex(R"(^(?:PRG:)?([0-9A-Fa-f]+):(\S+))");
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
|
||||
if (trimmed.empty() || trimmed[0] == ';') continue;
|
||||
|
||||
std::smatch match;
|
||||
if (std::regex_search(trimmed, match, label_regex)) {
|
||||
auto addr = ParseAddress(match[1].str());
|
||||
if (addr) {
|
||||
Symbol sym(match[2].str(), *addr);
|
||||
AddSymbol(sym);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SymbolProvider::ParseBsnesSymFile(const std::string& content) {
|
||||
// bsnes/No$snes format:
|
||||
// 008000 Reset
|
||||
// 008034 MainGameLoop
|
||||
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
|
||||
std::regex label_regex(R"(^([0-9A-Fa-f]{6})\s+(\S+))");
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
|
||||
if (trimmed.empty() || trimmed[0] == ';' || trimmed[0] == '#') continue;
|
||||
|
||||
std::smatch match;
|
||||
if (std::regex_search(trimmed, match, label_regex)) {
|
||||
auto addr = ParseAddress(match[1].str());
|
||||
if (addr) {
|
||||
Symbol sym(match[2].str(), *addr);
|
||||
AddSymbol(sym);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
SymbolFormat SymbolProvider::DetectFormat(const std::string& content,
|
||||
const std::string& extension) const {
|
||||
// Check extension first
|
||||
if (extension == ".asm" || extension == ".s") {
|
||||
return SymbolFormat::kAsar;
|
||||
}
|
||||
if (extension == ".mlb") {
|
||||
return SymbolFormat::kMesen;
|
||||
}
|
||||
|
||||
// Check content for format hints
|
||||
if (content.find("[labels]") != std::string::npos) {
|
||||
return SymbolFormat::kWlaDx;
|
||||
}
|
||||
if (content.find("PRG:") != std::string::npos) {
|
||||
return SymbolFormat::kMesen;
|
||||
}
|
||||
if (content.find("#_") != std::string::npos) {
|
||||
return SymbolFormat::kAsar;
|
||||
}
|
||||
|
||||
// Default to bsnes format (most generic)
|
||||
return SymbolFormat::kBsnes;
|
||||
}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
208
src/app/emu/debug/symbol_provider.h
Normal file
208
src/app/emu/debug/symbol_provider.h
Normal file
@@ -0,0 +1,208 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_SYMBOL_PROVIDER_H_
|
||||
#define YAZE_APP_EMU_DEBUG_SYMBOL_PROVIDER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
/**
|
||||
* @brief Information about a symbol (label, constant, or address)
|
||||
*/
|
||||
struct Symbol {
|
||||
std::string name; // Symbol name (e.g., "MainGameLoop", "Reset")
|
||||
uint32_t address; // 24-bit SNES address
|
||||
std::string file; // Source file (if known)
|
||||
int line = 0; // Line number (if known)
|
||||
std::string comment; // Optional comment or description
|
||||
bool is_local = false; // True for local labels (starting with .)
|
||||
|
||||
Symbol() = default;
|
||||
Symbol(const std::string& n, uint32_t addr)
|
||||
: name(n), address(addr) {}
|
||||
Symbol(const std::string& n, uint32_t addr, const std::string& f, int l)
|
||||
: name(n), address(addr), file(f), line(l) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Supported symbol file formats
|
||||
*/
|
||||
enum class SymbolFormat {
|
||||
kAuto, // Auto-detect based on file extension/content
|
||||
kAsar, // Asar-style .asm/.sym files (label: at address #_XXXXXX:)
|
||||
kWlaDx, // WLA-DX .sym format (bank:address name)
|
||||
kMesen, // Mesen .mlb format (address:name)
|
||||
kBsnes, // bsnes .sym format (address name)
|
||||
kNo$snes, // No$snes .sym format (bank:addr name)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Provider for symbol (label) resolution in disassembly
|
||||
*
|
||||
* This class manages symbol tables from multiple sources:
|
||||
* - Parsed ASM files (usdasm disassembly)
|
||||
* - Symbol files from various emulators/assemblers
|
||||
* - Asar patches (runtime symbols)
|
||||
*
|
||||
* AI agents use this to see meaningful label names instead of raw addresses
|
||||
* when debugging 65816 assembly code.
|
||||
*
|
||||
* Usage:
|
||||
* SymbolProvider symbols;
|
||||
* symbols.LoadAsarAsmFile("bank_00.asm");
|
||||
* symbols.LoadAsarAsmFile("bank_01.asm");
|
||||
*
|
||||
* auto name = symbols.GetSymbolName(0x008034); // Returns "MainGameLoop"
|
||||
* auto addr = symbols.FindSymbol("Reset"); // Returns Symbol at $008000
|
||||
*/
|
||||
class SymbolProvider {
|
||||
public:
|
||||
SymbolProvider() = default;
|
||||
|
||||
/**
|
||||
* @brief Load symbols from an Asar-style ASM file (usdasm format)
|
||||
*
|
||||
* Parses labels like:
|
||||
* MainGameLoop:
|
||||
* #_008034: LDA.b $12
|
||||
*
|
||||
* @param path Path to .asm file
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status LoadAsarAsmFile(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief Load symbols from a directory of ASM files
|
||||
*
|
||||
* Scans for all bank_XX.asm files and loads them
|
||||
*
|
||||
* @param directory_path Path to directory containing ASM files
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status LoadAsarAsmDirectory(const std::string& directory_path);
|
||||
|
||||
/**
|
||||
* @brief Load symbols from a .sym file (various formats)
|
||||
*
|
||||
* @param path Path to symbol file
|
||||
* @param format Symbol file format (kAuto for auto-detect)
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status LoadSymbolFile(const std::string& path,
|
||||
SymbolFormat format = SymbolFormat::kAuto);
|
||||
|
||||
/**
|
||||
* @brief Add a single symbol manually
|
||||
*/
|
||||
void AddSymbol(const Symbol& symbol);
|
||||
|
||||
/**
|
||||
* @brief Add symbols from Asar patch results
|
||||
*/
|
||||
void AddAsarSymbols(const std::vector<Symbol>& symbols);
|
||||
|
||||
/**
|
||||
* @brief Clear all loaded symbols
|
||||
*/
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* @brief Get symbol name for an address
|
||||
* @return Symbol name if found, empty string otherwise
|
||||
*/
|
||||
std::string GetSymbolName(uint32_t address) const;
|
||||
|
||||
/**
|
||||
* @brief Get full symbol info for an address
|
||||
* @return Symbol if found, nullopt otherwise
|
||||
*/
|
||||
std::optional<Symbol> GetSymbol(uint32_t address) const;
|
||||
|
||||
/**
|
||||
* @brief Get all symbols at an address (there may be multiple)
|
||||
*/
|
||||
std::vector<Symbol> GetSymbolsAtAddress(uint32_t address) const;
|
||||
|
||||
/**
|
||||
* @brief Find symbol by name
|
||||
* @return Symbol if found, nullopt otherwise
|
||||
*/
|
||||
std::optional<Symbol> FindSymbol(const std::string& name) const;
|
||||
|
||||
/**
|
||||
* @brief Find symbols matching a pattern (supports wildcards)
|
||||
* @param pattern Pattern with * as wildcard (e.g., "Module*", "*_Init")
|
||||
* @return Matching symbols
|
||||
*/
|
||||
std::vector<Symbol> FindSymbolsMatching(const std::string& pattern) const;
|
||||
|
||||
/**
|
||||
* @brief Get all symbols in an address range
|
||||
*/
|
||||
std::vector<Symbol> GetSymbolsInRange(uint32_t start, uint32_t end) const;
|
||||
|
||||
/**
|
||||
* @brief Get nearest symbol at or before an address
|
||||
*
|
||||
* Useful for showing "MainGameLoop+$10" style offsets
|
||||
*/
|
||||
std::optional<Symbol> GetNearestSymbol(uint32_t address) const;
|
||||
|
||||
/**
|
||||
* @brief Format an address with symbol info
|
||||
*
|
||||
* Returns formats like:
|
||||
* "MainGameLoop" (exact match)
|
||||
* "MainGameLoop+$10" (offset from nearest symbol)
|
||||
* "$00804D" (no nearby symbol)
|
||||
*/
|
||||
std::string FormatAddress(uint32_t address,
|
||||
uint32_t max_offset = 0x100) const;
|
||||
|
||||
/**
|
||||
* @brief Get total number of loaded symbols
|
||||
*/
|
||||
size_t GetSymbolCount() const { return symbols_by_address_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Check if any symbols are loaded
|
||||
*/
|
||||
bool HasSymbols() const { return !symbols_by_address_.empty(); }
|
||||
|
||||
/**
|
||||
* @brief Create a symbol resolver function for the disassembler
|
||||
*/
|
||||
std::function<std::string(uint32_t)> CreateResolver() const;
|
||||
|
||||
private:
|
||||
// Parse different symbol file formats
|
||||
absl::Status ParseAsarAsmContent(const std::string& content,
|
||||
const std::string& filename);
|
||||
absl::Status ParseWlaDxSymFile(const std::string& content);
|
||||
absl::Status ParseMesenMlbFile(const std::string& content);
|
||||
absl::Status ParseBsnesSymFile(const std::string& content);
|
||||
|
||||
// Detect format from file content
|
||||
SymbolFormat DetectFormat(const std::string& content,
|
||||
const std::string& extension) const;
|
||||
|
||||
// Primary storage: address -> symbols (may have multiple per address)
|
||||
std::multimap<uint32_t, Symbol> symbols_by_address_;
|
||||
|
||||
// Secondary index: name -> symbol (for reverse lookup)
|
||||
std::map<std::string, Symbol> symbols_by_name_;
|
||||
};
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_DEBUG_SYMBOL_PROVIDER_H_
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "app/platform/app_delegate.h"
|
||||
#endif
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "absl/flags/parse.h"
|
||||
#include "app/emu/snes.h"
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
#include "app/gfx/backend/sdl2_renderer.h"
|
||||
#include "app/gfx/backend/renderer_factory.h"
|
||||
#include "app/rom.h"
|
||||
#include "util/sdl_deleter.h"
|
||||
|
||||
@@ -89,8 +89,8 @@ int main(int argc, char** argv) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Create and initialize the renderer
|
||||
auto renderer = std::make_unique<yaze::gfx::SDL2Renderer>();
|
||||
// Create and initialize the renderer (uses factory for SDL2/SDL3 selection)
|
||||
auto renderer = yaze::gfx::RendererFactory::Create();
|
||||
if (!renderer->Initialize(window_.get())) {
|
||||
printf("Failed to initialize renderer\n");
|
||||
SDL_Quit();
|
||||
|
||||
@@ -55,6 +55,8 @@ if(YAZE_BUILD_EMU AND NOT YAZE_MINIMAL_BUILD)
|
||||
)
|
||||
target_link_libraries(yaze_emu_test PRIVATE
|
||||
yaze_emulator
|
||||
yaze_editor
|
||||
yaze_gui
|
||||
yaze_util
|
||||
absl::flags
|
||||
absl::flags_parse
|
||||
|
||||
@@ -26,12 +26,19 @@ target_include_directories(yaze_emulator PUBLIC
|
||||
${PROJECT_BINARY_DIR}
|
||||
)
|
||||
|
||||
# Link to SDL (version-dependent)
|
||||
if(YAZE_USE_SDL3)
|
||||
set(SDL_TARGETS ${YAZE_SDL3_TARGETS})
|
||||
else()
|
||||
set(SDL_TARGETS ${YAZE_SDL2_TARGETS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(yaze_emulator PUBLIC
|
||||
yaze_util
|
||||
yaze_common
|
||||
yaze_app_core_lib
|
||||
${ABSL_TARGETS}
|
||||
${YAZE_SDL2_TARGETS}
|
||||
${SDL_TARGETS}
|
||||
)
|
||||
|
||||
set_target_properties(yaze_emulator PROPERTIES
|
||||
@@ -49,4 +56,11 @@ elseif(WIN32)
|
||||
target_compile_definitions(yaze_emulator PRIVATE WINDOWS)
|
||||
endif()
|
||||
|
||||
# SDL version compile definitions
|
||||
if(YAZE_USE_SDL3)
|
||||
target_compile_definitions(yaze_emulator PRIVATE YAZE_USE_SDL3=1 YAZE_SDL3=1)
|
||||
else()
|
||||
target_compile_definitions(yaze_emulator PRIVATE YAZE_SDL2=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "✓ yaze_emulator library configured")
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "app/emu/input/input_backend.h"
|
||||
|
||||
#include "SDL.h"
|
||||
#include "app/platform/sdl_compat.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "util/log.h"
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
#include "app/emu/input/sdl3_input_backend.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace input {
|
||||
@@ -204,19 +208,34 @@ class NullInputBackend : public IInputBackend {
|
||||
std::unique_ptr<IInputBackend> InputBackendFactory::Create(BackendType type) {
|
||||
switch (type) {
|
||||
case BackendType::SDL2:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
LOG_WARN("InputBackend",
|
||||
"SDL2 backend requested but SDL3 build enabled, using SDL3");
|
||||
return std::make_unique<SDL3InputBackend>();
|
||||
#else
|
||||
return std::make_unique<SDL2InputBackend>();
|
||||
#endif
|
||||
|
||||
case BackendType::SDL3:
|
||||
// TODO: Implement SDL3 backend when SDL3 is stable
|
||||
LOG_WARN("InputBackend", "SDL3 backend not yet implemented, using SDL2");
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return std::make_unique<SDL3InputBackend>();
|
||||
#else
|
||||
LOG_WARN("InputBackend",
|
||||
"SDL3 backend requested but not available, using SDL2");
|
||||
return std::make_unique<SDL2InputBackend>();
|
||||
#endif
|
||||
|
||||
case BackendType::NULL_BACKEND:
|
||||
return std::make_unique<NullInputBackend>();
|
||||
|
||||
default:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
LOG_ERROR("InputBackend", "Unknown backend type, using SDL3");
|
||||
return std::make_unique<SDL3InputBackend>();
|
||||
#else
|
||||
LOG_ERROR("InputBackend", "Unknown backend type, using SDL2");
|
||||
return std::make_unique<SDL2InputBackend>();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
346
src/app/emu/input/sdl3_input_backend.cc
Normal file
346
src/app/emu/input/sdl3_input_backend.cc
Normal file
@@ -0,0 +1,346 @@
|
||||
#include "app/emu/input/sdl3_input_backend.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace input {
|
||||
|
||||
SDL3InputBackend::SDL3InputBackend() = default;
|
||||
|
||||
SDL3InputBackend::~SDL3InputBackend() { Shutdown(); }
|
||||
|
||||
bool SDL3InputBackend::Initialize(const InputConfig& config) {
|
||||
if (initialized_) {
|
||||
LOG_WARN("InputBackend", "SDL3 backend already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
config_ = config;
|
||||
|
||||
// Set default SDL keycodes if not configured
|
||||
if (config_.key_a == 0) {
|
||||
config_.key_a = SDLK_x;
|
||||
config_.key_b = SDLK_z;
|
||||
config_.key_x = SDLK_s;
|
||||
config_.key_y = SDLK_a;
|
||||
config_.key_l = SDLK_d;
|
||||
config_.key_r = SDLK_c;
|
||||
config_.key_start = SDLK_RETURN;
|
||||
config_.key_select = SDLK_RSHIFT;
|
||||
config_.key_up = SDLK_UP;
|
||||
config_.key_down = SDLK_DOWN;
|
||||
config_.key_left = SDLK_LEFT;
|
||||
config_.key_right = SDLK_RIGHT;
|
||||
}
|
||||
|
||||
// Initialize gamepad if enabled
|
||||
if (config_.enable_gamepad) {
|
||||
gamepads_[0] = platform::OpenGamepad(config_.gamepad_index);
|
||||
if (gamepads_[0]) {
|
||||
LOG_INFO("InputBackend", "SDL3 Gamepad connected for player 1");
|
||||
}
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
LOG_INFO("InputBackend", "SDL3 Input Backend initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDL3InputBackend::Shutdown() {
|
||||
if (initialized_) {
|
||||
// Close all gamepads
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (gamepads_[i]) {
|
||||
platform::CloseGamepad(gamepads_[i]);
|
||||
gamepads_[i] = nullptr;
|
||||
}
|
||||
}
|
||||
initialized_ = false;
|
||||
LOG_INFO("InputBackend", "SDL3 Input Backend shut down");
|
||||
}
|
||||
}
|
||||
|
||||
ControllerState SDL3InputBackend::Poll(int player) {
|
||||
if (!initialized_) return ControllerState{};
|
||||
|
||||
ControllerState state;
|
||||
|
||||
if (config_.continuous_polling) {
|
||||
// Continuous polling mode (for games)
|
||||
// SDL3: SDL_GetKeyboardState returns const bool* instead of const Uint8*
|
||||
platform::KeyboardState keyboard_state = SDL_GetKeyboardState(nullptr);
|
||||
|
||||
// IMPORTANT: Only block input when actively typing in text fields
|
||||
// Allow game input even when ImGui windows are open/focused
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Only block if user is actively typing in a text input field
|
||||
// WantTextInput is true only when an InputText widget is active
|
||||
if (io.WantTextInput) {
|
||||
static int text_input_log_count = 0;
|
||||
if (text_input_log_count++ < 5) {
|
||||
LOG_DEBUG("InputBackend", "Blocking game input - WantTextInput=true");
|
||||
}
|
||||
return ControllerState{};
|
||||
}
|
||||
|
||||
// Map keyboard to SNES buttons using SDL3 API
|
||||
// Use platform::IsKeyPressed helper to handle bool* vs Uint8* difference
|
||||
state.SetButton(
|
||||
SnesButton::B,
|
||||
platform::IsKeyPressed(keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_b, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::Y,
|
||||
platform::IsKeyPressed(keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_y, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::SELECT,
|
||||
platform::IsKeyPressed(
|
||||
keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_select, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::START,
|
||||
platform::IsKeyPressed(
|
||||
keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_start, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::UP,
|
||||
platform::IsKeyPressed(
|
||||
keyboard_state, SDL_GetScancodeFromKey(config_.key_up, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::DOWN,
|
||||
platform::IsKeyPressed(
|
||||
keyboard_state, SDL_GetScancodeFromKey(config_.key_down, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::LEFT,
|
||||
platform::IsKeyPressed(
|
||||
keyboard_state, SDL_GetScancodeFromKey(config_.key_left, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::RIGHT,
|
||||
platform::IsKeyPressed(
|
||||
keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_right, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::A,
|
||||
platform::IsKeyPressed(keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_a, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::X,
|
||||
platform::IsKeyPressed(keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_x, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::L,
|
||||
platform::IsKeyPressed(keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_l, nullptr)));
|
||||
state.SetButton(
|
||||
SnesButton::R,
|
||||
platform::IsKeyPressed(keyboard_state,
|
||||
SDL_GetScancodeFromKey(config_.key_r, nullptr)));
|
||||
|
||||
// Poll gamepad if enabled
|
||||
if (config_.enable_gamepad) {
|
||||
PollGamepad(state, player);
|
||||
}
|
||||
} else {
|
||||
// Event-based mode (use cached event state)
|
||||
state = event_state_;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void SDL3InputBackend::PollGamepad(ControllerState& state, int player) {
|
||||
int gamepad_index = (player > 0 && player <= 4) ? player - 1 : 0;
|
||||
platform::GamepadHandle gamepad = gamepads_[gamepad_index];
|
||||
|
||||
if (!gamepad) return;
|
||||
|
||||
// Map gamepad buttons to SNES buttons using SDL3 Gamepad API
|
||||
// SDL3 uses SDL_GAMEPAD_BUTTON_* with directional naming (SOUTH, EAST, etc.)
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonA)) {
|
||||
state.SetButton(SnesButton::A, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonB)) {
|
||||
state.SetButton(SnesButton::B, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonX)) {
|
||||
state.SetButton(SnesButton::X, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonY)) {
|
||||
state.SetButton(SnesButton::Y, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonLeftShoulder)) {
|
||||
state.SetButton(SnesButton::L, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad,
|
||||
platform::kGamepadButtonRightShoulder)) {
|
||||
state.SetButton(SnesButton::R, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonStart)) {
|
||||
state.SetButton(SnesButton::START, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonBack)) {
|
||||
state.SetButton(SnesButton::SELECT, true);
|
||||
}
|
||||
|
||||
// D-pad buttons
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonDpadUp)) {
|
||||
state.SetButton(SnesButton::UP, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonDpadDown)) {
|
||||
state.SetButton(SnesButton::DOWN, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonDpadLeft)) {
|
||||
state.SetButton(SnesButton::LEFT, true);
|
||||
}
|
||||
if (platform::GetGamepadButton(gamepad, platform::kGamepadButtonDpadRight)) {
|
||||
state.SetButton(SnesButton::RIGHT, true);
|
||||
}
|
||||
|
||||
// Left analog stick for D-pad (with deadzone)
|
||||
int16_t axis_x = platform::GetGamepadAxis(gamepad, platform::kGamepadAxisLeftX);
|
||||
int16_t axis_y = platform::GetGamepadAxis(gamepad, platform::kGamepadAxisLeftY);
|
||||
|
||||
if (axis_x < -kAxisDeadzone) {
|
||||
state.SetButton(SnesButton::LEFT, true);
|
||||
} else if (axis_x > kAxisDeadzone) {
|
||||
state.SetButton(SnesButton::RIGHT, true);
|
||||
}
|
||||
|
||||
if (axis_y < -kAxisDeadzone) {
|
||||
state.SetButton(SnesButton::UP, true);
|
||||
} else if (axis_y > kAxisDeadzone) {
|
||||
state.SetButton(SnesButton::DOWN, true);
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3InputBackend::ProcessEvent(void* event) {
|
||||
if (!initialized_ || !event) return;
|
||||
|
||||
SDL_Event* sdl_event = static_cast<SDL_Event*>(event);
|
||||
|
||||
// Handle keyboard events
|
||||
// SDL3: Uses SDL_EVENT_KEY_DOWN/UP instead of SDL_KEYDOWN/UP
|
||||
// SDL3: Uses event.key.key instead of event.key.keysym.sym
|
||||
if (sdl_event->type == platform::kEventKeyDown) {
|
||||
UpdateEventState(platform::GetKeyFromEvent(*sdl_event), true);
|
||||
} else if (sdl_event->type == platform::kEventKeyUp) {
|
||||
UpdateEventState(platform::GetKeyFromEvent(*sdl_event), false);
|
||||
}
|
||||
|
||||
// Handle gamepad connection/disconnection events
|
||||
HandleGamepadEvent(*sdl_event);
|
||||
}
|
||||
|
||||
void SDL3InputBackend::HandleGamepadEvent(const SDL_Event& event) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
// SDL3 uses SDL_EVENT_GAMEPAD_ADDED/REMOVED
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
// Try to open the gamepad if we have a free slot
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (!gamepads_[i]) {
|
||||
gamepads_[i] = SDL_OpenGamepad(event.gdevice.which);
|
||||
if (gamepads_[i]) {
|
||||
LOG_INFO("InputBackend", "SDL3 Gamepad connected for player " +
|
||||
std::to_string(i + 1));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
// Find and close the disconnected gamepad
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (gamepads_[i] &&
|
||||
SDL_GetGamepadID(gamepads_[i]) == event.gdevice.which) {
|
||||
SDL_CloseGamepad(gamepads_[i]);
|
||||
gamepads_[i] = nullptr;
|
||||
LOG_INFO("InputBackend", "SDL3 Gamepad disconnected for player " +
|
||||
std::to_string(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
// SDL2 uses SDL_CONTROLLERDEVICEADDED/REMOVED
|
||||
if (event.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (!gamepads_[i]) {
|
||||
gamepads_[i] = platform::OpenGamepad(event.cdevice.which);
|
||||
if (gamepads_[i]) {
|
||||
LOG_INFO("InputBackend", "Gamepad connected for player " +
|
||||
std::to_string(i + 1));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (event.type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (gamepads_[i] && SDL_JoystickInstanceID(
|
||||
SDL_GameControllerGetJoystick(gamepads_[i])) ==
|
||||
event.cdevice.which) {
|
||||
platform::CloseGamepad(gamepads_[i]);
|
||||
gamepads_[i] = nullptr;
|
||||
LOG_INFO("InputBackend", "Gamepad disconnected for player " +
|
||||
std::to_string(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SDL3InputBackend::UpdateEventState(int keycode, bool pressed) {
|
||||
// Map keycode to button and update event state
|
||||
if (keycode == config_.key_a)
|
||||
event_state_.SetButton(SnesButton::A, pressed);
|
||||
else if (keycode == config_.key_b)
|
||||
event_state_.SetButton(SnesButton::B, pressed);
|
||||
else if (keycode == config_.key_x)
|
||||
event_state_.SetButton(SnesButton::X, pressed);
|
||||
else if (keycode == config_.key_y)
|
||||
event_state_.SetButton(SnesButton::Y, pressed);
|
||||
else if (keycode == config_.key_l)
|
||||
event_state_.SetButton(SnesButton::L, pressed);
|
||||
else if (keycode == config_.key_r)
|
||||
event_state_.SetButton(SnesButton::R, pressed);
|
||||
else if (keycode == config_.key_start)
|
||||
event_state_.SetButton(SnesButton::START, pressed);
|
||||
else if (keycode == config_.key_select)
|
||||
event_state_.SetButton(SnesButton::SELECT, pressed);
|
||||
else if (keycode == config_.key_up)
|
||||
event_state_.SetButton(SnesButton::UP, pressed);
|
||||
else if (keycode == config_.key_down)
|
||||
event_state_.SetButton(SnesButton::DOWN, pressed);
|
||||
else if (keycode == config_.key_left)
|
||||
event_state_.SetButton(SnesButton::LEFT, pressed);
|
||||
else if (keycode == config_.key_right)
|
||||
event_state_.SetButton(SnesButton::RIGHT, pressed);
|
||||
}
|
||||
|
||||
InputConfig SDL3InputBackend::GetConfig() const { return config_; }
|
||||
|
||||
void SDL3InputBackend::SetConfig(const InputConfig& config) {
|
||||
config_ = config;
|
||||
|
||||
// Re-initialize gamepad if gamepad settings changed
|
||||
if (config_.enable_gamepad && !gamepads_[0]) {
|
||||
gamepads_[0] = platform::OpenGamepad(config_.gamepad_index);
|
||||
if (gamepads_[0]) {
|
||||
LOG_INFO("InputBackend", "SDL3 Gamepad connected for player 1");
|
||||
}
|
||||
} else if (!config_.enable_gamepad && gamepads_[0]) {
|
||||
platform::CloseGamepad(gamepads_[0]);
|
||||
gamepads_[0] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::string SDL3InputBackend::GetBackendName() const { return "SDL3"; }
|
||||
|
||||
bool SDL3InputBackend::IsInitialized() const { return initialized_; }
|
||||
|
||||
} // namespace input
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
72
src/app/emu/input/sdl3_input_backend.h
Normal file
72
src/app/emu/input/sdl3_input_backend.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#ifndef YAZE_APP_EMU_INPUT_SDL3_INPUT_BACKEND_H_
|
||||
#define YAZE_APP_EMU_INPUT_SDL3_INPUT_BACKEND_H_
|
||||
|
||||
#include "app/emu/input/input_backend.h"
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace input {
|
||||
|
||||
/**
|
||||
* @brief SDL3 input backend implementation
|
||||
*
|
||||
* Implements the IInputBackend interface using SDL3 APIs.
|
||||
* Key differences from SDL2:
|
||||
* - SDL_GetKeyboardState() returns bool* instead of Uint8*
|
||||
* - SDL_GameController is replaced with SDL_Gamepad
|
||||
* - Event types use SDL_EVENT_* prefix instead of SDL_*
|
||||
* - event.key.keysym.sym is replaced with event.key.key
|
||||
*/
|
||||
class SDL3InputBackend : public IInputBackend {
|
||||
public:
|
||||
SDL3InputBackend();
|
||||
~SDL3InputBackend() override;
|
||||
|
||||
// IInputBackend interface
|
||||
bool Initialize(const InputConfig& config) override;
|
||||
void Shutdown() override;
|
||||
ControllerState Poll(int player = 1) override;
|
||||
void ProcessEvent(void* event) override;
|
||||
InputConfig GetConfig() const override;
|
||||
void SetConfig(const InputConfig& config) override;
|
||||
std::string GetBackendName() const override;
|
||||
bool IsInitialized() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Update event state from keyboard event
|
||||
* @param keycode The keycode from the event
|
||||
* @param pressed Whether the key is pressed
|
||||
*/
|
||||
void UpdateEventState(int keycode, bool pressed);
|
||||
|
||||
/**
|
||||
* @brief Poll gamepad state and update controller state
|
||||
* @param state The controller state to update
|
||||
* @param player The player number (1-4)
|
||||
*/
|
||||
void PollGamepad(ControllerState& state, int player);
|
||||
|
||||
/**
|
||||
* @brief Handle gamepad connection/disconnection
|
||||
* @param event The SDL event
|
||||
*/
|
||||
void HandleGamepadEvent(const SDL_Event& event);
|
||||
|
||||
InputConfig config_;
|
||||
bool initialized_ = false;
|
||||
ControllerState event_state_; // Cached state for event-based mode
|
||||
|
||||
// Gamepad handles for up to 4 players
|
||||
platform::GamepadHandle gamepads_[4] = {nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
// Axis deadzone for analog sticks
|
||||
static constexpr int16_t kAxisDeadzone = 8000;
|
||||
};
|
||||
|
||||
} // namespace input
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_INPUT_SDL3_INPUT_BACKEND_H_
|
||||
@@ -206,14 +206,23 @@ void Snes::RunCycle() {
|
||||
next_horiz_event = 512;
|
||||
if (memory_.v_pos() == 0)
|
||||
memory_.init_hdma_request();
|
||||
|
||||
// Start PPU line rendering (setup for JIT rendering)
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.StartLine(memory_.v_pos());
|
||||
} break;
|
||||
case 512: {
|
||||
next_horiz_event = 1104;
|
||||
// render the line halfway of the screen for better compatibility
|
||||
// Render the line halfway of the screen for better compatibility
|
||||
// Using CatchUp instead of RunLine for progressive rendering
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.RunLine(memory_.v_pos());
|
||||
ppu_.CatchUp(512);
|
||||
} break;
|
||||
case 1104: {
|
||||
// Finish rendering the visible line
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.CatchUp(1104);
|
||||
|
||||
if (!in_vblank_)
|
||||
memory_.run_hdma_request();
|
||||
if (!memory_.pal_timing()) {
|
||||
@@ -507,6 +516,11 @@ uint8_t Snes::Read(uint32_t adr) {
|
||||
|
||||
void Snes::WriteBBus(uint8_t adr, uint8_t val) {
|
||||
if (adr < 0x40) {
|
||||
// PPU Register write - catch up rendering first to ensure mid-scanline effects work
|
||||
// Only needed if we are in the visible portion of a visible scanline
|
||||
if (!in_vblank_ && memory_.v_pos() > 0 && memory_.h_pos() < 1100) {
|
||||
ppu_.CatchUp(memory_.h_pos());
|
||||
}
|
||||
ppu_.Write(adr, val);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "app/emu/ui/input_handler.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "app/gui/core/icons.h"
|
||||
#include "app/platform/sdl_compat.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
@@ -42,12 +41,13 @@ void RenderKeyboardConfig(input::InputManager* manager) {
|
||||
ImGui::Text("Press any key...");
|
||||
ImGui::Separator();
|
||||
|
||||
// Poll for key press (SDL2-specific for now)
|
||||
// Poll for key press (cross-version compatible)
|
||||
SDL_Event event;
|
||||
if (SDL_PollEvent(&event) && event.type == SDL_KEYDOWN) {
|
||||
if (event.key.keysym.sym != SDLK_UNKNOWN &&
|
||||
event.key.keysym.sym != SDLK_ESCAPE) {
|
||||
*key = event.key.keysym.sym;
|
||||
if (SDL_PollEvent(&event) &&
|
||||
event.type == platform::kEventKeyDown) {
|
||||
SDL_Keycode keycode = platform::GetKeyFromEvent(event);
|
||||
if (keycode != SDLK_UNKNOWN && keycode != SDLK_ESCAPE) {
|
||||
*key = keycode;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ void Ppu::Reset() {
|
||||
ppu1_open_bus_ = 0;
|
||||
ppu2_open_bus_ = 0;
|
||||
memset(pixelBuffer, 0, sizeof(pixelBuffer));
|
||||
last_rendered_x_ = 0;
|
||||
}
|
||||
|
||||
void Ppu::HandleFrameStart() {
|
||||
@@ -142,8 +143,10 @@ void Ppu::HandleFrameStart() {
|
||||
even_frame = !even_frame;
|
||||
}
|
||||
|
||||
void Ppu::RunLine(int line) {
|
||||
// called for lines 1-224/239
|
||||
void Ppu::StartLine(int line) {
|
||||
current_scanline_ = line;
|
||||
last_rendered_x_ = 0;
|
||||
|
||||
// evaluate sprites
|
||||
obj_pixel_buffer_.fill(0);
|
||||
if (!forced_blank_)
|
||||
@@ -151,9 +154,27 @@ void Ppu::RunLine(int line) {
|
||||
// actual line
|
||||
if (mode == 7)
|
||||
CalculateMode7Starts(line);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
HandlePixel(x, line);
|
||||
}
|
||||
|
||||
void Ppu::CatchUp(int h_pos) {
|
||||
// h_pos is in master cycles. 1 pixel = 4 cycles.
|
||||
// Visible pixels are 0-255, corresponding to h_pos 0-1024 roughly.
|
||||
int target_x = h_pos / 4;
|
||||
|
||||
// Clamp to screen width
|
||||
if (target_x > 256) target_x = 256;
|
||||
if (target_x <= last_rendered_x_) return;
|
||||
|
||||
for (int x = last_rendered_x_; x < target_x; x++) {
|
||||
HandlePixel(x, current_scanline_);
|
||||
}
|
||||
last_rendered_x_ = target_x;
|
||||
}
|
||||
|
||||
void Ppu::RunLine(int line) {
|
||||
// Legacy wrapper - renders the whole line at once
|
||||
StartLine(line);
|
||||
CatchUp(2000); // Ensure full line (256 pixels * 4 = 1024)
|
||||
}
|
||||
|
||||
void Ppu::HandlePixel(int x, int y) {
|
||||
|
||||
@@ -265,6 +265,8 @@ class Ppu {
|
||||
|
||||
void Reset();
|
||||
void HandleFrameStart();
|
||||
void StartLine(int line);
|
||||
void CatchUp(int h_pos);
|
||||
void RunLine(int line);
|
||||
void HandlePixel(int x, int y);
|
||||
|
||||
@@ -344,6 +346,8 @@ class Ppu {
|
||||
uint16_t cgram[0x100];
|
||||
|
||||
private:
|
||||
int last_rendered_x_ = 0;
|
||||
|
||||
uint8_t cgram_pointer_;
|
||||
bool cgram_second_write_;
|
||||
uint8_t cgram_buffer_;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
149
src/app/gfx/backend/renderer_factory.h
Normal file
149
src/app/gfx/backend/renderer_factory.h
Normal file
@@ -0,0 +1,149 @@
|
||||
#ifndef YAZE_APP_GFX_BACKEND_RENDERER_FACTORY_H_
|
||||
#define YAZE_APP_GFX_BACKEND_RENDERER_FACTORY_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
#include "app/gfx/backend/sdl2_renderer.h"
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
#include "app/gfx/backend/sdl3_renderer.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
|
||||
/**
|
||||
* @enum RendererBackendType
|
||||
* @brief Enumeration of available rendering backend types.
|
||||
*/
|
||||
enum class RendererBackendType {
|
||||
SDL2, ///< SDL2 renderer backend
|
||||
SDL3, ///< SDL3 renderer backend
|
||||
kDefault, ///< Use the default backend based on build configuration
|
||||
kAutoDetect ///< Automatically select the best available backend
|
||||
};
|
||||
|
||||
/**
|
||||
* @class RendererFactory
|
||||
* @brief Factory class for creating IRenderer instances.
|
||||
*
|
||||
* This factory provides a centralized way to create renderer instances
|
||||
* based on the desired backend type. It abstracts away the concrete
|
||||
* renderer implementations, allowing the application to be configured
|
||||
* for different SDL versions at compile time or runtime.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* // Create with default backend (based on build configuration)
|
||||
* auto renderer = RendererFactory::Create();
|
||||
*
|
||||
* // Create with specific backend
|
||||
* auto renderer = RendererFactory::Create(RendererBackendType::SDL2);
|
||||
* @endcode
|
||||
*/
|
||||
class RendererFactory {
|
||||
public:
|
||||
/**
|
||||
* @brief Create a renderer instance with the specified backend type.
|
||||
*
|
||||
* @param type The desired backend type. If kDefault or kAutoDetect,
|
||||
* the factory will use the backend based on build configuration
|
||||
* (SDL3 if YAZE_USE_SDL3 is defined, SDL2 otherwise).
|
||||
* @return A unique pointer to the created IRenderer instance.
|
||||
* Returns nullptr if the requested backend is not available.
|
||||
*/
|
||||
static std::unique_ptr<IRenderer> Create(
|
||||
RendererBackendType type = RendererBackendType::kDefault) {
|
||||
switch (type) {
|
||||
case RendererBackendType::SDL2:
|
||||
return std::make_unique<SDL2Renderer>();
|
||||
|
||||
case RendererBackendType::SDL3:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return std::make_unique<SDL3Renderer>();
|
||||
#else
|
||||
// SDL3 not available in this build, fall back to SDL2
|
||||
return std::make_unique<SDL2Renderer>();
|
||||
#endif
|
||||
|
||||
case RendererBackendType::kDefault:
|
||||
case RendererBackendType::kAutoDetect:
|
||||
default:
|
||||
// Use the default backend based on build configuration
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return std::make_unique<SDL3Renderer>();
|
||||
#else
|
||||
return std::make_unique<SDL2Renderer>();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a specific backend type is available in this build.
|
||||
*
|
||||
* @param type The backend type to check.
|
||||
* @return true if the backend is available, false otherwise.
|
||||
*/
|
||||
static bool IsBackendAvailable(RendererBackendType type) {
|
||||
switch (type) {
|
||||
case RendererBackendType::SDL2:
|
||||
// SDL2 is always available (base requirement)
|
||||
return true;
|
||||
|
||||
case RendererBackendType::SDL3:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
||||
case RendererBackendType::kDefault:
|
||||
case RendererBackendType::kAutoDetect:
|
||||
// Default/auto-detect is always available
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get a string name for a backend type.
|
||||
*
|
||||
* @param type The backend type.
|
||||
* @return A human-readable name for the backend.
|
||||
*/
|
||||
static const char* GetBackendName(RendererBackendType type) {
|
||||
switch (type) {
|
||||
case RendererBackendType::SDL2:
|
||||
return "SDL2";
|
||||
case RendererBackendType::SDL3:
|
||||
return "SDL3";
|
||||
case RendererBackendType::kDefault:
|
||||
return "Default";
|
||||
case RendererBackendType::kAutoDetect:
|
||||
return "AutoDetect";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the default backend type for this build.
|
||||
*
|
||||
* @return The default backend type based on build configuration.
|
||||
*/
|
||||
static RendererBackendType GetDefaultBackendType() {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return RendererBackendType::SDL3;
|
||||
#else
|
||||
return RendererBackendType::SDL2;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_GFX_BACKEND_RENDERER_FACTORY_H_
|
||||
216
src/app/gfx/backend/sdl3_renderer.cc
Normal file
216
src/app/gfx/backend/sdl3_renderer.cc
Normal file
@@ -0,0 +1,216 @@
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
#include "app/gfx/backend/sdl3_renderer.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
|
||||
SDL3Renderer::SDL3Renderer() = default;
|
||||
|
||||
SDL3Renderer::~SDL3Renderer() { Shutdown(); }
|
||||
|
||||
/**
|
||||
* @brief Initializes the SDL3 renderer.
|
||||
*
|
||||
* This function creates an SDL3 renderer and attaches it to the given window.
|
||||
* SDL3 simplified renderer creation - no driver index or flags parameter.
|
||||
* Use SDL_SetRenderVSync() separately for vsync control.
|
||||
*/
|
||||
bool SDL3Renderer::Initialize(SDL_Window* window) {
|
||||
// Create an SDL3 renderer.
|
||||
// SDL3 API: SDL_CreateRenderer(window, driver_name)
|
||||
// Pass nullptr to let SDL choose the best available driver.
|
||||
renderer_ = SDL_CreateRenderer(window, nullptr);
|
||||
|
||||
if (renderer_ == nullptr) {
|
||||
SDL_Log("SDL_CreateRenderer Error: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set blend mode for transparency support.
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Enable vsync for smoother rendering.
|
||||
SDL_SetRenderVSync(renderer_, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shuts down the renderer.
|
||||
*/
|
||||
void SDL3Renderer::Shutdown() {
|
||||
if (renderer_) {
|
||||
SDL_DestroyRenderer(renderer_);
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates an SDL_Texture with default streaming access.
|
||||
*
|
||||
* The texture is created with streaming access, which is suitable for textures
|
||||
* that are updated frequently.
|
||||
*/
|
||||
TextureHandle SDL3Renderer::CreateTexture(int width, int height) {
|
||||
// SDL3 texture creation is largely unchanged from SDL2.
|
||||
return static_cast<TextureHandle>(
|
||||
SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888,
|
||||
SDL_TEXTUREACCESS_STREAMING, width, height));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates an SDL_Texture with a specific pixel format and access
|
||||
* pattern.
|
||||
*
|
||||
* This is useful for specialized textures like emulator PPU output.
|
||||
*/
|
||||
TextureHandle SDL3Renderer::CreateTextureWithFormat(int width, int height,
|
||||
uint32_t format,
|
||||
int access) {
|
||||
return static_cast<TextureHandle>(
|
||||
SDL_CreateTexture(renderer_, format, access, width, height));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Updates an SDL_Texture with data from a Bitmap.
|
||||
*
|
||||
* This involves converting the bitmap's surface to the correct format and
|
||||
* updating the texture. SDL3 renamed SDL_ConvertSurfaceFormat to
|
||||
* SDL_ConvertSurface and removed the flags parameter.
|
||||
*/
|
||||
void SDL3Renderer::UpdateTexture(TextureHandle texture, const Bitmap& bitmap) {
|
||||
SDL_Surface* surface = bitmap.surface();
|
||||
|
||||
// Validate texture, surface, and surface format
|
||||
if (!texture || !surface || surface->format == SDL_PIXELFORMAT_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate surface has pixels
|
||||
if (!surface->pixels || surface->w <= 0 || surface->h <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the bitmap's surface to RGBA8888 format for compatibility with the
|
||||
// texture.
|
||||
// SDL3 API: SDL_ConvertSurface(surface, format) - no flags parameter
|
||||
SDL_Surface* converted_surface =
|
||||
SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA8888);
|
||||
|
||||
if (!converted_surface || !converted_surface->pixels) {
|
||||
if (converted_surface) {
|
||||
SDL_DestroySurface(converted_surface);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the texture with the pixels from the converted surface.
|
||||
SDL_UpdateTexture(static_cast<SDL_Texture*>(texture), nullptr,
|
||||
converted_surface->pixels, converted_surface->pitch);
|
||||
|
||||
// SDL3 uses SDL_DestroySurface instead of SDL_FreeSurface
|
||||
SDL_DestroySurface(converted_surface);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destroys an SDL_Texture.
|
||||
*/
|
||||
void SDL3Renderer::DestroyTexture(TextureHandle texture) {
|
||||
if (texture) {
|
||||
SDL_DestroyTexture(static_cast<SDL_Texture*>(texture));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Locks a texture for direct pixel access.
|
||||
*/
|
||||
bool SDL3Renderer::LockTexture(TextureHandle texture, SDL_Rect* rect,
|
||||
void** pixels, int* pitch) {
|
||||
// SDL3 LockTexture now takes SDL_FRect*, but for simplicity we use the
|
||||
// integer version when available. In SDL3, LockTexture still accepts
|
||||
// SDL_Rect* for the region.
|
||||
return SDL_LockTexture(static_cast<SDL_Texture*>(texture), rect, pixels,
|
||||
pitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Unlocks a previously locked texture.
|
||||
*/
|
||||
void SDL3Renderer::UnlockTexture(TextureHandle texture) {
|
||||
SDL_UnlockTexture(static_cast<SDL_Texture*>(texture));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clears the screen with the current draw color.
|
||||
*/
|
||||
void SDL3Renderer::Clear() { SDL_RenderClear(renderer_); }
|
||||
|
||||
/**
|
||||
* @brief Presents the rendered frame to the screen.
|
||||
*/
|
||||
void SDL3Renderer::Present() { SDL_RenderPresent(renderer_); }
|
||||
|
||||
/**
|
||||
* @brief Copies a texture to the render target.
|
||||
*
|
||||
* SDL3 renamed SDL_RenderCopy to SDL_RenderTexture and uses SDL_FRect
|
||||
* for the destination rectangle.
|
||||
*/
|
||||
void SDL3Renderer::RenderCopy(TextureHandle texture, const SDL_Rect* srcrect,
|
||||
const SDL_Rect* dstrect) {
|
||||
SDL_FRect src_frect, dst_frect;
|
||||
SDL_FRect* src_ptr = ToFRect(srcrect, &src_frect);
|
||||
SDL_FRect* dst_ptr = ToFRect(dstrect, &dst_frect);
|
||||
|
||||
// SDL3 API: SDL_RenderTexture(renderer, texture, srcrect, dstrect)
|
||||
// Both rectangles use SDL_FRect (float) in SDL3.
|
||||
SDL_RenderTexture(renderer_, static_cast<SDL_Texture*>(texture), src_ptr,
|
||||
dst_ptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the render target.
|
||||
*/
|
||||
void SDL3Renderer::SetRenderTarget(TextureHandle texture) {
|
||||
SDL_SetRenderTarget(renderer_, static_cast<SDL_Texture*>(texture));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the draw color.
|
||||
*/
|
||||
void SDL3Renderer::SetDrawColor(SDL_Color color) {
|
||||
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert SDL_Rect (int) to SDL_FRect (float).
|
||||
*
|
||||
* SDL3 uses floating-point rectangles for many rendering operations.
|
||||
* This helper converts integer rectangles to float rectangles.
|
||||
*
|
||||
* @param rect Input integer rectangle (may be nullptr)
|
||||
* @param frect Output float rectangle
|
||||
* @return Pointer to frect if rect was valid, nullptr otherwise
|
||||
*/
|
||||
SDL_FRect* SDL3Renderer::ToFRect(const SDL_Rect* rect, SDL_FRect* frect) {
|
||||
if (!rect || !frect) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
frect->x = static_cast<float>(rect->x);
|
||||
frect->y = static_cast<float>(rect->y);
|
||||
frect->w = static_cast<float>(rect->w);
|
||||
frect->h = static_cast<float>(rect->h);
|
||||
|
||||
return frect;
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
86
src/app/gfx/backend/sdl3_renderer.h
Normal file
86
src/app/gfx/backend/sdl3_renderer.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#ifndef YAZE_APP_GFX_BACKEND_SDL3_RENDERER_H_
|
||||
#define YAZE_APP_GFX_BACKEND_SDL3_RENDERER_H_
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
|
||||
/**
|
||||
* @class SDL3Renderer
|
||||
* @brief A concrete implementation of the IRenderer interface using SDL3.
|
||||
*
|
||||
* This class encapsulates all rendering logic specific to the SDL3 renderer API.
|
||||
* It translates the abstract calls from the IRenderer interface into concrete
|
||||
* SDL3 commands.
|
||||
*
|
||||
* Key SDL3 API differences from SDL2:
|
||||
* - SDL_CreateRenderer() takes a driver name (nullptr for auto) instead of index
|
||||
* - SDL_RenderCopy() is replaced by SDL_RenderTexture()
|
||||
* - Many functions now use SDL_FRect (float) instead of SDL_Rect (int)
|
||||
* - SDL_FreeSurface() is replaced by SDL_DestroySurface()
|
||||
* - SDL_ConvertSurfaceFormat() is replaced by SDL_ConvertSurface()
|
||||
* - Surface pixel format access uses SDL_GetPixelFormatDetails()
|
||||
*/
|
||||
class SDL3Renderer : public IRenderer {
|
||||
public:
|
||||
SDL3Renderer();
|
||||
~SDL3Renderer() override;
|
||||
|
||||
// --- Lifecycle and Initialization ---
|
||||
bool Initialize(SDL_Window* window) override;
|
||||
void Shutdown() override;
|
||||
|
||||
// --- Texture Management ---
|
||||
TextureHandle CreateTexture(int width, int height) override;
|
||||
TextureHandle CreateTextureWithFormat(int width, int height, uint32_t format,
|
||||
int access) override;
|
||||
void UpdateTexture(TextureHandle texture, const Bitmap& bitmap) override;
|
||||
void DestroyTexture(TextureHandle texture) override;
|
||||
|
||||
// --- Direct Pixel Access ---
|
||||
bool LockTexture(TextureHandle texture, SDL_Rect* rect, void** pixels,
|
||||
int* pitch) override;
|
||||
void UnlockTexture(TextureHandle texture) override;
|
||||
|
||||
// --- Rendering Primitives ---
|
||||
void Clear() override;
|
||||
void Present() override;
|
||||
void RenderCopy(TextureHandle texture, const SDL_Rect* srcrect,
|
||||
const SDL_Rect* dstrect) override;
|
||||
void SetRenderTarget(TextureHandle texture) override;
|
||||
void SetDrawColor(SDL_Color color) override;
|
||||
|
||||
/**
|
||||
* @brief Provides access to the underlying SDL_Renderer*.
|
||||
* @return A void pointer that can be safely cast to an SDL_Renderer*.
|
||||
*/
|
||||
void* GetBackendRenderer() override { return renderer_; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Convert SDL_Rect (int) to SDL_FRect (float) for SDL3 API calls.
|
||||
* @param rect Pointer to SDL_Rect to convert, may be nullptr.
|
||||
* @param frect Output SDL_FRect.
|
||||
* @return Pointer to frect if rect was valid, nullptr otherwise.
|
||||
*/
|
||||
static SDL_FRect* ToFRect(const SDL_Rect* rect, SDL_FRect* frect);
|
||||
|
||||
// The core SDL3 renderer object.
|
||||
// Unlike SDL2Renderer, we don't use a custom deleter because SDL3 has
|
||||
// different cleanup semantics and we want explicit control over shutdown.
|
||||
SDL_Renderer* renderer_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
|
||||
#endif // YAZE_APP_GFX_BACKEND_SDL3_RENDERER_H_
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "bitmap.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring> // for memcpy
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_GFX_BITMAP_H
|
||||
#define YAZE_APP_GFX_BITMAP_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_GFX_PERFORMANCE_PERFORMANCE_PROFILER_H
|
||||
#define YAZE_APP_GFX_PERFORMANCE_PERFORMANCE_PROFILER_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
@@ -58,6 +58,11 @@ set(GFX_BACKEND_SRC
|
||||
app/gfx/backend/sdl2_renderer.cc
|
||||
)
|
||||
|
||||
# Conditionally add SDL3 renderer when YAZE_USE_SDL3 is enabled
|
||||
if(YAZE_USE_SDL3)
|
||||
list(APPEND GFX_BACKEND_SRC app/gfx/backend/sdl3_renderer.cc)
|
||||
endif()
|
||||
|
||||
# build_cleaner:auto-maintain
|
||||
set(GFX_RESOURCE_SRC
|
||||
app/gfx/resource/memory_pool.cc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_GFX_ATLAS_RENDERER_H
|
||||
#define YAZE_APP_GFX_ATLAS_RENDERER_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "app/gfx/resource/arena.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_GFX_BPP_FORMAT_MANAGER_H
|
||||
#define YAZE_APP_GFX_BPP_FORMAT_MANAGER_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_GFX_scad_format_H
|
||||
#define YAZE_APP_GFX_scad_format_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
57
src/app/gui/style/theme.h
Normal file
57
src/app/gui/style/theme.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Theme definitions for yaze UI components.
|
||||
// Centralized color palette and style constants to ensure visual consistency.
|
||||
|
||||
#ifndef YAZE_SRC_APP_GUI_STYLE_THEME_H_
|
||||
#define YAZE_SRC_APP_GUI_STYLE_THEME_H_
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace yaze::gui::style {
|
||||
|
||||
struct Theme {
|
||||
// Primary brand color (used for titles, highlights)
|
||||
ImVec4 primary = ImVec4(0.196f, 0.6f, 0.8f, 1.0f); // teal-ish
|
||||
// Secondary accent (buttons, active states)
|
||||
ImVec4 secondary = ImVec4(0.133f, 0.545f, 0.133f, 1.0f); // forest green
|
||||
// Warning / error color
|
||||
ImVec4 warning = ImVec4(0.8f, 0.2f, 0.2f, 1.0f);
|
||||
// Success color
|
||||
ImVec4 success = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
|
||||
// Background for panels
|
||||
ImVec4 panel_bg = ImVec4(0.07f, 0.07f, 0.07f, 0.95f);
|
||||
// Text color (default)
|
||||
ImVec4 text = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
// Rounded corner radius for windows and child panels
|
||||
float rounding = 6.0f;
|
||||
};
|
||||
|
||||
// Returns the default theme used throughout the application.
|
||||
inline const Theme& DefaultTheme() {
|
||||
static Theme theme;
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Apply the theme to ImGui style (call once per frame before drawing UI).
|
||||
inline void ApplyTheme(const Theme& theme) {
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.WindowRounding = theme.rounding;
|
||||
style.ChildRounding = theme.rounding;
|
||||
style.FrameRounding = theme.rounding;
|
||||
style.GrabRounding = theme.rounding;
|
||||
style.PopupRounding = theme.rounding;
|
||||
style.ScrollbarRounding = theme.rounding;
|
||||
|
||||
// Colors – we keep most defaults, but override key ones.
|
||||
style.Colors[ImGuiCol_TitleBgActive] = theme.primary;
|
||||
style.Colors[ImGuiCol_Button] = theme.secondary;
|
||||
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(theme.secondary.x * 1.2f,
|
||||
theme.secondary.y * 1.2f,
|
||||
theme.secondary.z * 1.2f, 1.0f);
|
||||
style.Colors[ImGuiCol_Text] = theme.text;
|
||||
style.Colors[ImGuiCol_ChildBg] = theme.panel_bg;
|
||||
}
|
||||
|
||||
} // namespace yaze::gui::style
|
||||
|
||||
#endif // YAZE_SRC_APP_GUI_STYLE_THEME_H_
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "util/crash_handler.h"
|
||||
#include "util/flag.h"
|
||||
#include "util/log.h"
|
||||
#include "util/platform_paths.h"
|
||||
#include "yaze.h" // For YAZE_VERSION_STRING
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
@@ -92,7 +93,17 @@ int main(int argc, char** argv) {
|
||||
log_categories.insert(categories_str.substr(start));
|
||||
}
|
||||
|
||||
yaze::util::LogManager::instance().configure(log_level, FLAGS_log_file->Get(),
|
||||
// Determine log file path
|
||||
std::string log_path = FLAGS_log_file->Get();
|
||||
if (log_path.empty()) {
|
||||
// Default to ~/Documents/Yaze/logs/yaze.log if not specified
|
||||
auto logs_dir = yaze::util::PlatformPaths::GetUserDocumentsSubdirectory("logs");
|
||||
if (logs_dir.ok()) {
|
||||
log_path = (*logs_dir / "yaze.log").string();
|
||||
}
|
||||
}
|
||||
|
||||
yaze::util::LogManager::instance().configure(log_level, log_path,
|
||||
log_categories);
|
||||
|
||||
// Enable console logging via feature flag if debug is enabled.
|
||||
@@ -154,7 +165,7 @@ int main(int argc, char** argv) {
|
||||
auto status = api_server->Start(FLAGS_api_port->Get());
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("Main", "Failed to start API server: %s",
|
||||
std::string(status.message()).c_str());
|
||||
std::string(status.message().data(), status.message().size()).c_str());
|
||||
} else {
|
||||
LOG_INFO("Main", "API Server started on port %d", FLAGS_api_port->Get());
|
||||
}
|
||||
|
||||
293
src/app/platform/iwindow.h
Normal file
293
src/app/platform/iwindow.h
Normal file
@@ -0,0 +1,293 @@
|
||||
// iwindow.h - Window Backend Abstraction Layer
|
||||
// Provides interface for swapping window implementations (SDL2, SDL3)
|
||||
|
||||
#ifndef YAZE_APP_PLATFORM_IWINDOW_H_
|
||||
#define YAZE_APP_PLATFORM_IWINDOW_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
|
||||
// Forward declarations to avoid SDL header dependency in interface
|
||||
struct SDL_Window;
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
/**
|
||||
* @brief Window configuration parameters
|
||||
*/
|
||||
struct WindowConfig {
|
||||
std::string title = "Yet Another Zelda3 Editor";
|
||||
int width = 0; // 0 means auto-detect from display
|
||||
int height = 0; // 0 means auto-detect from display
|
||||
float display_scale = 0.8f; // Percentage of display to use when auto-detect
|
||||
bool resizable = true;
|
||||
bool maximized = false;
|
||||
bool fullscreen = false;
|
||||
bool high_dpi = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Window event types (platform-agnostic)
|
||||
*/
|
||||
enum class WindowEventType {
|
||||
None,
|
||||
Close,
|
||||
Resize,
|
||||
Minimized,
|
||||
Maximized,
|
||||
Restored,
|
||||
Shown,
|
||||
Hidden,
|
||||
Exposed,
|
||||
FocusGained,
|
||||
FocusLost,
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
MouseMotion,
|
||||
MouseButtonDown,
|
||||
MouseButtonUp,
|
||||
MouseWheel,
|
||||
Quit,
|
||||
DropFile
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Platform-agnostic window event data
|
||||
*/
|
||||
struct WindowEvent {
|
||||
WindowEventType type = WindowEventType::None;
|
||||
|
||||
// Window resize data
|
||||
int window_width = 0;
|
||||
int window_height = 0;
|
||||
|
||||
// Keyboard data
|
||||
int key_code = 0;
|
||||
int scan_code = 0;
|
||||
bool key_shift = false;
|
||||
bool key_ctrl = false;
|
||||
bool key_alt = false;
|
||||
bool key_super = false;
|
||||
|
||||
// Mouse data
|
||||
float mouse_x = 0.0f;
|
||||
float mouse_y = 0.0f;
|
||||
int mouse_button = 0;
|
||||
float wheel_x = 0.0f;
|
||||
float wheel_y = 0.0f;
|
||||
|
||||
// Drop file data
|
||||
std::string dropped_file;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Window backend status information
|
||||
*/
|
||||
struct WindowStatus {
|
||||
bool is_active = true;
|
||||
bool is_minimized = false;
|
||||
bool is_maximized = false;
|
||||
bool is_fullscreen = false;
|
||||
bool is_focused = true;
|
||||
bool is_resizing = false;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Abstract window backend interface
|
||||
*
|
||||
* Provides platform-agnostic window management, allowing different
|
||||
* SDL versions or other windowing libraries to be swapped without
|
||||
* changing application code.
|
||||
*/
|
||||
class IWindowBackend {
|
||||
public:
|
||||
virtual ~IWindowBackend() = default;
|
||||
|
||||
// =========================================================================
|
||||
// Lifecycle Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Initialize the window backend with configuration
|
||||
* @param config Window configuration parameters
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status Initialize(const WindowConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief Shutdown the window backend and release resources
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status Shutdown() = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if the backend is initialized
|
||||
*/
|
||||
virtual bool IsInitialized() const = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Event Processing
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Poll and process pending events
|
||||
* @param out_event Output parameter for the next event
|
||||
* @return True if an event was available, false otherwise
|
||||
*/
|
||||
virtual bool PollEvent(WindowEvent& out_event) = 0;
|
||||
|
||||
/**
|
||||
* @brief Process a native SDL event (for ImGui integration)
|
||||
* @param native_event Pointer to native SDL_Event
|
||||
*/
|
||||
virtual void ProcessNativeEvent(void* native_event) = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Window State
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get current window status
|
||||
*/
|
||||
virtual WindowStatus GetStatus() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if window is still active (not closed)
|
||||
*/
|
||||
virtual bool IsActive() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Set window active state
|
||||
*/
|
||||
virtual void SetActive(bool active) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get window dimensions
|
||||
*/
|
||||
virtual void GetSize(int* width, int* height) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Set window dimensions
|
||||
*/
|
||||
virtual void SetSize(int width, int height) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get window title
|
||||
*/
|
||||
virtual std::string GetTitle() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Set window title
|
||||
*/
|
||||
virtual void SetTitle(const std::string& title) = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Renderer Integration
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Initialize renderer for this window
|
||||
* @param renderer The renderer to initialize
|
||||
* @return True if successful
|
||||
*/
|
||||
virtual bool InitializeRenderer(gfx::IRenderer* renderer) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the underlying SDL_Window pointer for ImGui integration
|
||||
* @return Native window handle (SDL_Window*)
|
||||
*/
|
||||
virtual SDL_Window* GetNativeWindow() = 0;
|
||||
|
||||
// =========================================================================
|
||||
// ImGui Integration
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Initialize ImGui backends for this window/renderer combo
|
||||
* @param renderer The renderer (for backend-specific init)
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status InitializeImGui(gfx::IRenderer* renderer) = 0;
|
||||
|
||||
/**
|
||||
* @brief Shutdown ImGui backends
|
||||
*/
|
||||
virtual void ShutdownImGui() = 0;
|
||||
|
||||
/**
|
||||
* @brief Start a new ImGui frame
|
||||
*/
|
||||
virtual void NewImGuiFrame() = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Audio Support (Legacy compatibility)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get audio device ID (for legacy audio buffer management)
|
||||
*/
|
||||
virtual uint32_t GetAudioDevice() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get audio buffer (for legacy audio management)
|
||||
*/
|
||||
virtual std::shared_ptr<int16_t> GetAudioBuffer() const = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Backend Information
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get backend name for debugging/logging
|
||||
*/
|
||||
virtual std::string GetBackendName() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get SDL version being used
|
||||
*/
|
||||
virtual int GetSDLVersion() const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Backend type enumeration for factory
|
||||
*/
|
||||
enum class WindowBackendType {
|
||||
SDL2,
|
||||
SDL3,
|
||||
Auto // Automatically select based on availability
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Factory for creating window backends
|
||||
*/
|
||||
class WindowBackendFactory {
|
||||
public:
|
||||
/**
|
||||
* @brief Create a window backend of the specified type
|
||||
* @param type The type of backend to create
|
||||
* @return Unique pointer to the created backend
|
||||
*/
|
||||
static std::unique_ptr<IWindowBackend> Create(WindowBackendType type);
|
||||
|
||||
/**
|
||||
* @brief Get the default backend type for this build
|
||||
*/
|
||||
static WindowBackendType GetDefaultType();
|
||||
|
||||
/**
|
||||
* @brief Check if a backend type is available
|
||||
*/
|
||||
static bool IsAvailable(WindowBackendType type);
|
||||
};
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_IWINDOW_H_
|
||||
435
src/app/platform/sdl2_window_backend.cc
Normal file
435
src/app/platform/sdl2_window_backend.cc
Normal file
@@ -0,0 +1,435 @@
|
||||
// sdl2_window_backend.cc - SDL2 Window Backend Implementation
|
||||
|
||||
#include "app/platform/sdl2_window_backend.h"
|
||||
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gui/core/style.h"
|
||||
#include "app/platform/font_loader.h"
|
||||
#include "imgui/backends/imgui_impl_sdl2.h"
|
||||
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
// Global flag for window resize state (used by emulator to pause)
|
||||
// This maintains compatibility with the legacy window.cc
|
||||
extern bool g_window_is_resizing;
|
||||
|
||||
SDL2WindowBackend::~SDL2WindowBackend() {
|
||||
if (initialized_) {
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status SDL2WindowBackend::Initialize(const WindowConfig& config) {
|
||||
if (initialized_) {
|
||||
LOG_WARN("SDL2WindowBackend", "Already initialized, shutting down first");
|
||||
RETURN_IF_ERROR(Shutdown());
|
||||
}
|
||||
|
||||
// Initialize SDL2 subsystems
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_Init failed: %s", SDL_GetError()));
|
||||
}
|
||||
|
||||
// Determine window size
|
||||
int screen_width = config.width;
|
||||
int screen_height = config.height;
|
||||
|
||||
if (screen_width == 0 || screen_height == 0) {
|
||||
// Auto-detect from display
|
||||
SDL_DisplayMode display_mode;
|
||||
if (SDL_GetCurrentDisplayMode(0, &display_mode) == 0) {
|
||||
screen_width = static_cast<int>(display_mode.w * config.display_scale);
|
||||
screen_height = static_cast<int>(display_mode.h * config.display_scale);
|
||||
} else {
|
||||
// Fallback to reasonable defaults
|
||||
screen_width = 1280;
|
||||
screen_height = 720;
|
||||
LOG_WARN("SDL2WindowBackend",
|
||||
"Failed to get display mode, using defaults: %dx%d",
|
||||
screen_width, screen_height);
|
||||
}
|
||||
}
|
||||
|
||||
// Build window flags
|
||||
Uint32 flags = 0;
|
||||
if (config.resizable) {
|
||||
flags |= SDL_WINDOW_RESIZABLE;
|
||||
}
|
||||
if (config.maximized) {
|
||||
flags |= SDL_WINDOW_MAXIMIZED;
|
||||
}
|
||||
if (config.fullscreen) {
|
||||
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
if (config.high_dpi) {
|
||||
flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
}
|
||||
|
||||
// Create window
|
||||
window_ = std::unique_ptr<SDL_Window, util::SDL_Deleter>(
|
||||
SDL_CreateWindow(config.title.c_str(), SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL_WINDOWPOS_UNDEFINED, screen_width, screen_height,
|
||||
flags),
|
||||
util::SDL_Deleter());
|
||||
|
||||
if (!window_) {
|
||||
SDL_Quit();
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_CreateWindow failed: %s", SDL_GetError()));
|
||||
}
|
||||
|
||||
// Allocate legacy audio buffer for backwards compatibility
|
||||
const int audio_frequency = 48000;
|
||||
const size_t buffer_size = (audio_frequency / 50) * 2; // Stereo PAL
|
||||
audio_buffer_ = std::shared_ptr<int16_t>(new int16_t[buffer_size],
|
||||
std::default_delete<int16_t[]>());
|
||||
|
||||
LOG_INFO("SDL2WindowBackend",
|
||||
"Initialized: %dx%d, audio buffer: %zu samples", screen_width,
|
||||
screen_height, buffer_size);
|
||||
|
||||
initialized_ = true;
|
||||
active_ = true;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SDL2WindowBackend::Shutdown() {
|
||||
if (!initialized_) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Pause and close audio device if open
|
||||
if (audio_device_ != 0) {
|
||||
SDL_PauseAudioDevice(audio_device_, 1);
|
||||
SDL_CloseAudioDevice(audio_device_);
|
||||
audio_device_ = 0;
|
||||
}
|
||||
|
||||
// Shutdown ImGui if initialized
|
||||
if (imgui_initialized_) {
|
||||
ShutdownImGui();
|
||||
}
|
||||
|
||||
// Shutdown graphics arena while renderer is still valid
|
||||
LOG_INFO("SDL2WindowBackend", "Shutting down graphics arena...");
|
||||
gfx::Arena::Get().Shutdown();
|
||||
|
||||
// Destroy window
|
||||
if (window_) {
|
||||
LOG_INFO("SDL2WindowBackend", "Destroying window...");
|
||||
window_.reset();
|
||||
}
|
||||
|
||||
// Quit SDL
|
||||
LOG_INFO("SDL2WindowBackend", "Shutting down SDL...");
|
||||
SDL_Quit();
|
||||
|
||||
initialized_ = false;
|
||||
LOG_INFO("SDL2WindowBackend", "Shutdown complete");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool SDL2WindowBackend::PollEvent(WindowEvent& out_event) {
|
||||
SDL_Event sdl_event;
|
||||
if (SDL_PollEvent(&sdl_event)) {
|
||||
// Let ImGui process the event first
|
||||
if (imgui_initialized_) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&sdl_event);
|
||||
}
|
||||
|
||||
// Convert to platform-agnostic event
|
||||
out_event = ConvertSDL2Event(sdl_event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::ProcessNativeEvent(void* native_event) {
|
||||
if (native_event && imgui_initialized_) {
|
||||
ImGui_ImplSDL2_ProcessEvent(static_cast<SDL_Event*>(native_event));
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent SDL2WindowBackend::ConvertSDL2Event(const SDL_Event& sdl_event) {
|
||||
WindowEvent event;
|
||||
event.type = WindowEventType::None;
|
||||
|
||||
switch (sdl_event.type) {
|
||||
case SDL_QUIT:
|
||||
event.type = WindowEventType::Quit;
|
||||
active_ = false;
|
||||
break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
event.type = WindowEventType::KeyDown;
|
||||
event.key_code = sdl_event.key.keysym.sym;
|
||||
event.scan_code = sdl_event.key.keysym.scancode;
|
||||
UpdateModifierState();
|
||||
event.key_shift = key_shift_;
|
||||
event.key_ctrl = key_ctrl_;
|
||||
event.key_alt = key_alt_;
|
||||
event.key_super = key_super_;
|
||||
break;
|
||||
|
||||
case SDL_KEYUP:
|
||||
event.type = WindowEventType::KeyUp;
|
||||
event.key_code = sdl_event.key.keysym.sym;
|
||||
event.scan_code = sdl_event.key.keysym.scancode;
|
||||
UpdateModifierState();
|
||||
event.key_shift = key_shift_;
|
||||
event.key_ctrl = key_ctrl_;
|
||||
event.key_alt = key_alt_;
|
||||
event.key_super = key_super_;
|
||||
break;
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
event.type = WindowEventType::MouseMotion;
|
||||
event.mouse_x = static_cast<float>(sdl_event.motion.x);
|
||||
event.mouse_y = static_cast<float>(sdl_event.motion.y);
|
||||
break;
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
event.type = WindowEventType::MouseButtonDown;
|
||||
event.mouse_x = static_cast<float>(sdl_event.button.x);
|
||||
event.mouse_y = static_cast<float>(sdl_event.button.y);
|
||||
event.mouse_button = sdl_event.button.button;
|
||||
break;
|
||||
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
event.type = WindowEventType::MouseButtonUp;
|
||||
event.mouse_x = static_cast<float>(sdl_event.button.x);
|
||||
event.mouse_y = static_cast<float>(sdl_event.button.y);
|
||||
event.mouse_button = sdl_event.button.button;
|
||||
break;
|
||||
|
||||
case SDL_MOUSEWHEEL:
|
||||
event.type = WindowEventType::MouseWheel;
|
||||
event.wheel_x = static_cast<float>(sdl_event.wheel.x);
|
||||
event.wheel_y = static_cast<float>(sdl_event.wheel.y);
|
||||
break;
|
||||
|
||||
case SDL_DROPFILE:
|
||||
event.type = WindowEventType::DropFile;
|
||||
if (sdl_event.drop.file) {
|
||||
event.dropped_file = sdl_event.drop.file;
|
||||
SDL_free(sdl_event.drop.file);
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (sdl_event.window.event) {
|
||||
case SDL_WINDOWEVENT_CLOSE:
|
||||
event.type = WindowEventType::Close;
|
||||
active_ = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_RESIZED:
|
||||
event.type = WindowEventType::Resize;
|
||||
event.window_width = sdl_event.window.data1;
|
||||
event.window_height = sdl_event.window.data2;
|
||||
is_resizing_ = true;
|
||||
g_window_is_resizing = true;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_MINIMIZED:
|
||||
event.type = WindowEventType::Minimized;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_MAXIMIZED:
|
||||
event.type = WindowEventType::Maximized;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_RESTORED:
|
||||
event.type = WindowEventType::Restored;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_SHOWN:
|
||||
event.type = WindowEventType::Shown;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_HIDDEN:
|
||||
event.type = WindowEventType::Hidden;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
event.type = WindowEventType::Exposed;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_FOCUS_GAINED:
|
||||
event.type = WindowEventType::FocusGained;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
event.type = WindowEventType::FocusLost;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::UpdateModifierState() {
|
||||
SDL_Keymod mod = SDL_GetModState();
|
||||
key_shift_ = (mod & KMOD_SHIFT) != 0;
|
||||
key_ctrl_ = (mod & KMOD_CTRL) != 0;
|
||||
key_alt_ = (mod & KMOD_ALT) != 0;
|
||||
key_super_ = (mod & KMOD_GUI) != 0;
|
||||
}
|
||||
|
||||
WindowStatus SDL2WindowBackend::GetStatus() const {
|
||||
WindowStatus status;
|
||||
status.is_active = active_;
|
||||
status.is_resizing = is_resizing_;
|
||||
|
||||
if (window_) {
|
||||
Uint32 flags = SDL_GetWindowFlags(window_.get());
|
||||
status.is_minimized = (flags & SDL_WINDOW_MINIMIZED) != 0;
|
||||
status.is_maximized = (flags & SDL_WINDOW_MAXIMIZED) != 0;
|
||||
status.is_fullscreen =
|
||||
(flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP)) != 0;
|
||||
status.is_focused = (flags & SDL_WINDOW_INPUT_FOCUS) != 0;
|
||||
|
||||
SDL_GetWindowSize(window_.get(), &status.width, &status.height);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::GetSize(int* width, int* height) const {
|
||||
if (window_) {
|
||||
SDL_GetWindowSize(window_.get(), width, height);
|
||||
} else {
|
||||
if (width) *width = 0;
|
||||
if (height) *height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::SetSize(int width, int height) {
|
||||
if (window_) {
|
||||
SDL_SetWindowSize(window_.get(), width, height);
|
||||
}
|
||||
}
|
||||
|
||||
std::string SDL2WindowBackend::GetTitle() const {
|
||||
if (window_) {
|
||||
const char* title = SDL_GetWindowTitle(window_.get());
|
||||
return title ? title : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::SetTitle(const std::string& title) {
|
||||
if (window_) {
|
||||
SDL_SetWindowTitle(window_.get(), title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool SDL2WindowBackend::InitializeRenderer(gfx::IRenderer* renderer) {
|
||||
if (!window_ || !renderer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (renderer->GetBackendRenderer()) {
|
||||
// Already initialized
|
||||
return true;
|
||||
}
|
||||
|
||||
return renderer->Initialize(window_.get());
|
||||
}
|
||||
|
||||
absl::Status SDL2WindowBackend::InitializeImGui(gfx::IRenderer* renderer) {
|
||||
if (imgui_initialized_) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
if (!renderer) {
|
||||
return absl::InvalidArgumentError("Renderer is null");
|
||||
}
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
|
||||
// Initialize ImGui backends
|
||||
SDL_Renderer* sdl_renderer =
|
||||
static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
|
||||
|
||||
if (!sdl_renderer) {
|
||||
return absl::InternalError("Failed to get SDL renderer from IRenderer");
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForSDLRenderer(window_.get(), sdl_renderer)) {
|
||||
return absl::InternalError("ImGui_ImplSDL2_InitForSDLRenderer failed");
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDLRenderer2_Init(sdl_renderer)) {
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
return absl::InternalError("ImGui_ImplSDLRenderer2_Init failed");
|
||||
}
|
||||
|
||||
// Load fonts
|
||||
RETURN_IF_ERROR(LoadPackageFonts());
|
||||
|
||||
// Apply default style
|
||||
gui::ColorsYaze();
|
||||
|
||||
imgui_initialized_ = true;
|
||||
LOG_INFO("SDL2WindowBackend", "ImGui initialized successfully");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::ShutdownImGui() {
|
||||
if (!imgui_initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("SDL2WindowBackend", "Shutting down ImGui implementations...");
|
||||
ImGui_ImplSDLRenderer2_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
|
||||
LOG_INFO("SDL2WindowBackend", "Destroying ImGui context...");
|
||||
ImGui::DestroyContext();
|
||||
|
||||
imgui_initialized_ = false;
|
||||
}
|
||||
|
||||
void SDL2WindowBackend::NewImGuiFrame() {
|
||||
if (!imgui_initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui_ImplSDLRenderer2_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
}
|
||||
|
||||
// Define the global variable for backward compatibility
|
||||
bool g_window_is_resizing = false;
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
91
src/app/platform/sdl2_window_backend.h
Normal file
91
src/app/platform/sdl2_window_backend.h
Normal file
@@ -0,0 +1,91 @@
|
||||
// sdl2_window_backend.h - SDL2 Window Backend Implementation
|
||||
|
||||
#ifndef YAZE_APP_PLATFORM_SDL2_WINDOW_BACKEND_H_
|
||||
#define YAZE_APP_PLATFORM_SDL2_WINDOW_BACKEND_H_
|
||||
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/platform/iwindow.h"
|
||||
#include "util/sdl_deleter.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
/**
|
||||
* @brief SDL2 implementation of the window backend interface
|
||||
*
|
||||
* Wraps SDL2 window management, event handling, and ImGui integration
|
||||
* for the main YAZE application window.
|
||||
*/
|
||||
class SDL2WindowBackend : public IWindowBackend {
|
||||
public:
|
||||
SDL2WindowBackend() = default;
|
||||
~SDL2WindowBackend() override;
|
||||
|
||||
// =========================================================================
|
||||
// IWindowBackend Implementation
|
||||
// =========================================================================
|
||||
|
||||
absl::Status Initialize(const WindowConfig& config) override;
|
||||
absl::Status Shutdown() override;
|
||||
bool IsInitialized() const override { return initialized_; }
|
||||
|
||||
bool PollEvent(WindowEvent& out_event) override;
|
||||
void ProcessNativeEvent(void* native_event) override;
|
||||
|
||||
WindowStatus GetStatus() const override;
|
||||
bool IsActive() const override { return active_; }
|
||||
void SetActive(bool active) override { active_ = active; }
|
||||
|
||||
void GetSize(int* width, int* height) const override;
|
||||
void SetSize(int width, int height) override;
|
||||
std::string GetTitle() const override;
|
||||
void SetTitle(const std::string& title) override;
|
||||
|
||||
bool InitializeRenderer(gfx::IRenderer* renderer) override;
|
||||
SDL_Window* GetNativeWindow() override { return window_.get(); }
|
||||
|
||||
absl::Status InitializeImGui(gfx::IRenderer* renderer) override;
|
||||
void ShutdownImGui() override;
|
||||
void NewImGuiFrame() override;
|
||||
|
||||
uint32_t GetAudioDevice() const override { return audio_device_; }
|
||||
std::shared_ptr<int16_t> GetAudioBuffer() const override {
|
||||
return audio_buffer_;
|
||||
}
|
||||
|
||||
std::string GetBackendName() const override { return "SDL2"; }
|
||||
int GetSDLVersion() const override { return 2; }
|
||||
|
||||
private:
|
||||
// Convert SDL2 event to platform-agnostic WindowEvent
|
||||
WindowEvent ConvertSDL2Event(const SDL_Event& sdl_event);
|
||||
|
||||
// Update modifier key state from SDL
|
||||
void UpdateModifierState();
|
||||
|
||||
std::unique_ptr<SDL_Window, util::SDL_Deleter> window_;
|
||||
bool initialized_ = false;
|
||||
bool active_ = true;
|
||||
bool is_resizing_ = false;
|
||||
bool imgui_initialized_ = false;
|
||||
|
||||
// Modifier key state
|
||||
bool key_shift_ = false;
|
||||
bool key_ctrl_ = false;
|
||||
bool key_alt_ = false;
|
||||
bool key_super_ = false;
|
||||
|
||||
// Legacy audio support
|
||||
SDL_AudioDeviceID audio_device_ = 0;
|
||||
std::shared_ptr<int16_t> audio_buffer_;
|
||||
};
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_SDL2_WINDOW_BACKEND_H_
|
||||
456
src/app/platform/sdl3_window_backend.cc
Normal file
456
src/app/platform/sdl3_window_backend.cc
Normal file
@@ -0,0 +1,456 @@
|
||||
// sdl3_window_backend.cc - SDL3 Window Backend Implementation
|
||||
|
||||
// Only compile SDL3 backend when YAZE_USE_SDL3 is defined
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
#include "app/platform/sdl3_window_backend.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gui/core/style.h"
|
||||
#include "app/platform/font_loader.h"
|
||||
#include "imgui/backends/imgui_impl_sdl3.h"
|
||||
#include "imgui/backends/imgui_impl_sdlrenderer3.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
// Global flag for window resize state (used by emulator to pause)
|
||||
extern bool g_window_is_resizing;
|
||||
|
||||
SDL3WindowBackend::~SDL3WindowBackend() {
|
||||
if (initialized_) {
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status SDL3WindowBackend::Initialize(const WindowConfig& config) {
|
||||
if (initialized_) {
|
||||
LOG_WARN("SDL3WindowBackend", "Already initialized, shutting down first");
|
||||
RETURN_IF_ERROR(Shutdown());
|
||||
}
|
||||
|
||||
// Initialize SDL3 subsystems
|
||||
// Note: SDL3 removed SDL_INIT_TIMER (timer is always available)
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_Init failed: %s", SDL_GetError()));
|
||||
}
|
||||
|
||||
// Determine window size
|
||||
int screen_width = config.width;
|
||||
int screen_height = config.height;
|
||||
|
||||
if (screen_width == 0 || screen_height == 0) {
|
||||
// Auto-detect from display
|
||||
// SDL3 uses SDL_GetPrimaryDisplay() and SDL_GetCurrentDisplayMode()
|
||||
SDL_DisplayID display_id = SDL_GetPrimaryDisplay();
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display_id);
|
||||
|
||||
if (mode) {
|
||||
screen_width = static_cast<int>(mode->w * config.display_scale);
|
||||
screen_height = static_cast<int>(mode->h * config.display_scale);
|
||||
} else {
|
||||
// Fallback to reasonable defaults
|
||||
screen_width = 1280;
|
||||
screen_height = 720;
|
||||
LOG_WARN("SDL3WindowBackend",
|
||||
"Failed to get display mode, using defaults: %dx%d",
|
||||
screen_width, screen_height);
|
||||
}
|
||||
}
|
||||
|
||||
// Build window flags
|
||||
// Note: SDL3 changed some flag names
|
||||
SDL_WindowFlags flags = 0;
|
||||
if (config.resizable) {
|
||||
flags |= SDL_WINDOW_RESIZABLE;
|
||||
}
|
||||
if (config.maximized) {
|
||||
flags |= SDL_WINDOW_MAXIMIZED;
|
||||
}
|
||||
if (config.fullscreen) {
|
||||
flags |= SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
if (config.high_dpi) {
|
||||
flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
||||
}
|
||||
|
||||
// Create window
|
||||
// Note: SDL3 uses SDL_CreateWindow with different signature
|
||||
SDL_Window* raw_window =
|
||||
SDL_CreateWindow(config.title.c_str(), screen_width, screen_height, flags);
|
||||
|
||||
if (!raw_window) {
|
||||
SDL_Quit();
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_CreateWindow failed: %s", SDL_GetError()));
|
||||
}
|
||||
|
||||
window_ = std::unique_ptr<SDL_Window, SDL3WindowDeleter>(raw_window);
|
||||
|
||||
// Allocate legacy audio buffer for backwards compatibility
|
||||
const int audio_frequency = 48000;
|
||||
const size_t buffer_size = (audio_frequency / 50) * 2; // Stereo PAL
|
||||
audio_buffer_ = std::shared_ptr<int16_t>(new int16_t[buffer_size],
|
||||
std::default_delete<int16_t[]>());
|
||||
|
||||
LOG_INFO("SDL3WindowBackend",
|
||||
"Initialized: %dx%d, audio buffer: %zu samples", screen_width,
|
||||
screen_height, buffer_size);
|
||||
|
||||
initialized_ = true;
|
||||
active_ = true;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SDL3WindowBackend::Shutdown() {
|
||||
if (!initialized_) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Shutdown ImGui if initialized
|
||||
if (imgui_initialized_) {
|
||||
ShutdownImGui();
|
||||
}
|
||||
|
||||
// Shutdown graphics arena while renderer is still valid
|
||||
LOG_INFO("SDL3WindowBackend", "Shutting down graphics arena...");
|
||||
gfx::Arena::Get().Shutdown();
|
||||
|
||||
// Destroy window
|
||||
if (window_) {
|
||||
LOG_INFO("SDL3WindowBackend", "Destroying window...");
|
||||
window_.reset();
|
||||
}
|
||||
|
||||
// Quit SDL
|
||||
LOG_INFO("SDL3WindowBackend", "Shutting down SDL...");
|
||||
SDL_Quit();
|
||||
|
||||
initialized_ = false;
|
||||
LOG_INFO("SDL3WindowBackend", "Shutdown complete");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool SDL3WindowBackend::PollEvent(WindowEvent& out_event) {
|
||||
SDL_Event sdl_event;
|
||||
if (SDL_PollEvent(&sdl_event)) {
|
||||
// Let ImGui process the event first
|
||||
if (imgui_initialized_) {
|
||||
ImGui_ImplSDL3_ProcessEvent(&sdl_event);
|
||||
}
|
||||
|
||||
// Convert to platform-agnostic event
|
||||
out_event = ConvertSDL3Event(sdl_event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::ProcessNativeEvent(void* native_event) {
|
||||
if (native_event && imgui_initialized_) {
|
||||
ImGui_ImplSDL3_ProcessEvent(static_cast<SDL_Event*>(native_event));
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent SDL3WindowBackend::ConvertSDL3Event(const SDL_Event& sdl_event) {
|
||||
WindowEvent event;
|
||||
event.type = WindowEventType::None;
|
||||
|
||||
switch (sdl_event.type) {
|
||||
// =========================================================================
|
||||
// Application Events
|
||||
// =========================================================================
|
||||
case SDL_EVENT_QUIT:
|
||||
event.type = WindowEventType::Quit;
|
||||
active_ = false;
|
||||
break;
|
||||
|
||||
// =========================================================================
|
||||
// Keyboard Events
|
||||
// Note: SDL3 uses event.key.key instead of event.key.keysym.sym
|
||||
// =========================================================================
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
event.type = WindowEventType::KeyDown;
|
||||
event.key_code = sdl_event.key.key;
|
||||
event.scan_code = sdl_event.key.scancode;
|
||||
UpdateModifierState();
|
||||
event.key_shift = key_shift_;
|
||||
event.key_ctrl = key_ctrl_;
|
||||
event.key_alt = key_alt_;
|
||||
event.key_super = key_super_;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_KEY_UP:
|
||||
event.type = WindowEventType::KeyUp;
|
||||
event.key_code = sdl_event.key.key;
|
||||
event.scan_code = sdl_event.key.scancode;
|
||||
UpdateModifierState();
|
||||
event.key_shift = key_shift_;
|
||||
event.key_ctrl = key_ctrl_;
|
||||
event.key_alt = key_alt_;
|
||||
event.key_super = key_super_;
|
||||
break;
|
||||
|
||||
// =========================================================================
|
||||
// Mouse Events
|
||||
// Note: SDL3 uses float coordinates
|
||||
// =========================================================================
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
event.type = WindowEventType::MouseMotion;
|
||||
event.mouse_x = sdl_event.motion.x;
|
||||
event.mouse_y = sdl_event.motion.y;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
event.type = WindowEventType::MouseButtonDown;
|
||||
event.mouse_x = sdl_event.button.x;
|
||||
event.mouse_y = sdl_event.button.y;
|
||||
event.mouse_button = sdl_event.button.button;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
event.type = WindowEventType::MouseButtonUp;
|
||||
event.mouse_x = sdl_event.button.x;
|
||||
event.mouse_y = sdl_event.button.y;
|
||||
event.mouse_button = sdl_event.button.button;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
event.type = WindowEventType::MouseWheel;
|
||||
event.wheel_x = sdl_event.wheel.x;
|
||||
event.wheel_y = sdl_event.wheel.y;
|
||||
break;
|
||||
|
||||
// =========================================================================
|
||||
// Drop Events
|
||||
// =========================================================================
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
event.type = WindowEventType::DropFile;
|
||||
if (sdl_event.drop.data) {
|
||||
event.dropped_file = sdl_event.drop.data;
|
||||
// Note: SDL3 drop.data is managed by SDL, don't free it
|
||||
}
|
||||
break;
|
||||
|
||||
// =========================================================================
|
||||
// Window Events - SDL3 Major Change
|
||||
// SDL3 no longer uses SDL_WINDOWEVENT with sub-types.
|
||||
// Each window event type is now a top-level event.
|
||||
// =========================================================================
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
event.type = WindowEventType::Close;
|
||||
active_ = false;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
event.type = WindowEventType::Resize;
|
||||
event.window_width = sdl_event.window.data1;
|
||||
event.window_height = sdl_event.window.data2;
|
||||
is_resizing_ = true;
|
||||
g_window_is_resizing = true;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
// This is the SDL3 equivalent of SDL_WINDOWEVENT_SIZE_CHANGED
|
||||
event.type = WindowEventType::Resize;
|
||||
event.window_width = sdl_event.window.data1;
|
||||
event.window_height = sdl_event.window.data2;
|
||||
is_resizing_ = true;
|
||||
g_window_is_resizing = true;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_MINIMIZED:
|
||||
event.type = WindowEventType::Minimized;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_MAXIMIZED:
|
||||
event.type = WindowEventType::Maximized;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_RESTORED:
|
||||
event.type = WindowEventType::Restored;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_SHOWN:
|
||||
event.type = WindowEventType::Shown;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_HIDDEN:
|
||||
event.type = WindowEventType::Hidden;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_EXPOSED:
|
||||
event.type = WindowEventType::Exposed;
|
||||
is_resizing_ = false;
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
event.type = WindowEventType::FocusGained;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
event.type = WindowEventType::FocusLost;
|
||||
break;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::UpdateModifierState() {
|
||||
// SDL3 uses SDL_GetModState which returns SDL_Keymod
|
||||
SDL_Keymod mod = SDL_GetModState();
|
||||
key_shift_ = (mod & SDL_KMOD_SHIFT) != 0;
|
||||
key_ctrl_ = (mod & SDL_KMOD_CTRL) != 0;
|
||||
key_alt_ = (mod & SDL_KMOD_ALT) != 0;
|
||||
key_super_ = (mod & SDL_KMOD_GUI) != 0;
|
||||
}
|
||||
|
||||
WindowStatus SDL3WindowBackend::GetStatus() const {
|
||||
WindowStatus status;
|
||||
status.is_active = active_;
|
||||
status.is_resizing = is_resizing_;
|
||||
|
||||
if (window_) {
|
||||
SDL_WindowFlags flags = SDL_GetWindowFlags(window_.get());
|
||||
status.is_minimized = (flags & SDL_WINDOW_MINIMIZED) != 0;
|
||||
status.is_maximized = (flags & SDL_WINDOW_MAXIMIZED) != 0;
|
||||
status.is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN) != 0;
|
||||
status.is_focused = (flags & SDL_WINDOW_INPUT_FOCUS) != 0;
|
||||
|
||||
SDL_GetWindowSize(window_.get(), &status.width, &status.height);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::GetSize(int* width, int* height) const {
|
||||
if (window_) {
|
||||
SDL_GetWindowSize(window_.get(), width, height);
|
||||
} else {
|
||||
if (width) *width = 0;
|
||||
if (height) *height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::SetSize(int width, int height) {
|
||||
if (window_) {
|
||||
SDL_SetWindowSize(window_.get(), width, height);
|
||||
}
|
||||
}
|
||||
|
||||
std::string SDL3WindowBackend::GetTitle() const {
|
||||
if (window_) {
|
||||
const char* title = SDL_GetWindowTitle(window_.get());
|
||||
return title ? title : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::SetTitle(const std::string& title) {
|
||||
if (window_) {
|
||||
SDL_SetWindowTitle(window_.get(), title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool SDL3WindowBackend::InitializeRenderer(gfx::IRenderer* renderer) {
|
||||
if (!window_ || !renderer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (renderer->GetBackendRenderer()) {
|
||||
// Already initialized
|
||||
return true;
|
||||
}
|
||||
|
||||
return renderer->Initialize(window_.get());
|
||||
}
|
||||
|
||||
absl::Status SDL3WindowBackend::InitializeImGui(gfx::IRenderer* renderer) {
|
||||
if (imgui_initialized_) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
if (!renderer) {
|
||||
return absl::InvalidArgumentError("Renderer is null");
|
||||
}
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
|
||||
// Initialize ImGui backends for SDL3
|
||||
SDL_Renderer* sdl_renderer =
|
||||
static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
|
||||
|
||||
if (!sdl_renderer) {
|
||||
return absl::InternalError("Failed to get SDL renderer from IRenderer");
|
||||
}
|
||||
|
||||
// Note: SDL3 uses different ImGui backend functions
|
||||
if (!ImGui_ImplSDL3_InitForSDLRenderer(window_.get(), sdl_renderer)) {
|
||||
return absl::InternalError("ImGui_ImplSDL3_InitForSDLRenderer failed");
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDLRenderer3_Init(sdl_renderer)) {
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
return absl::InternalError("ImGui_ImplSDLRenderer3_Init failed");
|
||||
}
|
||||
|
||||
// Load fonts
|
||||
RETURN_IF_ERROR(LoadPackageFonts());
|
||||
|
||||
// Apply default style
|
||||
gui::ColorsYaze();
|
||||
|
||||
imgui_initialized_ = true;
|
||||
LOG_INFO("SDL3WindowBackend", "ImGui initialized successfully");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::ShutdownImGui() {
|
||||
if (!imgui_initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("SDL3WindowBackend", "Shutting down ImGui implementations...");
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
|
||||
LOG_INFO("SDL3WindowBackend", "Destroying ImGui context...");
|
||||
ImGui::DestroyContext();
|
||||
|
||||
imgui_initialized_ = false;
|
||||
}
|
||||
|
||||
void SDL3WindowBackend::NewImGuiFrame() {
|
||||
if (!imgui_initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
103
src/app/platform/sdl3_window_backend.h
Normal file
103
src/app/platform/sdl3_window_backend.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// sdl3_window_backend.h - SDL3 Window Backend Implementation
|
||||
|
||||
#ifndef YAZE_APP_PLATFORM_SDL3_WINDOW_BACKEND_H_
|
||||
#define YAZE_APP_PLATFORM_SDL3_WINDOW_BACKEND_H_
|
||||
|
||||
// Only compile SDL3 backend when YAZE_USE_SDL3 is defined
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/platform/iwindow.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
// Forward declaration for unique_ptr custom deleter
|
||||
struct SDL3WindowDeleter {
|
||||
void operator()(SDL_Window* p) const {
|
||||
if (p) SDL_DestroyWindow(p);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief SDL3 implementation of the window backend interface
|
||||
*
|
||||
* Handles the significant event handling changes in SDL3:
|
||||
* - Individual window events instead of SDL_WINDOWEVENT
|
||||
* - SDL_EVENT_* naming convention
|
||||
* - event.key.key instead of event.key.keysym.sym
|
||||
* - bool* keyboard state instead of Uint8*
|
||||
*/
|
||||
class SDL3WindowBackend : public IWindowBackend {
|
||||
public:
|
||||
SDL3WindowBackend() = default;
|
||||
~SDL3WindowBackend() override;
|
||||
|
||||
// =========================================================================
|
||||
// IWindowBackend Implementation
|
||||
// =========================================================================
|
||||
|
||||
absl::Status Initialize(const WindowConfig& config) override;
|
||||
absl::Status Shutdown() override;
|
||||
bool IsInitialized() const override { return initialized_; }
|
||||
|
||||
bool PollEvent(WindowEvent& out_event) override;
|
||||
void ProcessNativeEvent(void* native_event) override;
|
||||
|
||||
WindowStatus GetStatus() const override;
|
||||
bool IsActive() const override { return active_; }
|
||||
void SetActive(bool active) override { active_ = active; }
|
||||
|
||||
void GetSize(int* width, int* height) const override;
|
||||
void SetSize(int width, int height) override;
|
||||
std::string GetTitle() const override;
|
||||
void SetTitle(const std::string& title) override;
|
||||
|
||||
bool InitializeRenderer(gfx::IRenderer* renderer) override;
|
||||
SDL_Window* GetNativeWindow() override { return window_.get(); }
|
||||
|
||||
absl::Status InitializeImGui(gfx::IRenderer* renderer) override;
|
||||
void ShutdownImGui() override;
|
||||
void NewImGuiFrame() override;
|
||||
|
||||
uint32_t GetAudioDevice() const override { return 0; } // SDL3 uses streams
|
||||
std::shared_ptr<int16_t> GetAudioBuffer() const override {
|
||||
return audio_buffer_;
|
||||
}
|
||||
|
||||
std::string GetBackendName() const override { return "SDL3"; }
|
||||
int GetSDLVersion() const override { return 3; }
|
||||
|
||||
private:
|
||||
// Convert SDL3 event to platform-agnostic WindowEvent
|
||||
WindowEvent ConvertSDL3Event(const SDL_Event& sdl_event);
|
||||
|
||||
// Update modifier key state from SDL3
|
||||
void UpdateModifierState();
|
||||
|
||||
std::unique_ptr<SDL_Window, SDL3WindowDeleter> window_;
|
||||
bool initialized_ = false;
|
||||
bool active_ = true;
|
||||
bool is_resizing_ = false;
|
||||
bool imgui_initialized_ = false;
|
||||
|
||||
// Modifier key state
|
||||
bool key_shift_ = false;
|
||||
bool key_ctrl_ = false;
|
||||
bool key_alt_ = false;
|
||||
bool key_super_ = false;
|
||||
|
||||
// Legacy audio buffer for compatibility
|
||||
std::shared_ptr<int16_t> audio_buffer_;
|
||||
};
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
#endif // YAZE_APP_PLATFORM_SDL3_WINDOW_BACKEND_H_
|
||||
510
src/app/platform/sdl_compat.h
Normal file
510
src/app/platform/sdl_compat.h
Normal file
@@ -0,0 +1,510 @@
|
||||
#ifndef YAZE_APP_PLATFORM_SDL_COMPAT_H_
|
||||
#define YAZE_APP_PLATFORM_SDL_COMPAT_H_
|
||||
|
||||
/**
|
||||
* @file sdl_compat.h
|
||||
* @brief SDL2/SDL3 compatibility layer
|
||||
*
|
||||
* This header provides cross-version compatibility between SDL2 and SDL3.
|
||||
* It defines type aliases, macros, and wrapper functions that allow
|
||||
* application code to work with both versions.
|
||||
*/
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
#include <SDL3/SDL.h>
|
||||
#else
|
||||
#include <SDL.h>
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
// ============================================================================
|
||||
// Type Aliases
|
||||
// ============================================================================
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
// SDL3 uses bool* for keyboard state
|
||||
using KeyboardState = const bool*;
|
||||
#else
|
||||
// SDL2 uses Uint8* for keyboard state
|
||||
using KeyboardState = const Uint8*;
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// Event Type Constants
|
||||
// ============================================================================
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
constexpr auto kEventKeyDown = SDL_EVENT_KEY_DOWN;
|
||||
constexpr auto kEventKeyUp = SDL_EVENT_KEY_UP;
|
||||
constexpr auto kEventMouseMotion = SDL_EVENT_MOUSE_MOTION;
|
||||
constexpr auto kEventMouseButtonDown = SDL_EVENT_MOUSE_BUTTON_DOWN;
|
||||
constexpr auto kEventMouseButtonUp = SDL_EVENT_MOUSE_BUTTON_UP;
|
||||
constexpr auto kEventMouseWheel = SDL_EVENT_MOUSE_WHEEL;
|
||||
constexpr auto kEventQuit = SDL_EVENT_QUIT;
|
||||
constexpr auto kEventDropFile = SDL_EVENT_DROP_FILE;
|
||||
constexpr auto kEventWindowCloseRequested = SDL_EVENT_WINDOW_CLOSE_REQUESTED;
|
||||
constexpr auto kEventWindowResized = SDL_EVENT_WINDOW_RESIZED;
|
||||
constexpr auto kEventGamepadAdded = SDL_EVENT_GAMEPAD_ADDED;
|
||||
constexpr auto kEventGamepadRemoved = SDL_EVENT_GAMEPAD_REMOVED;
|
||||
#else
|
||||
constexpr auto kEventKeyDown = SDL_KEYDOWN;
|
||||
constexpr auto kEventKeyUp = SDL_KEYUP;
|
||||
constexpr auto kEventMouseMotion = SDL_MOUSEMOTION;
|
||||
constexpr auto kEventMouseButtonDown = SDL_MOUSEBUTTONDOWN;
|
||||
constexpr auto kEventMouseButtonUp = SDL_MOUSEBUTTONUP;
|
||||
constexpr auto kEventMouseWheel = SDL_MOUSEWHEEL;
|
||||
constexpr auto kEventQuit = SDL_QUIT;
|
||||
constexpr auto kEventDropFile = SDL_DROPFILE;
|
||||
// SDL2 uses SDL_WINDOWEVENT with sub-types, not individual events
|
||||
// These are handled specially in window code
|
||||
constexpr auto kEventWindowEvent = SDL_WINDOWEVENT;
|
||||
constexpr auto kEventControllerDeviceAdded = SDL_CONTROLLERDEVICEADDED;
|
||||
constexpr auto kEventControllerDeviceRemoved = SDL_CONTROLLERDEVICEREMOVED;
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Get keyboard state from SDL event
|
||||
* @param event The SDL event
|
||||
* @return The keycode from the event
|
||||
*/
|
||||
inline SDL_Keycode GetKeyFromEvent(const SDL_Event& event) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return event.key.key;
|
||||
#else
|
||||
return event.key.keysym.sym;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a key is pressed using the keyboard state
|
||||
* @param state The keyboard state from SDL_GetKeyboardState
|
||||
* @param scancode The scancode to check
|
||||
* @return True if the key is pressed
|
||||
*/
|
||||
inline bool IsKeyPressed(KeyboardState state, SDL_Scancode scancode) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
// SDL3 returns bool*
|
||||
return state[scancode];
|
||||
#else
|
||||
// SDL2 returns Uint8*, non-zero means pressed
|
||||
return state[scancode] != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Gamepad/Controller Helpers
|
||||
// ============================================================================
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
// SDL3 uses SDL_Gamepad instead of SDL_GameController
|
||||
using GamepadHandle = SDL_Gamepad*;
|
||||
|
||||
inline GamepadHandle OpenGamepad(int index) {
|
||||
SDL_JoystickID* joysticks = SDL_GetGamepads(nullptr);
|
||||
if (joysticks && index < 4) {
|
||||
SDL_JoystickID id = joysticks[index];
|
||||
SDL_free(joysticks);
|
||||
return SDL_OpenGamepad(id);
|
||||
}
|
||||
if (joysticks) SDL_free(joysticks);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline void CloseGamepad(GamepadHandle gamepad) {
|
||||
if (gamepad) SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
|
||||
inline bool GetGamepadButton(GamepadHandle gamepad, SDL_GamepadButton button) {
|
||||
return SDL_GetGamepadButton(gamepad, button);
|
||||
}
|
||||
|
||||
inline int16_t GetGamepadAxis(GamepadHandle gamepad, SDL_GamepadAxis axis) {
|
||||
return SDL_GetGamepadAxis(gamepad, axis);
|
||||
}
|
||||
|
||||
inline bool IsGamepadConnected(int index) {
|
||||
int count = 0;
|
||||
SDL_JoystickID* joysticks = SDL_GetGamepads(&count);
|
||||
if (joysticks) {
|
||||
SDL_free(joysticks);
|
||||
}
|
||||
return index < count;
|
||||
}
|
||||
#else
|
||||
// SDL2 uses SDL_GameController
|
||||
using GamepadHandle = SDL_GameController*;
|
||||
|
||||
inline GamepadHandle OpenGamepad(int index) {
|
||||
if (SDL_IsGameController(index)) {
|
||||
return SDL_GameControllerOpen(index);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline void CloseGamepad(GamepadHandle gamepad) {
|
||||
if (gamepad) SDL_GameControllerClose(gamepad);
|
||||
}
|
||||
|
||||
inline bool GetGamepadButton(GamepadHandle gamepad,
|
||||
SDL_GameControllerButton button) {
|
||||
return SDL_GameControllerGetButton(gamepad, button) != 0;
|
||||
}
|
||||
|
||||
inline int16_t GetGamepadAxis(GamepadHandle gamepad,
|
||||
SDL_GameControllerAxis axis) {
|
||||
return SDL_GameControllerGetAxis(gamepad, axis);
|
||||
}
|
||||
|
||||
inline bool IsGamepadConnected(int index) {
|
||||
return SDL_IsGameController(index);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// Button/Axis Type Aliases
|
||||
// ============================================================================
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
using GamepadButton = SDL_GamepadButton;
|
||||
using GamepadAxis = SDL_GamepadAxis;
|
||||
|
||||
constexpr auto kGamepadButtonA = SDL_GAMEPAD_BUTTON_SOUTH;
|
||||
constexpr auto kGamepadButtonB = SDL_GAMEPAD_BUTTON_EAST;
|
||||
constexpr auto kGamepadButtonX = SDL_GAMEPAD_BUTTON_WEST;
|
||||
constexpr auto kGamepadButtonY = SDL_GAMEPAD_BUTTON_NORTH;
|
||||
constexpr auto kGamepadButtonBack = SDL_GAMEPAD_BUTTON_BACK;
|
||||
constexpr auto kGamepadButtonStart = SDL_GAMEPAD_BUTTON_START;
|
||||
constexpr auto kGamepadButtonLeftShoulder = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER;
|
||||
constexpr auto kGamepadButtonRightShoulder = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER;
|
||||
constexpr auto kGamepadButtonDpadUp = SDL_GAMEPAD_BUTTON_DPAD_UP;
|
||||
constexpr auto kGamepadButtonDpadDown = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
|
||||
constexpr auto kGamepadButtonDpadLeft = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
|
||||
constexpr auto kGamepadButtonDpadRight = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
|
||||
|
||||
constexpr auto kGamepadAxisLeftX = SDL_GAMEPAD_AXIS_LEFTX;
|
||||
constexpr auto kGamepadAxisLeftY = SDL_GAMEPAD_AXIS_LEFTY;
|
||||
#else
|
||||
using GamepadButton = SDL_GameControllerButton;
|
||||
using GamepadAxis = SDL_GameControllerAxis;
|
||||
|
||||
constexpr auto kGamepadButtonA = SDL_CONTROLLER_BUTTON_A;
|
||||
constexpr auto kGamepadButtonB = SDL_CONTROLLER_BUTTON_B;
|
||||
constexpr auto kGamepadButtonX = SDL_CONTROLLER_BUTTON_X;
|
||||
constexpr auto kGamepadButtonY = SDL_CONTROLLER_BUTTON_Y;
|
||||
constexpr auto kGamepadButtonBack = SDL_CONTROLLER_BUTTON_BACK;
|
||||
constexpr auto kGamepadButtonStart = SDL_CONTROLLER_BUTTON_START;
|
||||
constexpr auto kGamepadButtonLeftShoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
||||
constexpr auto kGamepadButtonRightShoulder = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
||||
constexpr auto kGamepadButtonDpadUp = SDL_CONTROLLER_BUTTON_DPAD_UP;
|
||||
constexpr auto kGamepadButtonDpadDown = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
|
||||
constexpr auto kGamepadButtonDpadLeft = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
|
||||
constexpr auto kGamepadButtonDpadRight = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
|
||||
|
||||
constexpr auto kGamepadAxisLeftX = SDL_CONTROLLER_AXIS_LEFTX;
|
||||
constexpr auto kGamepadAxisLeftY = SDL_CONTROLLER_AXIS_LEFTY;
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// Renderer Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Create a renderer with default settings.
|
||||
*
|
||||
* SDL2: SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED)
|
||||
* SDL3: SDL_CreateRenderer(window, nullptr)
|
||||
*/
|
||||
inline SDL_Renderer* CreateRenderer(SDL_Window* window) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return SDL_CreateRenderer(window, nullptr);
|
||||
#else
|
||||
return SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set vertical sync for the renderer.
|
||||
*
|
||||
* SDL2: VSync is set at renderer creation time via flags
|
||||
* SDL3: SDL_SetRenderVSync(renderer, interval)
|
||||
*/
|
||||
inline void SetRenderVSync(SDL_Renderer* renderer, int interval) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
SDL_SetRenderVSync(renderer, interval);
|
||||
#else
|
||||
// SDL2 sets vsync at creation time, this is a no-op
|
||||
(void)renderer;
|
||||
(void)interval;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Render a texture to the current render target.
|
||||
*
|
||||
* SDL2: SDL_RenderCopy(renderer, texture, srcrect, dstrect)
|
||||
* SDL3: SDL_RenderTexture(renderer, texture, srcrect, dstrect)
|
||||
*
|
||||
* Note: This version handles the int to float conversion for SDL3.
|
||||
*/
|
||||
inline bool RenderTexture(SDL_Renderer* renderer, SDL_Texture* texture,
|
||||
const SDL_Rect* srcrect, const SDL_Rect* dstrect) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
SDL_FRect src_frect, dst_frect;
|
||||
SDL_FRect* src_ptr = nullptr;
|
||||
SDL_FRect* dst_ptr = nullptr;
|
||||
|
||||
if (srcrect) {
|
||||
src_frect.x = static_cast<float>(srcrect->x);
|
||||
src_frect.y = static_cast<float>(srcrect->y);
|
||||
src_frect.w = static_cast<float>(srcrect->w);
|
||||
src_frect.h = static_cast<float>(srcrect->h);
|
||||
src_ptr = &src_frect;
|
||||
}
|
||||
|
||||
if (dstrect) {
|
||||
dst_frect.x = static_cast<float>(dstrect->x);
|
||||
dst_frect.y = static_cast<float>(dstrect->y);
|
||||
dst_frect.w = static_cast<float>(dstrect->w);
|
||||
dst_frect.h = static_cast<float>(dstrect->h);
|
||||
dst_ptr = &dst_frect;
|
||||
}
|
||||
|
||||
return SDL_RenderTexture(renderer, texture, src_ptr, dst_ptr);
|
||||
#else
|
||||
return SDL_RenderCopy(renderer, texture, srcrect, dstrect) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Surface Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Free/destroy a surface.
|
||||
*
|
||||
* SDL2: SDL_FreeSurface(surface)
|
||||
* SDL3: SDL_DestroySurface(surface)
|
||||
*/
|
||||
inline void FreeSurface(SDL_Surface* surface) {
|
||||
if (!surface) return;
|
||||
#ifdef YAZE_USE_SDL3
|
||||
SDL_DestroySurface(surface);
|
||||
#else
|
||||
SDL_FreeSurface(surface);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert a surface to a specific pixel format.
|
||||
*
|
||||
* SDL2: SDL_ConvertSurfaceFormat(surface, format, flags)
|
||||
* SDL3: SDL_ConvertSurface(surface, format)
|
||||
*/
|
||||
inline SDL_Surface* ConvertSurfaceFormat(SDL_Surface* surface, uint32_t format,
|
||||
uint32_t flags = 0) {
|
||||
if (!surface) return nullptr;
|
||||
#ifdef YAZE_USE_SDL3
|
||||
(void)flags; // SDL3 removed flags parameter
|
||||
return SDL_ConvertSurface(surface, format);
|
||||
#else
|
||||
return SDL_ConvertSurfaceFormat(surface, format, flags);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get bits per pixel from a surface.
|
||||
*
|
||||
* SDL2: surface->format->BitsPerPixel
|
||||
* SDL3: SDL_GetPixelFormatDetails(surface->format)->bits_per_pixel
|
||||
*/
|
||||
inline int GetSurfaceBitsPerPixel(SDL_Surface* surface) {
|
||||
if (!surface) return 0;
|
||||
#ifdef YAZE_USE_SDL3
|
||||
const SDL_PixelFormatDetails* details =
|
||||
SDL_GetPixelFormatDetails(surface->format);
|
||||
return details ? details->bits_per_pixel : 0;
|
||||
#else
|
||||
return surface->format ? surface->format->BitsPerPixel : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get bytes per pixel from a surface.
|
||||
*
|
||||
* SDL2: surface->format->BytesPerPixel
|
||||
* SDL3: SDL_GetPixelFormatDetails(surface->format)->bytes_per_pixel
|
||||
*/
|
||||
inline int GetSurfaceBytesPerPixel(SDL_Surface* surface) {
|
||||
if (!surface) return 0;
|
||||
#ifdef YAZE_USE_SDL3
|
||||
const SDL_PixelFormatDetails* details =
|
||||
SDL_GetPixelFormatDetails(surface->format);
|
||||
return details ? details->bytes_per_pixel : 0;
|
||||
#else
|
||||
return surface->format ? surface->format->BytesPerPixel : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Window Event Compatibility Macros
|
||||
// These macros allow code to handle window events consistently across SDL2/SDL3
|
||||
// ============================================================================
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
|
||||
// SDL3 has individual window events at the top level
|
||||
#define YAZE_SDL_QUIT SDL_EVENT_QUIT
|
||||
#define YAZE_SDL_WINDOWEVENT 0 // Placeholder - SDL3 has no combined event
|
||||
|
||||
// SDL3 window events are individual event types
|
||||
#define YAZE_SDL_WINDOW_CLOSE SDL_EVENT_WINDOW_CLOSE_REQUESTED
|
||||
#define YAZE_SDL_WINDOW_RESIZED SDL_EVENT_WINDOW_RESIZED
|
||||
#define YAZE_SDL_WINDOW_SIZE_CHANGED SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED
|
||||
#define YAZE_SDL_WINDOW_MINIMIZED SDL_EVENT_WINDOW_MINIMIZED
|
||||
#define YAZE_SDL_WINDOW_MAXIMIZED SDL_EVENT_WINDOW_MAXIMIZED
|
||||
#define YAZE_SDL_WINDOW_RESTORED SDL_EVENT_WINDOW_RESTORED
|
||||
#define YAZE_SDL_WINDOW_SHOWN SDL_EVENT_WINDOW_SHOWN
|
||||
#define YAZE_SDL_WINDOW_HIDDEN SDL_EVENT_WINDOW_HIDDEN
|
||||
#define YAZE_SDL_WINDOW_EXPOSED SDL_EVENT_WINDOW_EXPOSED
|
||||
#define YAZE_SDL_WINDOW_FOCUS_GAINED SDL_EVENT_WINDOW_FOCUS_GAINED
|
||||
#define YAZE_SDL_WINDOW_FOCUS_LOST SDL_EVENT_WINDOW_FOCUS_LOST
|
||||
|
||||
// SDL3 has no nested window events
|
||||
#define YAZE_SDL_HAS_INDIVIDUAL_WINDOW_EVENTS 1
|
||||
|
||||
#else // SDL2
|
||||
|
||||
// SDL2 event types
|
||||
#define YAZE_SDL_QUIT SDL_QUIT
|
||||
#define YAZE_SDL_WINDOWEVENT SDL_WINDOWEVENT
|
||||
|
||||
// SDL2 window events are nested under SDL_WINDOWEVENT
|
||||
#define YAZE_SDL_WINDOW_CLOSE SDL_WINDOWEVENT_CLOSE
|
||||
#define YAZE_SDL_WINDOW_RESIZED SDL_WINDOWEVENT_RESIZED
|
||||
#define YAZE_SDL_WINDOW_SIZE_CHANGED SDL_WINDOWEVENT_SIZE_CHANGED
|
||||
#define YAZE_SDL_WINDOW_MINIMIZED SDL_WINDOWEVENT_MINIMIZED
|
||||
#define YAZE_SDL_WINDOW_MAXIMIZED SDL_WINDOWEVENT_MAXIMIZED
|
||||
#define YAZE_SDL_WINDOW_RESTORED SDL_WINDOWEVENT_RESTORED
|
||||
#define YAZE_SDL_WINDOW_SHOWN SDL_WINDOWEVENT_SHOWN
|
||||
#define YAZE_SDL_WINDOW_HIDDEN SDL_WINDOWEVENT_HIDDEN
|
||||
#define YAZE_SDL_WINDOW_EXPOSED SDL_WINDOWEVENT_EXPOSED
|
||||
#define YAZE_SDL_WINDOW_FOCUS_GAINED SDL_WINDOWEVENT_FOCUS_GAINED
|
||||
#define YAZE_SDL_WINDOW_FOCUS_LOST SDL_WINDOWEVENT_FOCUS_LOST
|
||||
|
||||
// SDL2 uses nested window events
|
||||
#define YAZE_SDL_HAS_INDIVIDUAL_WINDOW_EVENTS 0
|
||||
|
||||
#endif // YAZE_USE_SDL3
|
||||
|
||||
// ============================================================================
|
||||
// Window Event Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Check if an event is a window close event
|
||||
* Works correctly for both SDL2 (nested) and SDL3 (individual) events
|
||||
*/
|
||||
inline bool IsWindowCloseEvent(const SDL_Event& event) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED;
|
||||
#else
|
||||
return event.type == SDL_WINDOWEVENT &&
|
||||
event.window.event == SDL_WINDOWEVENT_CLOSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an event is a window resize event
|
||||
*/
|
||||
inline bool IsWindowResizeEvent(const SDL_Event& event) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return event.type == SDL_EVENT_WINDOW_RESIZED ||
|
||||
event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
|
||||
#else
|
||||
return event.type == SDL_WINDOWEVENT &&
|
||||
(event.window.event == SDL_WINDOWEVENT_RESIZED ||
|
||||
event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an event is a window minimize event
|
||||
*/
|
||||
inline bool IsWindowMinimizedEvent(const SDL_Event& event) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return event.type == SDL_EVENT_WINDOW_MINIMIZED;
|
||||
#else
|
||||
return event.type == SDL_WINDOWEVENT &&
|
||||
event.window.event == SDL_WINDOWEVENT_MINIMIZED;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an event is a window restore event
|
||||
*/
|
||||
inline bool IsWindowRestoredEvent(const SDL_Event& event) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return event.type == SDL_EVENT_WINDOW_RESTORED;
|
||||
#else
|
||||
return event.type == SDL_WINDOWEVENT &&
|
||||
event.window.event == SDL_WINDOWEVENT_RESTORED;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get window width from resize event data
|
||||
*/
|
||||
inline int GetWindowEventWidth(const SDL_Event& event) {
|
||||
return event.window.data1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get window height from resize event data
|
||||
*/
|
||||
inline int GetWindowEventHeight(const SDL_Event& event) {
|
||||
return event.window.data2;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Initialization Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Check if SDL initialization succeeded.
|
||||
*
|
||||
* SDL2: Returns 0 on success
|
||||
* SDL3: Returns true (non-zero) on success
|
||||
*/
|
||||
inline bool InitSucceeded(int result) {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
// SDL3 returns bool (non-zero for success)
|
||||
return result != 0;
|
||||
#else
|
||||
// SDL2 returns 0 for success
|
||||
return result == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get recommended init flags.
|
||||
*
|
||||
* SDL3 removed SDL_INIT_TIMER (timer is always available).
|
||||
*/
|
||||
inline uint32_t GetDefaultInitFlags() {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS;
|
||||
#else
|
||||
return SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER | SDL_INIT_EVENTS;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_SDL_COMPAT_H_
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_CORE_TIMING_H
|
||||
#define YAZE_APP_CORE_TIMING_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_CORE_WINDOW_H_
|
||||
#define YAZE_CORE_WINDOW_H_
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
75
src/app/platform/window_backend_factory.cc
Normal file
75
src/app/platform/window_backend_factory.cc
Normal file
@@ -0,0 +1,75 @@
|
||||
// window_backend_factory.cc - Window Backend Factory Implementation
|
||||
|
||||
#include "app/platform/iwindow.h"
|
||||
|
||||
#include "app/platform/sdl2_window_backend.h"
|
||||
#include "util/log.h"
|
||||
|
||||
#ifdef YAZE_USE_SDL3
|
||||
#include "app/platform/sdl3_window_backend.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace platform {
|
||||
|
||||
std::unique_ptr<IWindowBackend> WindowBackendFactory::Create(
|
||||
WindowBackendType type) {
|
||||
switch (type) {
|
||||
case WindowBackendType::SDL2:
|
||||
#ifndef YAZE_USE_SDL3
|
||||
return std::make_unique<SDL2WindowBackend>();
|
||||
#else
|
||||
LOG_WARN("WindowBackendFactory",
|
||||
"SDL2 backend requested but built with SDL3, using SDL3");
|
||||
return std::make_unique<SDL3WindowBackend>();
|
||||
#endif
|
||||
|
||||
case WindowBackendType::SDL3:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return std::make_unique<SDL3WindowBackend>();
|
||||
#else
|
||||
LOG_WARN("WindowBackendFactory",
|
||||
"SDL3 backend requested but not available, using SDL2");
|
||||
return std::make_unique<SDL2WindowBackend>();
|
||||
#endif
|
||||
|
||||
case WindowBackendType::Auto:
|
||||
default:
|
||||
return Create(GetDefaultType());
|
||||
}
|
||||
}
|
||||
|
||||
WindowBackendType WindowBackendFactory::GetDefaultType() {
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return WindowBackendType::SDL3;
|
||||
#else
|
||||
return WindowBackendType::SDL2;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WindowBackendFactory::IsAvailable(WindowBackendType type) {
|
||||
switch (type) {
|
||||
case WindowBackendType::SDL2:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return false; // Built with SDL3, SDL2 not available
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
|
||||
case WindowBackendType::SDL3:
|
||||
#ifdef YAZE_USE_SDL3
|
||||
return true;
|
||||
#else
|
||||
return false; // SDL3 not built in
|
||||
#endif
|
||||
|
||||
case WindowBackendType::Auto:
|
||||
return true; // Auto always available
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
} // namespace yaze
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef YAZE_APP_ROM_H
|
||||
#define YAZE_APP_ROM_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
#include <zelda.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
@@ -47,7 +47,7 @@ grpc::Status ConvertStatus(const absl::Status& status) {
|
||||
break;
|
||||
}
|
||||
|
||||
return grpc::Status(code, std::string(status.message()));
|
||||
return grpc::Status(code, std::string(status.message().data(), status.message().size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
// Undefine Windows macros that conflict with protobuf generated code
|
||||
// SDL.h includes Windows.h on Windows, which defines these macros
|
||||
@@ -425,7 +425,7 @@ class ImGuiTestHarnessServiceGrpc final : public ImGuiTestHarness::Service {
|
||||
break;
|
||||
}
|
||||
|
||||
return grpc::Status(code, std::string(status.message()));
|
||||
return grpc::Status(code, std::string(status.message().data(), status.message().size()));
|
||||
}
|
||||
|
||||
ImGuiTestHarnessServiceImpl* impl_;
|
||||
@@ -1644,7 +1644,7 @@ absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
|
||||
} else {
|
||||
status = absl::InvalidArgumentError(
|
||||
absl::StrFormat("Unsupported action '%s'", step.action));
|
||||
step_message = std::string(status.message());
|
||||
step_message = std::string(status.message().data(), status.message().size());
|
||||
}
|
||||
|
||||
auto* assertion = response->add_assertions();
|
||||
@@ -1653,7 +1653,7 @@ absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
|
||||
|
||||
if (!status.ok()) {
|
||||
assertion->set_passed(false);
|
||||
assertion->set_error_message(std::string(status.message()));
|
||||
assertion->set_error_message(std::string(status.message().data(), status.message().size()));
|
||||
overall_success = false;
|
||||
overall_message = step_message;
|
||||
logs.push_back(absl::StrFormat(" Error: %s", status.message()));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
#include <SDL.h>
|
||||
#include "app/platform/sdl_compat.h"
|
||||
|
||||
// Undefine Windows macros that conflict with protobuf generated code
|
||||
// SDL.h includes Windows.h on Windows, which defines these macros
|
||||
|
||||
@@ -55,18 +55,13 @@ endif()
|
||||
# Link agent library if available (for z3ed test suites)
|
||||
# yaze_agent contains all the CLI service code (tile16_proposal_generator, gui_automation_client, etc.)
|
||||
if(TARGET yaze_agent)
|
||||
# Use whole-archive on Unix to ensure agent symbols (GuiAutomationClient etc) are included
|
||||
if(APPLE)
|
||||
target_link_options(yaze_test_support PUBLIC
|
||||
"LINKER:-force_load,$<TARGET_FILE:yaze_agent>")
|
||||
target_link_libraries(yaze_test_support PUBLIC yaze_agent)
|
||||
elseif(UNIX)
|
||||
target_link_libraries(yaze_test_support PUBLIC
|
||||
-Wl,--whole-archive yaze_agent -Wl,--no-whole-archive)
|
||||
else()
|
||||
# Windows: Normal linking
|
||||
target_link_libraries(yaze_test_support PUBLIC yaze_agent)
|
||||
endif()
|
||||
# Use normal linking to avoid circular dependencies
|
||||
# The previous force_load/whole-archive approach created a circular dependency:
|
||||
# yaze_test_support -> force_load(yaze_agent) -> yaze_test_support
|
||||
# This caused SIGSEGV during static initialization.
|
||||
# If specific agent symbols are not being pulled in, they should be explicitly
|
||||
# referenced in the test code or restructured into a separate test library.
|
||||
target_link_libraries(yaze_test_support PUBLIC yaze_agent)
|
||||
|
||||
if(YAZE_WITH_GRPC)
|
||||
message(STATUS "✓ z3ed test suites enabled with gRPC support")
|
||||
|
||||
Reference in New Issue
Block a user