diff options
Diffstat (limited to 'src')
37 files changed, 1035 insertions, 74 deletions
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 5a2747e78..13b5e400e 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRCS hle/dsp.cpp hle/filter.cpp hle/pipe.cpp + hle/source.cpp interpolate.cpp sink_details.cpp ) @@ -15,6 +16,7 @@ set(HEADERS hle/dsp.h hle/filter.h hle/pipe.h + hle/source.h interpolate.h null_sink.h sink.h @@ -23,7 +25,18 @@ set(HEADERS include_directories(../../externals/soundtouch/include) +if(SDL2_FOUND) + set(SRCS ${SRCS} sdl2_sink.cpp) + set(HEADERS ${HEADERS} sdl2_sink.h) + include_directories(${SDL2_INCLUDE_DIR}) +endif() + create_directory_groups(${SRCS} ${HEADERS}) add_library(audio_core STATIC ${SRCS} ${HEADERS}) target_link_libraries(audio_core SoundTouch) + +if(SDL2_FOUND) + target_link_libraries(audio_core ${SDL2_LIBRARY}) + set_property(TARGET audio_core APPEND PROPERTY COMPILE_DEFINITIONS HAVE_SDL2) +endif() diff --git a/src/audio_core/hle/common.h b/src/audio_core/hle/common.h index 7910f42ae..596b67eaf 100644 --- a/src/audio_core/hle/common.h +++ b/src/audio_core/hle/common.h @@ -27,7 +27,7 @@ using QuadFrame32 = std::array<std::array<s32, 4>, samples_per_frame>; */ template<typename FrameT, typename FilterT> void FilterFrame(FrameT& frame, FilterT& filter) { - std::transform(frame.begin(), frame.end(), frame.begin(), [&filter](const typename FrameT::value_type& sample) { + std::transform(frame.begin(), frame.end(), frame.begin(), [&filter](const auto& sample) { return filter.ProcessSample(sample); }); } diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp index 4d44bd2d9..0cdbdb06a 100644 --- a/src/audio_core/hle/dsp.cpp +++ b/src/audio_core/hle/dsp.cpp @@ -2,10 +2,12 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <array> #include <memory> #include "audio_core/hle/dsp.h" #include "audio_core/hle/pipe.h" +#include "audio_core/hle/source.h" #include "audio_core/sink.h" namespace DSP { @@ -38,16 +40,38 @@ static SharedMemory& WriteRegion() { return g_regions[1 - CurrentRegionIndex()]; } +static std::array<Source, num_sources> sources = { + Source(0), Source(1), Source(2), Source(3), Source(4), Source(5), + Source(6), Source(7), Source(8), Source(9), Source(10), Source(11), + Source(12), Source(13), Source(14), Source(15), Source(16), Source(17), + Source(18), Source(19), Source(20), Source(21), Source(22), Source(23) +}; + static std::unique_ptr<AudioCore::Sink> sink; void Init() { DSP::HLE::ResetPipes(); + for (auto& source : sources) { + source.Reset(); + } } void Shutdown() { } bool Tick() { + SharedMemory& read = ReadRegion(); + SharedMemory& write = WriteRegion(); + + std::array<QuadFrame32, 3> intermediate_mixes = {}; + + for (size_t i = 0; i < num_sources; i++) { + write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]); + for (size_t mix = 0; mix < 3; mix++) { + sources[i].MixInto(intermediate_mixes[mix], mix); + } + } + return true; } diff --git a/src/audio_core/hle/dsp.h b/src/audio_core/hle/dsp.h index 4f2410c27..f6e53f68f 100644 --- a/src/audio_core/hle/dsp.h +++ b/src/audio_core/hle/dsp.h @@ -33,13 +33,9 @@ namespace HLE { // double-buffer. The frame counter is located as the very last u16 of each region and is incremented // each audio tick. -struct SharedMemory; - constexpr VAddr region0_base = 0x1FF50000; constexpr VAddr region1_base = 0x1FF70000; -extern std::array<SharedMemory, 2> g_regions; - /** * The DSP is native 16-bit. The DSP also appears to be big-endian. When reading 32-bit numbers from * its memory regions, the higher and lower 16-bit halves are swapped compared to the little-endian @@ -169,9 +165,9 @@ struct SourceConfiguration { float_le rate_multiplier; enum class InterpolationMode : u8 { - None = 0, + Polyphase = 0, Linear = 1, - Polyphase = 2 + None = 2 }; InterpolationMode interpolation_mode; @@ -318,10 +314,10 @@ ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20); struct SourceStatus { struct Status { u8 is_enabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) - u8 previous_buffer_id_dirty; ///< Non-zero when previous_buffer_id changes + u8 current_buffer_id_dirty; ///< Non-zero when current_buffer_id changes u16_le sync; ///< Is set by the DSP to the value of SourceConfiguration::sync u32_dsp buffer_position; ///< Number of samples into the current buffer - u16_le previous_buffer_id; ///< Updated when a buffer finishes playing + u16_le current_buffer_id; ///< Updated when a buffer finishes playing INSERT_PADDING_DSPWORDS(1); }; @@ -507,6 +503,8 @@ struct SharedMemory { }; ASSERT_DSP_STRUCT(SharedMemory, 0x8000); +extern std::array<SharedMemory, 2> g_regions; + // Structures must have an offset that is a multiple of two. static_assert(offsetof(SharedMemory, frame_counter) % 2 == 0, "Structures in DSP::HLE::SharedMemory must be 2-byte aligned"); static_assert(offsetof(SharedMemory, source_configurations) % 2 == 0, "Structures in DSP::HLE::SharedMemory must be 2-byte aligned"); diff --git a/src/audio_core/hle/filter.h b/src/audio_core/hle/filter.h index 75738f600..43d2035cd 100644 --- a/src/audio_core/hle/filter.h +++ b/src/audio_core/hle/filter.h @@ -16,6 +16,7 @@ namespace HLE { /// Preprocessing filters. There is an independent set of filters for each Source. class SourceFilters final { +public: SourceFilters() { Reset(); } /// Reset internal state. diff --git a/src/audio_core/hle/source.cpp b/src/audio_core/hle/source.cpp new file mode 100644 index 000000000..daaf6e3f3 --- /dev/null +++ b/src/audio_core/hle/source.cpp @@ -0,0 +1,320 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <array> + +#include "audio_core/codec.h" +#include "audio_core/hle/common.h" +#include "audio_core/hle/source.h" +#include "audio_core/interpolate.h" + +#include "common/assert.h" +#include "common/logging/log.h" + +#include "core/memory.h" + +namespace DSP { +namespace HLE { + +SourceStatus::Status Source::Tick(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]) { + ParseConfig(config, adpcm_coeffs); + + if (state.enabled) { + GenerateFrame(); + } + + return GetCurrentStatus(); +} + +void Source::MixInto(QuadFrame32& dest, size_t intermediate_mix_id) const { + if (!state.enabled) + return; + + const std::array<float, 4>& gains = state.gain.at(intermediate_mix_id); + for (size_t samplei = 0; samplei < samples_per_frame; samplei++) { + // Conversion from stereo (current_frame) to quadraphonic (dest) occurs here. + dest[samplei][0] += static_cast<s32>(gains[0] * current_frame[samplei][0]); + dest[samplei][1] += static_cast<s32>(gains[1] * current_frame[samplei][1]); + dest[samplei][2] += static_cast<s32>(gains[2] * current_frame[samplei][0]); + dest[samplei][3] += static_cast<s32>(gains[3] * current_frame[samplei][1]); + } +} + +void Source::Reset() { + current_frame.fill({}); + state = {}; +} + +void Source::ParseConfig(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]) { + if (!config.dirty_raw) { + return; + } + + if (config.reset_flag) { + config.reset_flag.Assign(0); + Reset(); + LOG_TRACE(Audio_DSP, "source_id=%zu reset", source_id); + } + + if (config.partial_reset_flag) { + config.partial_reset_flag.Assign(0); + state.input_queue = std::priority_queue<Buffer, std::vector<Buffer>, BufferOrder>{}; + LOG_TRACE(Audio_DSP, "source_id=%zu partial_reset", source_id); + } + + if (config.enable_dirty) { + config.enable_dirty.Assign(0); + state.enabled = config.enable != 0; + LOG_TRACE(Audio_DSP, "source_id=%zu enable=%d", source_id, state.enabled); + } + + if (config.sync_dirty) { + config.sync_dirty.Assign(0); + state.sync = config.sync; + LOG_TRACE(Audio_DSP, "source_id=%zu sync=%u", source_id, state.sync); + } + + if (config.rate_multiplier_dirty) { + config.rate_multiplier_dirty.Assign(0); + state.rate_multiplier = config.rate_multiplier; + LOG_TRACE(Audio_DSP, "source_id=%zu rate=%f", source_id, state.rate_multiplier); + + if (state.rate_multiplier <= 0) { + LOG_ERROR(Audio_DSP, "Was given an invalid rate multiplier: source_id=%zu rate=%f", source_id, state.rate_multiplier); + state.rate_multiplier = 1.0f; + // Note: Actual firmware starts producing garbage if this occurs. + } + } + + if (config.adpcm_coefficients_dirty) { + config.adpcm_coefficients_dirty.Assign(0); + std::transform(adpcm_coeffs, adpcm_coeffs + state.adpcm_coeffs.size(), state.adpcm_coeffs.begin(), + [](const auto& coeff) { return static_cast<s16>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu adpcm update", source_id); + } + + if (config.gain_0_dirty) { + config.gain_0_dirty.Assign(0); + std::transform(config.gain[0], config.gain[0] + state.gain[0].size(), state.gain[0].begin(), + [](const auto& coeff) { return static_cast<float>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu gain 0 update", source_id); + } + + if (config.gain_1_dirty) { + config.gain_1_dirty.Assign(0); + std::transform(config.gain[1], config.gain[1] + state.gain[1].size(), state.gain[1].begin(), + [](const auto& coeff) { return static_cast<float>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu gain 1 update", source_id); + } + + if (config.gain_2_dirty) { + config.gain_2_dirty.Assign(0); + std::transform(config.gain[2], config.gain[2] + state.gain[2].size(), state.gain[2].begin(), + [](const auto& coeff) { return static_cast<float>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu gain 2 update", source_id); + } + + if (config.filters_enabled_dirty) { + config.filters_enabled_dirty.Assign(0); + state.filters.Enable(config.simple_filter_enabled.ToBool(), config.biquad_filter_enabled.ToBool()); + LOG_TRACE(Audio_DSP, "source_id=%zu enable_simple=%hu enable_biquad=%hu", + source_id, config.simple_filter_enabled.Value(), config.biquad_filter_enabled.Value()); + } + + if (config.simple_filter_dirty) { + config.simple_filter_dirty.Assign(0); + state.filters.Configure(config.simple_filter); + LOG_TRACE(Audio_DSP, "source_id=%zu simple filter update"); + } + + if (config.biquad_filter_dirty) { + config.biquad_filter_dirty.Assign(0); + state.filters.Configure(config.biquad_filter); + LOG_TRACE(Audio_DSP, "source_id=%zu biquad filter update"); + } + + if (config.interpolation_dirty) { + config.interpolation_dirty.Assign(0); + state.interpolation_mode = config.interpolation_mode; + LOG_TRACE(Audio_DSP, "source_id=%zu interpolation_mode=%zu", source_id, static_cast<size_t>(state.interpolation_mode)); + } + + if (config.format_dirty || config.embedded_buffer_dirty) { + config.format_dirty.Assign(0); + state.format = config.format; + LOG_TRACE(Audio_DSP, "source_id=%zu format=%zu", source_id, static_cast<size_t>(state.format)); + } + + if (config.mono_or_stereo_dirty || config.embedded_buffer_dirty) { + config.mono_or_stereo_dirty.Assign(0); + state.mono_or_stereo = config.mono_or_stereo; + LOG_TRACE(Audio_DSP, "source_id=%zu mono_or_stereo=%zu", source_id, static_cast<size_t>(state.mono_or_stereo)); + } + + if (config.embedded_buffer_dirty) { + config.embedded_buffer_dirty.Assign(0); + state.input_queue.emplace(Buffer{ + config.physical_address, + config.length, + static_cast<u8>(config.adpcm_ps), + { config.adpcm_yn[0], config.adpcm_yn[1] }, + config.adpcm_dirty.ToBool(), + config.is_looping.ToBool(), + config.buffer_id, + state.mono_or_stereo, + state.format, + false + }); + LOG_TRACE(Audio_DSP, "enqueuing embedded addr=0x%08x len=%u id=%hu", config.physical_address, config.length, config.buffer_id); + } + + if (config.buffer_queue_dirty) { + config.buffer_queue_dirty.Assign(0); + for (size_t i = 0; i < 4; i++) { + if (config.buffers_dirty & (1 << i)) { + const auto& b = config.buffers[i]; + state.input_queue.emplace(Buffer{ + b.physical_address, + b.length, + static_cast<u8>(b.adpcm_ps), + { b.adpcm_yn[0], b.adpcm_yn[1] }, + b.adpcm_dirty != 0, + b.is_looping != 0, + b.buffer_id, + state.mono_or_stereo, + state.format, + true + }); + LOG_TRACE(Audio_DSP, "enqueuing queued %zu addr=0x%08x len=%u id=%hu", i, b.physical_address, b.length, b.buffer_id); + } + } + config.buffers_dirty = 0; + } + + if (config.dirty_raw) { + LOG_DEBUG(Audio_DSP, "source_id=%zu remaining_dirty=%x", source_id, config.dirty_raw); + } + + config.dirty_raw = 0; +} + +void Source::GenerateFrame() { + current_frame.fill({}); + + if (state.current_buffer.empty() && !DequeueBuffer()) { + state.enabled = false; + state.buffer_update = true; + state.current_buffer_id = 0; + return; + } + + size_t frame_position = 0; + + state.current_sample_number = state.next_sample_number; + while (frame_position < current_frame.size()) { + if (state.current_buffer.empty() && !DequeueBuffer()) { + break; + } + + const size_t size_to_copy = std::min(state.current_buffer.size(), current_frame.size() - frame_position); + + std::copy(state.current_buffer.begin(), state.current_buffer.begin() + size_to_copy, current_frame.begin() + frame_position); + state.current_buffer.erase(state.current_buffer.begin(), state.current_buffer.begin() + size_to_copy); + + frame_position += size_to_copy; + state.next_sample_number += static_cast<u32>(size_to_copy); + } + + state.filters.ProcessFrame(current_frame); +} + + +bool Source::DequeueBuffer() { + ASSERT_MSG(state.current_buffer.empty(), "Shouldn't dequeue; we still have data in current_buffer"); + + if (state.input_queue.empty()) + return false; + + const Buffer buf = state.input_queue.top(); + state.input_queue.pop(); + + if (buf.adpcm_dirty) { + state.adpcm_state.yn1 = buf.adpcm_yn[0]; + state.adpcm_state.yn2 = buf.adpcm_yn[1]; + } + + if (buf.is_looping) { + LOG_ERROR(Audio_DSP, "Looped buffers are unimplemented at the moment"); + } + + const u8* const memory = Memory::GetPhysicalPointer(buf.physical_address); + if (memory) { + const unsigned num_channels = buf.mono_or_stereo == MonoOrStereo::Stereo ? 2 : 1; + switch (buf.format) { + case Format::PCM8: + state.current_buffer = Codec::DecodePCM8(num_channels, memory, buf.length); + break; + case Format::PCM16: + state.current_buffer = Codec::DecodePCM16(num_channels, memory, buf.length); + break; + case Format::ADPCM: + DEBUG_ASSERT(num_channels == 1); + state.current_buffer = Codec::DecodeADPCM(memory, buf.length, state.adpcm_coeffs, state.adpcm_state); + break; + default: + UNIMPLEMENTED(); + break; + } + } else { + LOG_WARNING(Audio_DSP, "source_id=%zu buffer_id=%hu length=%u: Invalid physical address 0x%08X", + source_id, buf.buffer_id, buf.length, buf.physical_address); + state.current_buffer.clear(); + return true; + } + + switch (state.interpolation_mode) { + case InterpolationMode::None: + state.current_buffer = AudioInterp::None(state.interp_state, state.current_buffer, state.rate_multiplier); + break; + case InterpolationMode::Linear: + state.current_buffer = AudioInterp::Linear(state.interp_state, state.current_buffer, state.rate_multiplier); + break; + case InterpolationMode::Polyphase: + // TODO(merry): Implement polyphase interpolation + state.current_buffer = AudioInterp::Linear(state.interp_state, state.current_buffer, state.rate_multiplier); + break; + default: + UNIMPLEMENTED(); + break; + } + + state.current_sample_number = 0; + state.next_sample_number = 0; + state.current_buffer_id = buf.buffer_id; + state.buffer_update = buf.from_queue; + + LOG_TRACE(Audio_DSP, "source_id=%zu buffer_id=%hu from_queue=%s current_buffer.size()=%zu", + source_id, buf.buffer_id, buf.from_queue ? "true" : "false", state.current_buffer.size()); + return true; +} + +SourceStatus::Status Source::GetCurrentStatus() { + SourceStatus::Status ret; + + // Applications depend on the correct emulation of + // current_buffer_id_dirty and current_buffer_id to synchronise + // audio with video. + ret.is_enabled = state.enabled; + ret.current_buffer_id_dirty = state.buffer_update ? 1 : 0; + state.buffer_update = false; + ret.current_buffer_id = state.current_buffer_id; + ret.buffer_position = state.current_sample_number; + ret.sync = state.sync; + + return ret; +} + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/source.h b/src/audio_core/hle/source.h new file mode 100644 index 000000000..7ee08d424 --- /dev/null +++ b/src/audio_core/hle/source.h @@ -0,0 +1,144 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <queue> +#include <vector> + +#include "audio_core/codec.h" +#include "audio_core/hle/common.h" +#include "audio_core/hle/dsp.h" +#include "audio_core/hle/filter.h" +#include "audio_core/interpolate.h" + +#include "common/common_types.h" + +namespace DSP { +namespace HLE { + +/** + * This module performs: + * - Buffer management + * - Decoding of buffers + * - Buffer resampling and interpolation + * - Per-source filtering (SimpleFilter, BiquadFilter) + * - Per-source gain + * - Other per-source processing + */ +class Source final { +public: + explicit Source(size_t source_id_) : source_id(source_id_) { + Reset(); + } + + /// Resets internal state. + void Reset(); + + /** + * This is called once every audio frame. This performs per-source processing every frame. + * @param config The new configuration we've got for this Source from the application. + * @param adpcm_coeffs ADPCM coefficients to use if config tells us to use them (may contain invalid values otherwise). + * @return The current status of this Source. This is given back to the emulated application via SharedMemory. + */ + SourceStatus::Status Tick(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]); + + /** + * Mix this source's output into dest, using the gains for the `intermediate_mix_id`-th intermediate mixer. + * @param dest The QuadFrame32 to mix into. + * @param intermediate_mix_id The id of the intermediate mix whose gains we are using. + */ + void MixInto(QuadFrame32& dest, size_t intermediate_mix_id) const; + +private: + const size_t source_id; + StereoFrame16 current_frame; + + using Format = SourceConfiguration::Configuration::Format; + using InterpolationMode = SourceConfiguration::Configuration::InterpolationMode; + using MonoOrStereo = SourceConfiguration::Configuration::MonoOrStereo; + + /// Internal representation of a buffer for our buffer queue + struct Buffer { + PAddr physical_address; + u32 length; + u8 adpcm_ps; + std::array<u16, 2> adpcm_yn; + bool adpcm_dirty; + bool is_looping; + u16 buffer_id; + + MonoOrStereo mono_or_stereo; + Format format; + + bool from_queue; + }; + + struct BufferOrder { + bool operator() (const Buffer& a, const Buffer& b) const { + // Lower buffer_id comes first. + return a.buffer_id > b.buffer_id; + } + }; + + struct { + + // State variables + + bool enabled = false; + u16 sync = 0; + + // Mixing + + std::array<std::array<float, 4>, 3> gain = {}; + + // Buffer queue + + std::priority_queue<Buffer, std::vector<Buffer>, BufferOrder> input_queue; + MonoOrStereo mono_or_stereo = MonoOrStereo::Mono; + Format format = Format::ADPCM; + + // Current buffer + + u32 current_sample_number = 0; + u32 next_sample_number = 0; + std::vector<std::array<s16, 2>> current_buffer; + + // buffer_id state + + bool buffer_update = false; + u32 current_buffer_id = 0; + + // Decoding state + + std::array<s16, 16> adpcm_coeffs = {}; + Codec::ADPCMState adpcm_state = {}; + + // Resampling state + + float rate_multiplier = 1.0; + InterpolationMode interpolation_mode = InterpolationMode::Polyphase; + AudioInterp::State interp_state = {}; + + // Filter state + + SourceFilters filters; + + } state; + + // Internal functions + + /// INTERNAL: Update our internal state based on the current config. + void ParseConfig(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]); + /// INTERNAL: Generate the current audio output for this frame based on our internal state. + void GenerateFrame(); + /// INTERNAL: Dequeues a buffer and does preprocessing on it (decoding, resampling). Puts it into current_buffer. + bool DequeueBuffer(); + /// INTERNAL: Generates a SourceStatus::Status based on our internal state. + SourceStatus::Status GetCurrentStatus(); +}; + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/sdl2_sink.cpp b/src/audio_core/sdl2_sink.cpp new file mode 100644 index 000000000..dc75c04ee --- /dev/null +++ b/src/audio_core/sdl2_sink.cpp @@ -0,0 +1,126 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <list> +#include <vector> + +#include <SDL.h> + +#include "audio_core/audio_core.h" +#include "audio_core/sdl2_sink.h" + +#include "common/assert.h" +#include "common/logging/log.h" +#include <numeric> + +namespace AudioCore { + +struct SDL2Sink::Impl { + unsigned int sample_rate = 0; + + SDL_AudioDeviceID audio_device_id = 0; + + std::list<std::vector<s16>> queue; + + static void Callback(void* impl_, u8* buffer, int buffer_size_in_bytes); +}; + +SDL2Sink::SDL2Sink() : impl(std::make_unique<Impl>()) { + if (SDL_Init(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_Init(SDL_INIT_AUDIO) failed"); + impl->audio_device_id = 0; + return; + } + + SDL_AudioSpec desired_audiospec; + SDL_zero(desired_audiospec); + desired_audiospec.format = AUDIO_S16; + desired_audiospec.channels = 2; + desired_audiospec.freq = native_sample_rate; + desired_audiospec.samples = 1024; + desired_audiospec.userdata = impl.get(); + desired_audiospec.callback = &Impl::Callback; + + SDL_AudioSpec obtained_audiospec; + SDL_zero(obtained_audiospec); + + impl->audio_device_id = SDL_OpenAudioDevice(nullptr, false, &desired_audiospec, &obtained_audiospec, 0); + if (impl->audio_device_id <= 0) { + LOG_CRITICAL(Audio_Sink, "SDL_OpenAudioDevice failed"); + return; + } + + impl->sample_rate = obtained_audiospec.freq; + + // SDL2 audio devices start out paused, unpause it: + SDL_PauseAudioDevice(impl->audio_device_id, 0); +} + +SDL2Sink::~SDL2Sink() { + if (impl->audio_device_id <= 0) + return; + + SDL_CloseAudioDevice(impl->audio_device_id); +} + +unsigned int SDL2Sink::GetNativeSampleRate() const { + if (impl->audio_device_id <= 0) + return native_sample_rate; + + return impl->sample_rate; +} + +void SDL2Sink::EnqueueSamples(const std::vector<s16>& samples) { + if (impl->audio_device_id <= 0) + return; + + ASSERT_MSG(samples.size() % 2 == 0, "Samples must be in interleaved stereo PCM16 format (size must be a multiple of two)"); + + SDL_LockAudioDevice(impl->audio_device_id); + impl->queue.emplace_back(samples); + SDL_UnlockAudioDevice(impl->audio_device_id); +} + +size_t SDL2Sink::SamplesInQueue() const { + if (impl->audio_device_id <= 0) + return 0; + + SDL_LockAudioDevice(impl->audio_device_id); + + size_t total_size = std::accumulate(impl->queue.begin(), impl->queue.end(), static_cast<size_t>(0), + [](size_t sum, const auto& buffer) { + // Division by two because each stereo sample is made of two s16. + return sum + buffer.size() / 2; + }); + + SDL_UnlockAudioDevice(impl->audio_device_id); + + return total_size; +} + +void SDL2Sink::Impl::Callback(void* impl_, u8* buffer, int buffer_size_in_bytes) { + Impl* impl = reinterpret_cast<Impl*>(impl_); + + size_t remaining_size = static_cast<size_t>(buffer_size_in_bytes) / sizeof(s16); // Keep track of size in 16-bit increments. + + while (remaining_size > 0 && !impl->queue.empty()) { + if (impl->queue.front().size() <= remaining_size) { + memcpy(buffer, impl->queue.front().data(), impl->queue.front().size() * sizeof(s16)); + buffer += impl->queue.front().size() * sizeof(s16); + remaining_size -= impl->queue.front().size(); + impl->queue.pop_front(); + } else { + memcpy(buffer, impl->queue.front().data(), remaining_size * sizeof(s16)); + buffer += remaining_size * sizeof(s16); + impl->queue.front().erase(impl->queue.front().begin(), impl->queue.front().begin() + remaining_size); + remaining_size = 0; + } + } + + if (remaining_size > 0) { + memset(buffer, 0, remaining_size * sizeof(s16)); + } +} + +} // namespace AudioCore diff --git a/src/audio_core/sdl2_sink.h b/src/audio_core/sdl2_sink.h new file mode 100644 index 000000000..0f296b673 --- /dev/null +++ b/src/audio_core/sdl2_sink.h @@ -0,0 +1,30 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <cstddef> +#include <memory> + +#include "audio_core/sink.h" + +namespace AudioCore { + +class SDL2Sink final : public Sink { +public: + SDL2Sink(); + ~SDL2Sink() override; + + unsigned int GetNativeSampleRate() const override; + + void EnqueueSamples(const std::vector<s16>& samples) override; + + size_t SamplesInQueue() const override; + +private: + struct Impl; + std::unique_ptr<Impl> impl; +}; + +} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h index cad21a85e..1c881c3d2 100644 --- a/src/audio_core/sink.h +++ b/src/audio_core/sink.h @@ -19,7 +19,7 @@ public: virtual ~Sink() = default; /// The native rate of this sink. The sink expects to be fed samples that respect this. (Units: samples/sec) - virtual unsigned GetNativeSampleRate() const = 0; + virtual unsigned int GetNativeSampleRate() const = 0; /** * Feed stereo samples to sink. diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index d2cc74103..ba5e83d17 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -8,10 +8,17 @@ #include "audio_core/null_sink.h" #include "audio_core/sink_details.h" +#ifdef HAVE_SDL2 +#include "audio_core/sdl2_sink.h" +#endif + namespace AudioCore { // g_sink_details is ordered in terms of desirability, with the best choice at the top. const std::vector<SinkDetails> g_sink_details = { +#ifdef HAVE_SDL2 + { "sdl2", []() { return std::make_unique<SDL2Sink>(); } }, +#endif { "null", []() { return std::make_unique<NullSink>(); } }, }; diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 0e6171736..49126356f 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -58,7 +58,7 @@ bg_green = [Audio] # Which audio output engine to use. -# auto (default): Auto-select, null: No audio output +# auto (default): Auto-select, null: No audio output, sdl2: SDL2 (if available) output_engine = [Data Storage] diff --git a/src/citra/emu_window/emu_window_sdl2.cpp b/src/citra/emu_window/emu_window_sdl2.cpp index 924189f4c..12cdd9d95 100644 --- a/src/citra/emu_window/emu_window_sdl2.cpp +++ b/src/citra/emu_window/emu_window_sdl2.cpp @@ -9,6 +9,8 @@ #define SDL_MAIN_HANDLED #include <SDL.h> +#include <glad/glad.h> + #include "common/key_map.h" #include "common/logging/log.h" #include "common/scm_rev.h" @@ -98,6 +100,11 @@ EmuWindow_SDL2::EmuWindow_SDL2() { exit(1); } + if (!gladLoadGLLoader(static_cast<GLADloadproc>(SDL_GL_GetProcAddress))) { + LOG_CRITICAL(Frontend, "Failed to initialize GL functions! Exiting..."); + exit(1); + } + OnResize(); OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); SDL_PumpEvents(); diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index cc9e0c624..3f0099200 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -55,6 +55,7 @@ set(HEADERS configure_dialog.h configure_general.h game_list.h + game_list_p.h hotkeys.h main.h ui_settings.h diff --git a/src/citra_qt/debugger/graphics_breakpoints.cpp b/src/citra_qt/debugger/graphics_breakpoints.cpp index c8510128a..fe66918a8 100644 --- a/src/citra_qt/debugger/graphics_breakpoints.cpp +++ b/src/citra_qt/debugger/graphics_breakpoints.cpp @@ -44,7 +44,7 @@ QVariant BreakPointModel::data(const QModelIndex& index, int role) const { Pica::DebugContext::Event::PicaCommandProcessed, tr("Pica command processed") }, { Pica::DebugContext::Event::IncomingPrimitiveBatch, tr("Incoming primitive batch") }, { Pica::DebugContext::Event::FinishedPrimitiveBatch, tr("Finished primitive batch") }, - { Pica::DebugContext::Event::VertexLoaded, tr("Vertex loaded") }, + { Pica::DebugContext::Event::VertexShaderInvocation, tr("Vertex shader invocation") }, { Pica::DebugContext::Event::IncomingDisplayTransfer, tr("Incoming display transfer") }, { Pica::DebugContext::Event::GSPCommandProcessed, tr("GSP command processed") }, { Pica::DebugContext::Event::BufferSwapped, tr("Buffers swapped") } diff --git a/src/citra_qt/debugger/graphics_vertex_shader.cpp b/src/citra_qt/debugger/graphics_vertex_shader.cpp index d648d4640..6e8d7ef42 100644 --- a/src/citra_qt/debugger/graphics_vertex_shader.cpp +++ b/src/citra_qt/debugger/graphics_vertex_shader.cpp @@ -365,7 +365,7 @@ GraphicsVertexShaderWidget::GraphicsVertexShaderWidget(std::shared_ptr< Pica::De input_data[i]->setValidator(new QDoubleValidator(input_data[i])); } - breakpoint_warning = new QLabel(tr("(data only available at VertexLoaded breakpoints)")); + breakpoint_warning = new QLabel(tr("(data only available at vertex shader invocation breakpoints)")); // TODO: Add some button for jumping to the shader entry point @@ -454,7 +454,7 @@ GraphicsVertexShaderWidget::GraphicsVertexShaderWidget(std::shared_ptr< Pica::De void GraphicsVertexShaderWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) { auto input = static_cast<Pica::Shader::InputVertex*>(data); - if (event == Pica::DebugContext::Event::VertexLoaded) { + if (event == Pica::DebugContext::Event::VertexShaderInvocation) { Reload(true, data); } else { // No vertex data is retrievable => invalidate currently stored vertex data diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index d14532102..d4ac9c96e 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -34,8 +34,8 @@ GameList::GameList(QWidget* parent) tree_view->setUniformRowHeights(true); item_model->insertColumns(0, COLUMN_COUNT); - item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type"); item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name"); + item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type"); item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size"); connect(tree_view, SIGNAL(activated(const QModelIndex&)), this, SLOT(ValidateEntry(const QModelIndex&))); @@ -109,7 +109,11 @@ void GameList::SaveInterfaceLayout() void GameList::LoadInterfaceLayout() { auto header = tree_view->header(); - header->restoreState(UISettings::values.gamelist_header_state); + if (!header->restoreState(UISettings::values.gamelist_header_state)) { + // We are using the name column to display icons and titles + // so make it as large as possible as default. + header->resizeSection(COLUMN_NAME, header->width()); + } item_model->sort(header->sortIndicatorSection(), header->sortIndicatorOrder()); } @@ -143,9 +147,15 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, bool d LOG_WARNING(Frontend, "Filetype and extension of file %s do not match.", physical_name.c_str()); } + std::vector<u8> smdh; + std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(FileUtil::IOFile(physical_name, "rb"), filetype, filename_filename, physical_name); + + if (loader) + loader->ReadIcon(smdh); + emit EntryReady({ + new GameListItemPath(QString::fromStdString(physical_name), smdh), new GameListItem(QString::fromStdString(Loader::GetFileTypeString(filetype))), - new GameListItemPath(QString::fromStdString(physical_name)), new GameListItemSize(FileUtil::GetSize(physical_name)), }); } diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index 48febdc60..198674f04 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -20,8 +20,8 @@ class GameList : public QWidget { public: enum { - COLUMN_FILE_TYPE, COLUMN_NAME, + COLUMN_FILE_TYPE, COLUMN_SIZE, COLUMN_COUNT, // Number of columns }; diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index 820012bce..284f5da81 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -6,13 +6,85 @@ #include <atomic> +#include <QImage> #include <QRunnable> #include <QStandardItem> #include <QString> #include "citra_qt/util/util.h" #include "common/string_util.h" +#include "common/color.h" +#include "core/loader/loader.h" + +#include "video_core/utils.h" + +/** + * Tests if data is a valid SMDH by its length and magic number. + * @param smdh_data data buffer to test + * @return bool test result + */ +static bool IsValidSMDH(const std::vector<u8>& smdh_data) { + if (smdh_data.size() < sizeof(Loader::SMDH)) + return false; + + u32 magic; + memcpy(&magic, smdh_data.data(), 4); + + return Loader::MakeMagic('S', 'M', 'D', 'H') == magic; +} + +/** + * Gets game icon from SMDH + * @param sdmh SMDH data + * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) + * @return QPixmap game icon + */ +static QPixmap GetIconFromSMDH(const Loader::SMDH& smdh, bool large) { + u32 size; + const u8* icon_data; + + if (large) { + size = 48; + icon_data = smdh.large_icon.data(); + } else { + size = 24; + icon_data = smdh.small_icon.data(); + } + + QImage icon(size, size, QImage::Format::Format_RGB888); + for (u32 x = 0; x < size; ++x) { + for (u32 y = 0; y < size; ++y) { + u32 coarse_y = y & ~7; + auto v = Color::DecodeRGB565( + icon_data + VideoCore::GetMortonOffset(x, y, 2) + coarse_y * size * 2); + icon.setPixel(x, y, qRgb(v.r(), v.g(), v.b())); + } + } + return QPixmap::fromImage(icon); +} + +/** + * Gets the default icon (for games without valid SMDH) + * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) + * @return QPixmap default icon + */ +static QPixmap GetDefaultIcon(bool large) { + int size = large ? 48 : 24; + QPixmap icon(size, size); + icon.fill(Qt::transparent); + return icon; +} + +/** + * Gets the short game title fromn SMDH + * @param sdmh SMDH data + * @param language title language + * @return QString short title + */ +static QString GetShortTitleFromSMDH(const Loader::SMDH& smdh, Loader::SMDH::TitleLanguage language) { + return QString::fromUtf16(smdh.titles[static_cast<int>(language)].short_title.data()); +} class GameListItem : public QStandardItem { @@ -27,29 +99,43 @@ public: * A specialization of GameListItem for path values. * This class ensures that for every full path value it holds, a correct string representation * of just the filename (with no extension) will be displayed to the user. + * If this class recieves valid SMDH data, it will also display game icons and titles. */ class GameListItemPath : public GameListItem { public: static const int FullPathRole = Qt::UserRole + 1; + static const int TitleRole = Qt::UserRole + 2; GameListItemPath(): GameListItem() {} - GameListItemPath(const QString& game_path): GameListItem() + GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data): GameListItem() { setData(game_path, FullPathRole); + + if (!IsValidSMDH(smdh_data)) { + // SMDH is not valid, set a default icon + setData(GetDefaultIcon(true), Qt::DecorationRole); + return; + } + + Loader::SMDH smdh; + memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + // Get icon from SMDH + setData(GetIconFromSMDH(smdh, true), Qt::DecorationRole); + + // Get title form SMDH + setData(GetShortTitleFromSMDH(smdh, Loader::SMDH::TitleLanguage::English), TitleRole); } - void setData(const QVariant& value, int role) override - { - // By specializing setData for FullPathRole, we can ensure that the two string - // representations of the data are always accurate and in the correct format. - if (role == FullPathRole) { + QVariant data(int role) const override { + if (role == Qt::DisplayRole) { std::string filename; - Common::SplitPath(value.toString().toStdString(), nullptr, &filename, nullptr); - GameListItem::setData(QString::fromStdString(filename), Qt::DisplayRole); - GameListItem::setData(value, FullPathRole); + Common::SplitPath(data(FullPathRole).toString().toStdString(), nullptr, &filename, nullptr); + QString title = data(TitleRole).toString(); + return QString::fromStdString(filename) + (title.isEmpty() ? "" : "\n " + title); } else { - GameListItem::setData(value, role); + return GameListItem::data(role); } } }; diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index f1ab29755..a85c94a4b 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -6,6 +6,9 @@ #include <memory> #include <thread> +#include <glad/glad.h> + +#define QT_NO_OPENGL #include <QDesktopWidget> #include <QtGui> #include <QFileDialog> @@ -240,6 +243,14 @@ bool GMainWindow::InitializeSystem() { if (emu_thread != nullptr) ShutdownGame(); + render_window->MakeCurrent(); + if (!gladLoadGL()) { + QMessageBox::critical(this, tr("Error while starting Citra!"), + tr("Failed to initialize the video core!\n\n" + "Please ensure that your GPU supports OpenGL 3.3 and that you have the latest graphics driver.")); + return false; + } + // Initialize the core emulation System::Result system_result = System::Init(render_window); if (System::Result::Success != system_result) { diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 3d39f94d5..d7008fc66 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -65,6 +65,7 @@ namespace Log { SUB(Render, OpenGL) \ CLS(Audio) \ SUB(Audio, DSP) \ + SUB(Audio, Sink) \ CLS(Loader) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/common/logging/log.h b/src/common/logging/log.h index 521362317..c6910b1c7 100644 --- a/src/common/logging/log.h +++ b/src/common/logging/log.h @@ -78,8 +78,9 @@ enum class Class : ClassType { Render, ///< Emulator video output and hardware acceleration Render_Software, ///< Software renderer backend Render_OpenGL, ///< OpenGL backend - Audio, ///< Emulator audio output + Audio, ///< Audio emulation Audio_DSP, ///< The HLE implementation of the DSP + Audio_Sink, ///< Emulator audio output backend Loader, ///< ROM loader Count ///< Total number of logging classes diff --git a/src/core/core.cpp b/src/core/core.cpp index 3bb843aab..cabab744a 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -51,7 +51,7 @@ void RunLoop(int tight_loop) { } HW::Update(); - if (HLE::g_reschedule) { + if (HLE::IsReschedulePending()) { Kernel::Reschedule(); } } diff --git a/src/core/hle/applets/mii_selector.cpp b/src/core/hle/applets/mii_selector.cpp index 1dd6757f7..b4456ca90 100644 --- a/src/core/hle/applets/mii_selector.cpp +++ b/src/core/hle/applets/mii_selector.cpp @@ -58,6 +58,11 @@ ResultCode MiiSelector::StartImpl(const Service::APT::AppletStartupParameter& pa // TODO(Subv): Set the expected fields in the response buffer before resending it to the application. // TODO(Subv): Reverse the parameter format for the Mii Selector + if(parameter.buffer_size >= sizeof(u32)) { + // TODO: defaults return no error, but garbage in other unknown fields + memset(parameter.data, 0, sizeof(u32)); + } + // Let the application know that we're closing Service::APT::MessageParameter message; message.buffer_size = parameter.buffer_size; diff --git a/src/core/hle/applets/mii_selector.h b/src/core/hle/applets/mii_selector.h index 5619f8399..be6b04642 100644 --- a/src/core/hle/applets/mii_selector.h +++ b/src/core/hle/applets/mii_selector.h @@ -16,6 +16,50 @@ namespace HLE { namespace Applets { +struct MiiConfig { + u8 unk_000; + u8 unk_001; + u8 unk_002; + u8 unk_003; + u8 unk_004; + INSERT_PADDING_BYTES(3); + u16 unk_008; + INSERT_PADDING_BYTES(0x8C - 0xA); + u8 unk_08C; + INSERT_PADDING_BYTES(3); + u16 unk_090; + INSERT_PADDING_BYTES(2); + u32 unk_094; + u16 unk_098; + u8 unk_09A[0x64]; + u8 unk_0FE; + u8 unk_0FF; + u32 unk_100; +}; + +static_assert(sizeof(MiiConfig) == 0x104, "MiiConfig structure has incorrect size"); +#define ASSERT_REG_POSITION(field_name, position) static_assert(offsetof(MiiConfig, field_name) == position, "Field "#field_name" has invalid position") +ASSERT_REG_POSITION(unk_008, 0x08); +ASSERT_REG_POSITION(unk_08C, 0x8C); +ASSERT_REG_POSITION(unk_090, 0x90); +ASSERT_REG_POSITION(unk_094, 0x94); +ASSERT_REG_POSITION(unk_0FE, 0xFE); +#undef ASSERT_REG_POSITION + +struct MiiResult { + u32 result_code; + u8 unk_04; + INSERT_PADDING_BYTES(7); + u8 unk_0C[0x60]; + u8 unk_6C[0x16]; + INSERT_PADDING_BYTES(2); +}; +static_assert(sizeof(MiiResult) == 0x84, "MiiResult structure has incorrect size"); +#define ASSERT_REG_POSITION(field_name, position) static_assert(offsetof(MiiResult, field_name) == position, "Field "#field_name" has invalid position") +ASSERT_REG_POSITION(unk_0C, 0x0C); +ASSERT_REG_POSITION(unk_6C, 0x6C); +#undef ASSERT_REG_POSITION + class MiiSelector final : public Applet { public: MiiSelector(Service::APT::AppletId id) : Applet(id), started(false) { } diff --git a/src/core/hle/hle.cpp b/src/core/hle/hle.cpp index e545de3b5..5c5373517 100644 --- a/src/core/hle/hle.cpp +++ b/src/core/hle/hle.cpp @@ -12,9 +12,13 @@ //////////////////////////////////////////////////////////////////////////////////////////////////// -namespace HLE { +namespace { + +bool reschedule; ///< If true, immediately reschedules the CPU to a new thread -bool g_reschedule; ///< If true, immediately reschedules the CPU to a new thread +} + +namespace HLE { void Reschedule(const char *reason) { DEBUG_ASSERT_MSG(reason != nullptr && strlen(reason) < 256, "Reschedule: Invalid or too long reason."); @@ -27,13 +31,21 @@ void Reschedule(const char *reason) { Core::g_app_core->PrepareReschedule(); - g_reschedule = true; + reschedule = true; +} + +bool IsReschedulePending() { + return reschedule; +} + +void DoneRescheduling() { + reschedule = false; } void Init() { Service::Init(); - g_reschedule = false; + reschedule = false; LOG_DEBUG(Kernel, "initialized OK"); } diff --git a/src/core/hle/hle.h b/src/core/hle/hle.h index e0b97797c..69ac0ade6 100644 --- a/src/core/hle/hle.h +++ b/src/core/hle/hle.h @@ -13,9 +13,9 @@ const Handle INVALID_HANDLE = 0; namespace HLE { -extern bool g_reschedule; ///< If true, immediately reschedules the CPU to a new thread - void Reschedule(const char *reason); +bool IsReschedulePending(); +void DoneRescheduling(); void Init(); void Shutdown(); diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index bf32f653d..6dc95d0f1 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -483,7 +483,8 @@ void Reschedule() { Thread* cur = GetCurrentThread(); Thread* next = PopNextReadyThread(); - HLE::g_reschedule = false; + + HLE::DoneRescheduling(); // Don't bother switching to the same thread if (next == cur) diff --git a/src/core/loader/3dsx.cpp b/src/core/loader/3dsx.cpp index 5fb3b9e2b..48a11ef81 100644 --- a/src/core/loader/3dsx.cpp +++ b/src/core/loader/3dsx.cpp @@ -303,4 +303,31 @@ ResultStatus AppLoader_THREEDSX::ReadRomFS(std::shared_ptr<FileUtil::IOFile>& ro return ResultStatus::ErrorNotUsed; } +ResultStatus AppLoader_THREEDSX::ReadIcon(std::vector<u8>& buffer) { + if (!file.IsOpen()) + return ResultStatus::Error; + + // Reset read pointer in case this file has been read before. + file.Seek(0, SEEK_SET); + + THREEDSX_Header hdr; + if (file.ReadBytes(&hdr, sizeof(THREEDSX_Header)) != sizeof(THREEDSX_Header)) + return ResultStatus::Error; + + if (hdr.header_size != sizeof(THREEDSX_Header)) + return ResultStatus::Error; + + // Check if the 3DSX has a SMDH... + if (hdr.smdh_offset != 0) { + file.Seek(hdr.smdh_offset, SEEK_SET); + buffer.resize(hdr.smdh_size); + + if (file.ReadBytes(&buffer[0], hdr.smdh_size) != hdr.smdh_size) + return ResultStatus::Error; + + return ResultStatus::Success; + } + return ResultStatus::ErrorNotUsed; +} + } // namespace Loader diff --git a/src/core/loader/3dsx.h b/src/core/loader/3dsx.h index 365ddb7a5..3ee686703 100644 --- a/src/core/loader/3dsx.h +++ b/src/core/loader/3dsx.h @@ -17,7 +17,7 @@ namespace Loader { /// Loads an 3DSX file class AppLoader_THREEDSX final : public AppLoader { public: - AppLoader_THREEDSX(FileUtil::IOFile&& file, std::string filename, const std::string& filepath) + AppLoader_THREEDSX(FileUtil::IOFile&& file, const std::string& filename, const std::string& filepath) : AppLoader(std::move(file)), filename(std::move(filename)), filepath(filepath) {} /** @@ -34,6 +34,13 @@ public: ResultStatus Load() override; /** + * Get the icon (typically icon section) of the application + * @param buffer Reference to buffer to store data + * @return ResultStatus result of function + */ + ResultStatus ReadIcon(std::vector<u8>& buffer) override; + + /** * Get the RomFS of the application * @param romfs_file Reference to buffer to store data * @param offset Offset in the file to the RomFS diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 886501c41..af3f62248 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -90,6 +90,28 @@ const char* GetFileTypeString(FileType type) { return "unknown"; } +std::unique_ptr<AppLoader> GetLoader(FileUtil::IOFile&& file, FileType type, + const std::string& filename, const std::string& filepath) { + switch (type) { + + // 3DSX file format. + case FileType::THREEDSX: + return std::make_unique<AppLoader_THREEDSX>(std::move(file), filename, filepath); + + // Standard ELF file format. + case FileType::ELF: + return std::make_unique<AppLoader_ELF>(std::move(file), filename); + + // NCCH/NCSD container formats. + case FileType::CXI: + case FileType::CCI: + return std::make_unique<AppLoader_NCCH>(std::move(file), filepath); + + default: + return std::unique_ptr<AppLoader>(); + } +} + ResultStatus LoadFile(const std::string& filename) { FileUtil::IOFile file(filename, "rb"); if (!file.IsOpen()) { @@ -111,38 +133,29 @@ ResultStatus LoadFile(const std::string& filename) { LOG_INFO(Loader, "Loading file %s as %s...", filename.c_str(), GetFileTypeString(type)); + std::unique_ptr<AppLoader> app_loader = GetLoader(std::move(file), type, filename_filename, filename); + switch (type) { - //3DSX file format... + // 3DSX file format... + // or NCCH/NCSD container formats... case FileType::THREEDSX: - { - AppLoader_THREEDSX app_loader(std::move(file), filename_filename, filename); - // Load application and RomFS - if (ResultStatus::Success == app_loader.Load()) { - Service::FS::RegisterArchiveType(std::make_unique<FileSys::ArchiveFactory_RomFS>(app_loader), Service::FS::ArchiveIdCode::RomFS); - return ResultStatus::Success; - } - break; - } - - // Standard ELF file format... - case FileType::ELF: - return AppLoader_ELF(std::move(file), filename_filename).Load(); - - // NCCH/NCSD container formats... case FileType::CXI: case FileType::CCI: { - AppLoader_NCCH app_loader(std::move(file), filename); - // Load application and RomFS - ResultStatus result = app_loader.Load(); + ResultStatus result = app_loader->Load(); if (ResultStatus::Success == result) { - Service::FS::RegisterArchiveType(std::make_unique<FileSys::ArchiveFactory_RomFS>(app_loader), Service::FS::ArchiveIdCode::RomFS); + Service::FS::RegisterArchiveType(std::make_unique<FileSys::ArchiveFactory_RomFS>(*app_loader), Service::FS::ArchiveIdCode::RomFS); + return ResultStatus::Success; } return result; } + // Standard ELF file format... + case FileType::ELF: + return app_loader->Load(); + // CIA file format... case FileType::CIA: return ResultStatus::ErrorNotImplemented; diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 84a4ce5fc..9d3e9ed3b 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -10,8 +10,10 @@ #include <string> #include <vector> +#include "common/common_funcs.h" #include "common/common_types.h" #include "common/file_util.h" +#include "common/swap.h" namespace Kernel { struct AddressMapping; @@ -78,6 +80,51 @@ constexpr u32 MakeMagic(char a, char b, char c, char d) { return a | b << 8 | c << 16 | d << 24; } +/// SMDH data structure that contains titles, icons etc. See https://www.3dbrew.org/wiki/SMDH +struct SMDH { + u32_le magic; + u16_le version; + INSERT_PADDING_BYTES(2); + + struct Title { + std::array<u16, 0x40> short_title; + std::array<u16, 0x80> long_title; + std::array<u16, 0x40> publisher; + }; + std::array<Title, 16> titles; + + std::array<u8, 16> ratings; + u32_le region_lockout; + u32_le match_maker_id; + u64_le match_maker_bit_id; + u32_le flags; + u16_le eula_version; + INSERT_PADDING_BYTES(2); + float_le banner_animation_frame; + u32_le cec_id; + INSERT_PADDING_BYTES(8); + + std::array<u8, 0x480> small_icon; + std::array<u8, 0x1200> large_icon; + + /// indicates the language used for each title entry + enum class TitleLanguage { + Japanese = 0, + English = 1, + French = 2, + German = 3, + Italian = 4, + Spanish = 5, + SimplifiedChinese = 6, + Korean= 7, + Dutch = 8, + Portuguese = 9, + Russian = 10, + TraditionalChinese = 11 + }; +}; +static_assert(sizeof(SMDH) == 0x36C0, "SMDH structure size is wrong"); + /// Interface for loading an application class AppLoader : NonCopyable { public: @@ -150,6 +197,16 @@ protected: extern const std::initializer_list<Kernel::AddressMapping> default_address_mappings; /** + * Get a loader for a file with a specific type + * @param file The file to load + * @param type The type of the file + * @param filename the file name (without path) + * @param filepath the file full path (with name) + * @return std::unique_ptr<AppLoader> a pointer to a loader object; nullptr for unsupported type + */ +std::unique_ptr<AppLoader> GetLoader(FileUtil::IOFile&& file, FileType type, const std::string& filename, const std::string& filepath); + +/** * Identifies and loads a bootable file * @param filename String filename of bootable file * @return ResultStatus result of function diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 066e91a9e..d362a4419 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -173,6 +173,10 @@ ResultStatus AppLoader_NCCH::LoadSectionExeFS(const char* name, std::vector<u8>& if (!file.IsOpen()) return ResultStatus::Error; + ResultStatus result = LoadExeFS(); + if (result != ResultStatus::Success) + return result; + LOG_DEBUG(Loader, "%d sections:", kMaxSections); // Iterate through the ExeFs archive until we find a section with the specified name... for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { @@ -215,9 +219,9 @@ ResultStatus AppLoader_NCCH::LoadSectionExeFS(const char* name, std::vector<u8>& return ResultStatus::ErrorNotUsed; } -ResultStatus AppLoader_NCCH::Load() { - if (is_loaded) - return ResultStatus::ErrorAlreadyLoaded; +ResultStatus AppLoader_NCCH::LoadExeFS() { + if (is_exefs_loaded) + return ResultStatus::Success; if (!file.IsOpen()) return ResultStatus::Error; @@ -282,6 +286,18 @@ ResultStatus AppLoader_NCCH::Load() { if (file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) return ResultStatus::Error; + is_exefs_loaded = true; + return ResultStatus::Success; +} + +ResultStatus AppLoader_NCCH::Load() { + if (is_loaded) + return ResultStatus::ErrorAlreadyLoaded; + + ResultStatus result = LoadExeFS(); + if (result != ResultStatus::Success) + return result; + is_loaded = true; // Set state to loaded return LoadExec(); // Load the executable into memory for booting diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h index ca6772a78..fd852c3de 100644 --- a/src/core/loader/ncch.h +++ b/src/core/loader/ncch.h @@ -232,6 +232,13 @@ private: */ ResultStatus LoadExec(); + /** + * Ensure ExeFS is loaded and ready for reading sections + * @return ResultStatus result of function + */ + ResultStatus LoadExeFS(); + + bool is_exefs_loaded = false; bool is_compressed = false; u32 entry_point = 0; diff --git a/src/video_core/command_processor.cpp b/src/video_core/command_processor.cpp index be1a936b2..dd1379503 100644 --- a/src/video_core/command_processor.cpp +++ b/src/video_core/command_processor.cpp @@ -146,10 +146,9 @@ static void WritePicaReg(u32 id, u32 value, u32 mask) { Shader::UnitState<false> shader_unit; Shader::Setup(); - if (g_debug_context) - g_debug_context->OnEvent(DebugContext::Event::VertexLoaded, static_cast<void*>(&immediate_input)); - // Send to vertex shader + if (g_debug_context) + g_debug_context->OnEvent(DebugContext::Event::VertexShaderInvocation, static_cast<void*>(&immediate_input)); Shader::OutputVertex output = Shader::Run(shader_unit, immediate_input, regs.vs.num_input_attributes+1); // Send to renderer @@ -272,10 +271,9 @@ static void WritePicaReg(u32 id, u32 value, u32 mask) { Shader::InputVertex input; loader.LoadVertex(base_address, index, vertex, input, memory_accesses); - if (g_debug_context) - g_debug_context->OnEvent(DebugContext::Event::VertexLoaded, (void*)&input); - // Send to vertex shader + if (g_debug_context) + g_debug_context->OnEvent(DebugContext::Event::VertexShaderInvocation, (void*)&input); output = Shader::Run(shader_unit, input, loader.GetNumTotalAttributes()); if (is_indexed) { diff --git a/src/video_core/debug_utils/debug_utils.h b/src/video_core/debug_utils/debug_utils.h index be2d0301a..f628292a4 100644 --- a/src/video_core/debug_utils/debug_utils.h +++ b/src/video_core/debug_utils/debug_utils.h @@ -40,7 +40,7 @@ public: PicaCommandProcessed, IncomingPrimitiveBatch, FinishedPrimitiveBatch, - VertexLoaded, + VertexShaderInvocation, IncomingDisplayTransfer, GSPCommandProcessed, BufferSwapped, diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 0e9a0be8b..7fcd36409 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -473,12 +473,6 @@ static void DebugHandler(GLenum source, GLenum type, GLuint id, GLenum severity, bool RendererOpenGL::Init() { render_window->MakeCurrent(); - // TODO: Make frontends initialize this, so they can use gladLoadGLLoader with their own loaders - if (!gladLoadGL()) { - LOG_CRITICAL(Render_OpenGL, "Failed to initialize GL functions! Exiting..."); - exit(-1); - } - if (GLAD_GL_KHR_debug) { glEnable(GL_DEBUG_OUTPUT); glDebugMessageCallback(DebugHandler, nullptr); |