backend-infra-engineer: Release v0.3.9-hotfix7 snapshot

This commit is contained in:
scawful
2025-11-23 13:37:10 -05:00
parent c8289bffda
commit 2934c82b75
202 changed files with 34914 additions and 845 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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>

View File

@@ -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");

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,
&current_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, &current_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(

View File

@@ -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();

View File

@@ -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

View File

@@ -1,6 +1,6 @@
#include "app/emu/audio/apu.h"
#include <SDL.h>
#include "app/platform/sdl_compat.h"
#include <cstdint>
#include <vector>

View File

@@ -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");

View File

@@ -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>

View 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

View 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

View 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

View 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_

View 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

View 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

View 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

View 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_

View 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

View 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_

View File

@@ -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();

View File

@@ -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

View File

@@ -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")

View File

@@ -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
}
}

View 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

View 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_

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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_;

View File

@@ -1,6 +1,6 @@
#pragma once
#include <SDL.h>
#include "app/platform/sdl_compat.h"
#include <memory>
#include <vector>

View 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_

View 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

View 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_

View File

@@ -1,6 +1,6 @@
#include "bitmap.h"
#include <SDL.h>
#include "app/platform/sdl_compat.h"
#include <cstdint>
#include <cstring> // for memcpy

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
#include "app/gfx/resource/arena.h"
#include <SDL.h>
#include "app/platform/sdl_compat.h"
#include <algorithm>

View File

@@ -1,6 +1,6 @@
#include "app/gfx/types/snes_palette.h"
#include <SDL.h>
#include "app/platform/sdl_compat.h"
#include <cstdint>
#include <cstdlib>

View File

@@ -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>

View File

@@ -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
View 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_

View File

@@ -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
View 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_

View 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

View 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_

View 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

View 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_

View 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_

View File

@@ -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>

View File

@@ -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>

View 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

View File

@@ -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>

View File

@@ -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

View File

@@ -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()));

View File

@@ -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

View File

@@ -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")