diff options
43 files changed, 749 insertions, 211 deletions
diff --git a/.gitmodules b/.gitmodules index 4f4e8690b..e73ca99e3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@  [submodule "opus"]      path = externals/opus      url = https://github.com/ogniK5377/opus.git +[submodule "soundtouch"] +	path = externals/soundtouch +	url = https://github.com/citra-emu/ext-soundtouch.git diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 3d8e10c2b..53dcf1f1a 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -50,6 +50,9 @@ add_subdirectory(open_source_archives EXCLUDE_FROM_ALL)  add_library(unicorn-headers INTERFACE)  target_include_directories(unicorn-headers INTERFACE ./unicorn/include) +# SoundTouch +add_subdirectory(soundtouch) +  # Xbyak  if (ARCHITECTURE_x86_64)      # Defined before "dynarmic" above diff --git a/externals/soundtouch b/externals/soundtouch new file mode 160000 +Subproject 060181eaf273180d3a7e87349895bd0cb6ccbf4 diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 82e4850f7..c381dbe1d 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -17,6 +17,8 @@ add_library(audio_core STATIC      sink_stream.h      stream.cpp      stream.h +    time_stretch.cpp +    time_stretch.h      $<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h>  ) @@ -24,6 +26,7 @@ add_library(audio_core STATIC  create_target_directory_groups(audio_core)  target_link_libraries(audio_core PUBLIC common core) +target_link_libraries(audio_core PRIVATE SoundTouch)  if(ENABLE_CUBEB)      target_link_libraries(audio_core PRIVATE cubeb) diff --git a/src/audio_core/cubeb_sink.cpp b/src/audio_core/cubeb_sink.cpp index 5a1177d0c..79155a7a0 100644 --- a/src/audio_core/cubeb_sink.cpp +++ b/src/audio_core/cubeb_sink.cpp @@ -3,27 +3,23 @@  // Refer to the license.txt file included.  #include <algorithm> +#include <atomic>  #include <cstring> -#include <mutex> -  #include "audio_core/cubeb_sink.h"  #include "audio_core/stream.h" +#include "audio_core/time_stretch.h"  #include "common/logging/log.h" +#include "common/ring_buffer.h" +#include "core/settings.h"  namespace AudioCore { -class SinkStreamImpl final : public SinkStream { +class CubebSinkStream final : public SinkStream {  public: -    SinkStreamImpl(cubeb* ctx, u32 sample_rate, u32 num_channels_, cubeb_devid output_device, -                   const std::string& name) -        : ctx{ctx}, num_channels{num_channels_} { - -        if (num_channels == 6) { -            // 6-channel audio does not seem to work with cubeb + SDL, so we downsample this to 2 -            // channel for now -            is_6_channel = true; -            num_channels = 2; -        } +    CubebSinkStream(cubeb* ctx, u32 sample_rate, u32 num_channels_, cubeb_devid output_device, +                    const std::string& name) +        : ctx{ctx}, num_channels{std::min(num_channels_, 2u)}, time_stretch{sample_rate, +                                                                            num_channels} {          cubeb_stream_params params{};          params.rate = sample_rate; @@ -38,7 +34,7 @@ public:          if (cubeb_stream_init(ctx, &stream_backend, name.c_str(), nullptr, nullptr, output_device,                                ¶ms, std::max(512u, minimum_latency), -                              &SinkStreamImpl::DataCallback, &SinkStreamImpl::StateCallback, +                              &CubebSinkStream::DataCallback, &CubebSinkStream::StateCallback,                                this) != CUBEB_OK) {              LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream");              return; @@ -50,7 +46,7 @@ public:          }      } -    ~SinkStreamImpl() { +    ~CubebSinkStream() {          if (!ctx) {              return;          } @@ -62,27 +58,32 @@ public:          cubeb_stream_destroy(stream_backend);      } -    void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) override { -        if (!ctx) { +    void EnqueueSamples(u32 source_num_channels, const std::vector<s16>& samples) override { +        if (source_num_channels > num_channels) { +            // Downsample 6 channels to 2 +            std::vector<s16> buf; +            buf.reserve(samples.size() * num_channels / source_num_channels); +            for (size_t i = 0; i < samples.size(); i += source_num_channels) { +                for (size_t ch = 0; ch < num_channels; ch++) { +                    buf.push_back(samples[i + ch]); +                } +            } +            queue.Push(buf);              return;          } -        std::lock_guard lock{queue_mutex}; +        queue.Push(samples); +    } -        queue.reserve(queue.size() + samples.size() * GetNumChannels()); +    size_t SamplesInQueue(u32 num_channels) const override { +        if (!ctx) +            return 0; -        if (is_6_channel) { -            // Downsample 6 channels to 2 -            const size_t sample_count_copy_size = samples.size() * 2; -            queue.reserve(sample_count_copy_size); -            for (size_t i = 0; i < samples.size(); i += num_channels) { -                queue.push_back(samples[i]); -                queue.push_back(samples[i + 1]); -            } -        } else { -            // Copy as-is -            std::copy(samples.begin(), samples.end(), std::back_inserter(queue)); -        } +        return queue.Size() / num_channels; +    } + +    void Flush() override { +        should_flush = true;      }      u32 GetNumChannels() const { @@ -95,10 +96,11 @@ private:      cubeb* ctx{};      cubeb_stream* stream_backend{};      u32 num_channels{}; -    bool is_6_channel{}; -    std::mutex queue_mutex; -    std::vector<s16> queue; +    Common::RingBuffer<s16, 0x10000> queue; +    std::array<s16, 2> last_frame; +    std::atomic<bool> should_flush{}; +    TimeStretcher time_stretch;      static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,                               void* output_buffer, long num_frames); @@ -144,38 +146,52 @@ CubebSink::~CubebSink() {  SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels,                                           const std::string& name) {      sink_streams.push_back( -        std::make_unique<SinkStreamImpl>(ctx, sample_rate, num_channels, output_device, name)); +        std::make_unique<CubebSinkStream>(ctx, sample_rate, num_channels, output_device, name));      return *sink_streams.back();  } -long SinkStreamImpl::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, -                                  void* output_buffer, long num_frames) { -    SinkStreamImpl* impl = static_cast<SinkStreamImpl*>(user_data); +long CubebSinkStream::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, +                                   void* output_buffer, long num_frames) { +    CubebSinkStream* impl = static_cast<CubebSinkStream*>(user_data);      u8* buffer = reinterpret_cast<u8*>(output_buffer);      if (!impl) {          return {};      } -    std::lock_guard lock{impl->queue_mutex}; +    const size_t num_channels = impl->GetNumChannels(); +    const size_t samples_to_write = num_channels * num_frames; +    size_t samples_written; + +    if (Settings::values.enable_audio_stretching) { +        const std::vector<s16> in{impl->queue.Pop()}; +        const size_t num_in{in.size() / num_channels}; +        s16* const out{reinterpret_cast<s16*>(buffer)}; +        const size_t out_frames = impl->time_stretch.Process(in.data(), num_in, out, num_frames); +        samples_written = out_frames * num_channels; -    const size_t frames_to_write{ -        std::min(impl->queue.size() / impl->GetNumChannels(), static_cast<size_t>(num_frames))}; +        if (impl->should_flush) { +            impl->time_stretch.Flush(); +            impl->should_flush = false; +        } +    } else { +        samples_written = impl->queue.Pop(buffer, samples_to_write); +    } -    memcpy(buffer, impl->queue.data(), frames_to_write * sizeof(s16) * impl->GetNumChannels()); -    impl->queue.erase(impl->queue.begin(), -                      impl->queue.begin() + frames_to_write * impl->GetNumChannels()); +    if (samples_written >= num_channels) { +        std::memcpy(&impl->last_frame[0], buffer + (samples_written - num_channels) * sizeof(s16), +                    num_channels * sizeof(s16)); +    } -    if (frames_to_write < num_frames) { -        // Fill the rest of the frames with silence -        memset(buffer + frames_to_write * sizeof(s16) * impl->GetNumChannels(), 0, -               (num_frames - frames_to_write) * sizeof(s16) * impl->GetNumChannels()); +    // Fill the rest of the frames with last_frame +    for (size_t i = samples_written; i < samples_to_write; i += num_channels) { +        std::memcpy(buffer + i * sizeof(s16), &impl->last_frame[0], num_channels * sizeof(s16));      }      return num_frames;  } -void SinkStreamImpl::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {} +void CubebSinkStream::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {}  std::vector<std::string> ListCubebSinkDevices() {      std::vector<std::string> device_list; diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h index f235d93e5..2ed0c83b6 100644 --- a/src/audio_core/null_sink.h +++ b/src/audio_core/null_sink.h @@ -21,6 +21,12 @@ public:  private:      struct NullSinkStreamImpl final : SinkStream {          void EnqueueSamples(u32 /*num_channels*/, const std::vector<s16>& /*samples*/) override {} + +        size_t SamplesInQueue(u32 /*num_channels*/) const override { +            return 0; +        } + +        void Flush() override {}      } null_sink_stream;  }; diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index 955ba20fb..67cf1f3b2 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -24,7 +24,7 @@ const std::vector<SinkDetails> g_sink_details = {                  [] { return std::vector<std::string>{"null"}; }},  }; -const SinkDetails& GetSinkDetails(std::string sink_id) { +const SinkDetails& GetSinkDetails(std::string_view sink_id) {      auto iter =          std::find_if(g_sink_details.begin(), g_sink_details.end(),                       [sink_id](const auto& sink_detail) { return sink_detail.id == sink_id; }); diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h index ea666c554..03534b187 100644 --- a/src/audio_core/sink_details.h +++ b/src/audio_core/sink_details.h @@ -6,6 +6,8 @@  #include <functional>  #include <memory> +#include <string> +#include <string_view>  #include <utility>  #include <vector> @@ -30,6 +32,6 @@ struct SinkDetails {  extern const std::vector<SinkDetails> g_sink_details; -const SinkDetails& GetSinkDetails(std::string sink_id); +const SinkDetails& GetSinkDetails(std::string_view sink_id);  } // namespace AudioCore diff --git a/src/audio_core/sink_stream.h b/src/audio_core/sink_stream.h index 41b6736d8..4309ad094 100644 --- a/src/audio_core/sink_stream.h +++ b/src/audio_core/sink_stream.h @@ -25,6 +25,10 @@ public:       * @param samples Samples in interleaved stereo PCM16 format.       */      virtual void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) = 0; + +    virtual std::size_t SamplesInQueue(u32 num_channels) const = 0; + +    virtual void Flush() = 0;  };  using SinkStreamPtr = std::unique_ptr<SinkStream>; diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp index dbae75d8c..84dcdd98d 100644 --- a/src/audio_core/stream.cpp +++ b/src/audio_core/stream.cpp @@ -73,6 +73,7 @@ static void VolumeAdjustSamples(std::vector<s16>& samples) {  void Stream::PlayNextBuffer() {      if (!IsPlaying()) {          // Ensure we are in playing state before playing the next buffer +        sink_stream.Flush();          return;      } @@ -83,6 +84,7 @@ void Stream::PlayNextBuffer() {      if (queued_buffers.empty()) {          // No queued buffers - we are effectively paused +        sink_stream.Flush();          return;      } @@ -90,6 +92,7 @@ void Stream::PlayNextBuffer() {      queued_buffers.pop();      VolumeAdjustSamples(active_buffer->Samples()); +      sink_stream.EnqueueSamples(GetNumChannels(), active_buffer->GetSamples());      CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {}); diff --git a/src/audio_core/time_stretch.cpp b/src/audio_core/time_stretch.cpp new file mode 100644 index 000000000..da094c46b --- /dev/null +++ b/src/audio_core/time_stretch.cpp @@ -0,0 +1,68 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <cmath> +#include <cstddef> +#include "audio_core/time_stretch.h" +#include "common/logging/log.h" + +namespace AudioCore { + +TimeStretcher::TimeStretcher(u32 sample_rate, u32 channel_count) +    : m_sample_rate(sample_rate), m_channel_count(channel_count) { +    m_sound_touch.setChannels(channel_count); +    m_sound_touch.setSampleRate(sample_rate); +    m_sound_touch.setPitch(1.0); +    m_sound_touch.setTempo(1.0); +} + +void TimeStretcher::Clear() { +    m_sound_touch.clear(); +} + +void TimeStretcher::Flush() { +    m_sound_touch.flush(); +} + +size_t TimeStretcher::Process(const s16* in, size_t num_in, s16* out, size_t num_out) { +    const double time_delta = static_cast<double>(num_out) / m_sample_rate; // seconds + +    // We were given actual_samples number of samples, and num_samples were requested from us. +    double current_ratio = static_cast<double>(num_in) / static_cast<double>(num_out); + +    const double max_latency = 1.0; // seconds +    const double max_backlog = m_sample_rate * max_latency; +    const double backlog_fullness = m_sound_touch.numSamples() / max_backlog; +    if (backlog_fullness > 5.0) { +        // Too many samples in backlog: Don't push anymore on +        num_in = 0; +    } + +    // We ideally want the backlog to be about 50% full. +    // This gives some headroom both ways to prevent underflow and overflow. +    // We tweak current_ratio to encourage this. +    constexpr double tweak_time_scale = 0.05; // seconds +    const double tweak_correction = (backlog_fullness - 0.5) * (time_delta / tweak_time_scale); +    current_ratio *= std::pow(1.0 + 2.0 * tweak_correction, tweak_correction < 0 ? 3.0 : 1.0); + +    // This low-pass filter smoothes out variance in the calculated stretch ratio. +    // The time-scale determines how responsive this filter is. +    constexpr double lpf_time_scale = 2.0; // seconds +    const double lpf_gain = 1.0 - std::exp(-time_delta / lpf_time_scale); +    m_stretch_ratio += lpf_gain * (current_ratio - m_stretch_ratio); + +    // Place a lower limit of 5% speed.  When a game boots up, there will be +    // many silence samples.  These do not need to be timestretched. +    m_stretch_ratio = std::max(m_stretch_ratio, 0.05); +    m_sound_touch.setTempo(m_stretch_ratio); + +    LOG_DEBUG(Audio, "{:5}/{:5} ratio:{:0.6f} backlog:{:0.6f}", num_in, num_out, m_stretch_ratio, +              backlog_fullness); + +    m_sound_touch.putSamples(in, num_in); +    return m_sound_touch.receiveSamples(out, num_out); +} + +} // namespace AudioCore diff --git a/src/audio_core/time_stretch.h b/src/audio_core/time_stretch.h new file mode 100644 index 000000000..7e39e695e --- /dev/null +++ b/src/audio_core/time_stretch.h @@ -0,0 +1,36 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <cstddef> +#include <SoundTouch.h> +#include "common/common_types.h" + +namespace AudioCore { + +class TimeStretcher { +public: +    TimeStretcher(u32 sample_rate, u32 channel_count); + +    /// @param in       Input sample buffer +    /// @param num_in   Number of input frames in `in` +    /// @param out      Output sample buffer +    /// @param num_out  Desired number of output frames in `out` +    /// @returns Actual number of frames written to `out` +    size_t Process(const s16* in, size_t num_in, s16* out, size_t num_out); + +    void Clear(); + +    void Flush(); + +private: +    u32 m_sample_rate; +    u32 m_channel_count; +    soundtouch::SoundTouch m_sound_touch; +    double m_stretch_ratio = 1.0; +}; + +} // namespace AudioCore diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index f41946cc6..6a3f1fe08 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -71,6 +71,7 @@ add_library(common STATIC      param_package.cpp      param_package.h      quaternion.h +    ring_buffer.h      scm_rev.cpp      scm_rev.h      scope_exit.h diff --git a/src/common/ring_buffer.h b/src/common/ring_buffer.h new file mode 100644 index 000000000..30d934a38 --- /dev/null +++ b/src/common/ring_buffer.h @@ -0,0 +1,111 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <algorithm> +#include <array> +#include <atomic> +#include <cstddef> +#include <cstring> +#include <type_traits> +#include <vector> +#include "common/common_types.h" + +namespace Common { + +/// SPSC ring buffer +/// @tparam T            Element type +/// @tparam capacity     Number of slots in ring buffer +/// @tparam granularity  Slot size in terms of number of elements +template <typename T, size_t capacity, size_t granularity = 1> +class RingBuffer { +    /// A "slot" is made of `granularity` elements of `T`. +    static constexpr size_t slot_size = granularity * sizeof(T); +    // T must be safely memcpy-able and have a trivial default constructor. +    static_assert(std::is_trivial_v<T>); +    // Ensure capacity is sensible. +    static_assert(capacity < std::numeric_limits<size_t>::max() / 2 / granularity); +    static_assert((capacity & (capacity - 1)) == 0, "capacity must be a power of two"); +    // Ensure lock-free. +    static_assert(std::atomic<size_t>::is_always_lock_free); + +public: +    /// Pushes slots into the ring buffer +    /// @param new_slots   Pointer to the slots to push +    /// @param slot_count  Number of slots to push +    /// @returns The number of slots actually pushed +    size_t Push(const void* new_slots, size_t slot_count) { +        const size_t write_index = m_write_index.load(); +        const size_t slots_free = capacity + m_read_index.load() - write_index; +        const size_t push_count = std::min(slot_count, slots_free); + +        const size_t pos = write_index % capacity; +        const size_t first_copy = std::min(capacity - pos, push_count); +        const size_t second_copy = push_count - first_copy; + +        const char* in = static_cast<const char*>(new_slots); +        std::memcpy(m_data.data() + pos * granularity, in, first_copy * slot_size); +        in += first_copy * slot_size; +        std::memcpy(m_data.data(), in, second_copy * slot_size); + +        m_write_index.store(write_index + push_count); + +        return push_count; +    } + +    size_t Push(const std::vector<T>& input) { +        return Push(input.data(), input.size()); +    } + +    /// Pops slots from the ring buffer +    /// @param output     Where to store the popped slots +    /// @param max_slots  Maximum number of slots to pop +    /// @returns The number of slots actually popped +    size_t Pop(void* output, size_t max_slots = ~size_t(0)) { +        const size_t read_index = m_read_index.load(); +        const size_t slots_filled = m_write_index.load() - read_index; +        const size_t pop_count = std::min(slots_filled, max_slots); + +        const size_t pos = read_index % capacity; +        const size_t first_copy = std::min(capacity - pos, pop_count); +        const size_t second_copy = pop_count - first_copy; + +        char* out = static_cast<char*>(output); +        std::memcpy(out, m_data.data() + pos * granularity, first_copy * slot_size); +        out += first_copy * slot_size; +        std::memcpy(out, m_data.data(), second_copy * slot_size); + +        m_read_index.store(read_index + pop_count); + +        return pop_count; +    } + +    std::vector<T> Pop(size_t max_slots = ~size_t(0)) { +        std::vector<T> out(std::min(max_slots, capacity) * granularity); +        const size_t count = Pop(out.data(), out.size() / granularity); +        out.resize(count * granularity); +        return out; +    } + +    /// @returns Number of slots used +    size_t Size() const { +        return m_write_index.load() - m_read_index.load(); +    } + +    /// @returns Maximum size of ring buffer +    constexpr size_t Capacity() const { +        return capacity; +    } + +private: +    // It is important to align the below variables for performance reasons: +    // Having them on the same cache-line would result in false-sharing between them. +    alignas(128) std::atomic<size_t> m_read_index{0}; +    alignas(128) std::atomic<size_t> m_write_index{0}; + +    std::array<T, granularity * capacity> m_data; +}; + +} // namespace Common diff --git a/src/core/hle/ipc.h b/src/core/hle/ipc.h index eaa5395ac..545cd884a 100644 --- a/src/core/hle/ipc.h +++ b/src/core/hle/ipc.h @@ -153,7 +153,7 @@ struct DataPayloadHeader {      u32_le magic;      INSERT_PADDING_WORDS(1);  }; -static_assert(sizeof(DataPayloadHeader) == 8, "DataPayloadRequest size is incorrect"); +static_assert(sizeof(DataPayloadHeader) == 8, "DataPayloadHeader size is incorrect");  struct DomainMessageHeader {      enum class CommandType : u32_le { diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h index 20f50458b..cb57ee78a 100644 --- a/src/core/hle/kernel/thread.h +++ b/src/core/hle/kernel/thread.h @@ -15,6 +15,12 @@  #include "core/hle/kernel/wait_object.h"  #include "core/hle/result.h" +namespace Kernel { + +class KernelCore; +class Process; +class Scheduler; +  enum ThreadPriority : u32 {      THREADPRIO_HIGHEST = 0,       ///< Highest thread priority      THREADPRIO_USERLAND_MAX = 24, ///< Highest thread priority for userland apps @@ -54,12 +60,6 @@ enum class ThreadWakeupReason {      Timeout // The thread was woken up due to a wait timeout.  }; -namespace Kernel { - -class KernelCore; -class Process; -class Scheduler; -  class Thread final : public WaitObject {  public:      /** diff --git a/src/core/hle/service/acc/acc_su.h b/src/core/hle/service/acc/acc_su.h index a3eb885bf..fcced063a 100644 --- a/src/core/hle/service/acc/acc_su.h +++ b/src/core/hle/service/acc/acc_su.h @@ -6,8 +6,7 @@  #include "core/hle/service/acc/acc.h" -namespace Service { -namespace Account { +namespace Service::Account {  class ACC_SU final : public Module::Interface {  public: @@ -16,5 +15,4 @@ public:      ~ACC_SU() override;  }; -} // namespace Account -} // namespace Service +} // namespace Service::Account diff --git a/src/core/hle/service/ns/pl_u.cpp b/src/core/hle/service/ns/pl_u.cpp index da1c46d59..447689a1a 100644 --- a/src/core/hle/service/ns/pl_u.cpp +++ b/src/core/hle/service/ns/pl_u.cpp @@ -2,6 +2,10 @@  // Licensed under GPLv2 or any later version  // Refer to the license.txt file included. +#include <algorithm> +#include <cstring> +#include <vector> +  #include <FontChineseSimplified.h>  #include <FontChineseTraditional.h>  #include <FontExtendedChineseSimplified.h> @@ -9,14 +13,19 @@  #include <FontNintendoExtended.h>  #include <FontStandard.h> +#include "common/assert.h"  #include "common/common_paths.h" +#include "common/common_types.h"  #include "common/file_util.h" +#include "common/logging/log.h" +#include "common/swap.h"  #include "core/core.h"  #include "core/file_sys/content_archive.h"  #include "core/file_sys/nca_metadata.h"  #include "core/file_sys/registered_cache.h"  #include "core/file_sys/romfs.h"  #include "core/hle/ipc_helpers.h" +#include "core/hle/kernel/shared_memory.h"  #include "core/hle/service/filesystem/filesystem.h"  #include "core/hle/service/ns/pl_u.h" @@ -35,49 +44,41 @@ struct FontRegion {      u32 size;  }; -static constexpr std::array<std::pair<FontArchives, const char*>, 7> SHARED_FONTS{ +constexpr std::array<std::pair<FontArchives, const char*>, 7> SHARED_FONTS{      std::make_pair(FontArchives::Standard, "nintendo_udsg-r_std_003.bfttf"),      std::make_pair(FontArchives::ChineseSimple, "nintendo_udsg-r_org_zh-cn_003.bfttf"),      std::make_pair(FontArchives::ChineseSimple, "nintendo_udsg-r_ext_zh-cn_003.bfttf"),      std::make_pair(FontArchives::ChineseTraditional, "nintendo_udjxh-db_zh-tw_003.bfttf"),      std::make_pair(FontArchives::Korean, "nintendo_udsg-r_ko_003.bfttf"),      std::make_pair(FontArchives::Extension, "nintendo_ext_003.bfttf"), -    std::make_pair(FontArchives::Extension, "nintendo_ext2_003.bfttf")}; +    std::make_pair(FontArchives::Extension, "nintendo_ext2_003.bfttf"), +}; -static constexpr std::array<const char*, 7> SHARED_FONTS_TTF{"FontStandard.ttf", -                                                             "FontChineseSimplified.ttf", -                                                             "FontExtendedChineseSimplified.ttf", -                                                             "FontChineseTraditional.ttf", -                                                             "FontKorean.ttf", -                                                             "FontNintendoExtended.ttf", -                                                             "FontNintendoExtended2.ttf"}; +constexpr std::array<const char*, 7> SHARED_FONTS_TTF{ +    "FontStandard.ttf", +    "FontChineseSimplified.ttf", +    "FontExtendedChineseSimplified.ttf", +    "FontChineseTraditional.ttf", +    "FontKorean.ttf", +    "FontNintendoExtended.ttf", +    "FontNintendoExtended2.ttf", +};  // The below data is specific to shared font data dumped from Switch on f/w 2.2  // Virtual address and offsets/sizes likely will vary by dump -static constexpr VAddr SHARED_FONT_MEM_VADDR{0x00000009d3016000ULL}; -static constexpr u32 EXPECTED_RESULT{ -    0x7f9a0218}; // What we expect the decrypted bfttf first 4 bytes to be -static constexpr u32 EXPECTED_MAGIC{ -    0x36f81a1e}; // What we expect the encrypted bfttf first 4 bytes to be -static constexpr u64 SHARED_FONT_MEM_SIZE{0x1100000}; -static constexpr FontRegion EMPTY_REGION{0, 0}; -std::vector<FontRegion> -    SHARED_FONT_REGIONS{}; // Automatically populated based on shared_fonts dump or system archives - -const FontRegion& GetSharedFontRegion(size_t index) { -    if (index >= SHARED_FONT_REGIONS.size() || SHARED_FONT_REGIONS.empty()) { -        // No font fallback -        return EMPTY_REGION; -    } -    return SHARED_FONT_REGIONS.at(index); -} +constexpr VAddr SHARED_FONT_MEM_VADDR{0x00000009d3016000ULL}; +constexpr u32 EXPECTED_RESULT{0x7f9a0218}; // What we expect the decrypted bfttf first 4 bytes to be +constexpr u32 EXPECTED_MAGIC{0x36f81a1e};  // What we expect the encrypted bfttf first 4 bytes to be +constexpr u64 SHARED_FONT_MEM_SIZE{0x1100000}; +constexpr FontRegion EMPTY_REGION{0, 0};  enum class LoadState : u32 {      Loading = 0,      Done = 1,  }; -void DecryptSharedFont(const std::vector<u32>& input, std::vector<u8>& output, size_t& offset) { +static void DecryptSharedFont(const std::vector<u32>& input, std::vector<u8>& output, +                              size_t& offset) {      ASSERT_MSG(offset + (input.size() * sizeof(u32)) < SHARED_FONT_MEM_SIZE,                 "Shared fonts exceeds 17mb!");      ASSERT_MSG(input[0] == EXPECTED_MAGIC, "Failed to derive key, unexpected magic number"); @@ -104,28 +105,52 @@ static void EncryptSharedFont(const std::vector<u8>& input, std::vector<u8>& out      offset += input.size() + (sizeof(u32) * 2);  } +// Helper function to make BuildSharedFontsRawRegions a bit nicer  static u32 GetU32Swapped(const u8* data) {      u32 value;      std::memcpy(&value, data, sizeof(value)); -    return Common::swap32(value); // Helper function to make BuildSharedFontsRawRegions a bit nicer +    return Common::swap32(value);  } -void BuildSharedFontsRawRegions(const std::vector<u8>& input) { -    unsigned cur_offset = 0; // As we can derive the xor key we can just populate the offsets based -                             // on the shared memory dump -    for (size_t i = 0; i < SHARED_FONTS.size(); i++) { -        // Out of shared fonts/Invalid font -        if (GetU32Swapped(input.data() + cur_offset) != EXPECTED_RESULT) -            break; -        const u32 KEY = GetU32Swapped(input.data() + cur_offset) ^ -                        EXPECTED_MAGIC; // Derive key withing inverse xor -        const u32 SIZE = GetU32Swapped(input.data() + cur_offset + 4) ^ KEY; -        SHARED_FONT_REGIONS.push_back(FontRegion{cur_offset + 8, SIZE}); -        cur_offset += SIZE + 8; +struct PL_U::Impl { +    const FontRegion& GetSharedFontRegion(size_t index) const { +        if (index >= shared_font_regions.size() || shared_font_regions.empty()) { +            // No font fallback +            return EMPTY_REGION; +        } +        return shared_font_regions.at(index);      } -} -PL_U::PL_U() : ServiceFramework("pl:u") { +    void BuildSharedFontsRawRegions(const std::vector<u8>& input) { +        // As we can derive the xor key we can just populate the offsets +        // based on the shared memory dump +        unsigned cur_offset = 0; + +        for (size_t i = 0; i < SHARED_FONTS.size(); i++) { +            // Out of shared fonts/invalid font +            if (GetU32Swapped(input.data() + cur_offset) != EXPECTED_RESULT) { +                break; +            } + +            // Derive key withing inverse xor +            const u32 KEY = GetU32Swapped(input.data() + cur_offset) ^ EXPECTED_MAGIC; +            const u32 SIZE = GetU32Swapped(input.data() + cur_offset + 4) ^ KEY; +            shared_font_regions.push_back(FontRegion{cur_offset + 8, SIZE}); +            cur_offset += SIZE + 8; +        } +    } + +    /// Handle to shared memory region designated for a shared font +    Kernel::SharedPtr<Kernel::SharedMemory> shared_font_mem; + +    /// Backing memory for the shared font data +    std::shared_ptr<std::vector<u8>> shared_font; + +    // Automatically populated based on shared_fonts dump or system archives. +    std::vector<FontRegion> shared_font_regions; +}; + +PL_U::PL_U() : ServiceFramework("pl:u"), impl{std::make_unique<Impl>()} {      static const FunctionInfo functions[] = {          {0, &PL_U::RequestLoad, "RequestLoad"},          {1, &PL_U::GetLoadState, "GetLoadState"}, @@ -141,7 +166,7 @@ PL_U::PL_U() : ServiceFramework("pl:u") {      // Rebuild shared fonts from data ncas      if (nand->HasEntry(static_cast<u64>(FontArchives::Standard),                         FileSys::ContentRecordType::Data)) { -        shared_font = std::make_shared<std::vector<u8>>(SHARED_FONT_MEM_SIZE); +        impl->shared_font = std::make_shared<std::vector<u8>>(SHARED_FONT_MEM_SIZE);          for (auto font : SHARED_FONTS) {              const auto nca =                  nand->GetEntry(static_cast<u64>(font.first), FileSys::ContentRecordType::Data); @@ -177,12 +202,12 @@ PL_U::PL_U() : ServiceFramework("pl:u") {                  static_cast<u32>(offset + 8),                  static_cast<u32>((font_data_u32.size() * sizeof(u32)) -                                   8)}; // Font offset and size do not account for the header -            DecryptSharedFont(font_data_u32, *shared_font, offset); -            SHARED_FONT_REGIONS.push_back(region); +            DecryptSharedFont(font_data_u32, *impl->shared_font, offset); +            impl->shared_font_regions.push_back(region);          }      } else { -        shared_font = std::make_shared<std::vector<u8>>( +        impl->shared_font = std::make_shared<std::vector<u8>>(              SHARED_FONT_MEM_SIZE); // Shared memory needs to always be allocated and a fixed size          const std::string user_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir); @@ -206,8 +231,8 @@ PL_U::PL_U() : ServiceFramework("pl:u") {                          static_cast<u32>(offset + 8),                          static_cast<u32>(ttf_bytes.size())}; // Font offset and size do not account                                                               // for the header -                    EncryptSharedFont(ttf_bytes, *shared_font, offset); -                    SHARED_FONT_REGIONS.push_back(region); +                    EncryptSharedFont(ttf_bytes, *impl->shared_font, offset); +                    impl->shared_font_regions.push_back(region);                  } else {                      LOG_WARNING(Service_NS, "Unable to load font: {}", font_ttf);                  } @@ -222,26 +247,28 @@ PL_U::PL_U() : ServiceFramework("pl:u") {          if (file.IsOpen()) {              // Read shared font data              ASSERT(file.GetSize() == SHARED_FONT_MEM_SIZE); -            file.ReadBytes(shared_font->data(), shared_font->size()); -            BuildSharedFontsRawRegions(*shared_font); +            file.ReadBytes(impl->shared_font->data(), impl->shared_font->size()); +            impl->BuildSharedFontsRawRegions(*impl->shared_font);          } else {              LOG_WARNING(Service_NS,                          "Shared Font file missing. Loading open source replacement from memory"); +            // clang-format off              const std::vector<std::vector<u8>> open_source_shared_fonts_ttf = {                  {std::begin(FontChineseSimplified), std::end(FontChineseSimplified)},                  {std::begin(FontChineseTraditional), std::end(FontChineseTraditional)}, -                {std::begin(FontExtendedChineseSimplified), -                 std::end(FontExtendedChineseSimplified)}, +                {std::begin(FontExtendedChineseSimplified), std::end(FontExtendedChineseSimplified)}, +                {std::begin(FontKorean), std::end(FontKorean)},                  {std::begin(FontNintendoExtended), std::end(FontNintendoExtended)},                  {std::begin(FontStandard), std::end(FontStandard)},              }; +            // clang-format on              for (const std::vector<u8>& font_ttf : open_source_shared_fonts_ttf) {                  const FontRegion region{static_cast<u32>(offset + 8),                                          static_cast<u32>(font_ttf.size())}; -                EncryptSharedFont(font_ttf, *shared_font, offset); -                SHARED_FONT_REGIONS.push_back(region); +                EncryptSharedFont(font_ttf, *impl->shared_font, offset); +                impl->shared_font_regions.push_back(region);              }          }      } @@ -275,7 +302,7 @@ void PL_U::GetSize(Kernel::HLERequestContext& ctx) {      LOG_DEBUG(Service_NS, "called, font_id={}", font_id);      IPC::ResponseBuilder rb{ctx, 3};      rb.Push(RESULT_SUCCESS); -    rb.Push<u32>(GetSharedFontRegion(font_id).size); +    rb.Push<u32>(impl->GetSharedFontRegion(font_id).size);  }  void PL_U::GetSharedMemoryAddressOffset(Kernel::HLERequestContext& ctx) { @@ -285,17 +312,18 @@ void PL_U::GetSharedMemoryAddressOffset(Kernel::HLERequestContext& ctx) {      LOG_DEBUG(Service_NS, "called, font_id={}", font_id);      IPC::ResponseBuilder rb{ctx, 3};      rb.Push(RESULT_SUCCESS); -    rb.Push<u32>(GetSharedFontRegion(font_id).offset); +    rb.Push<u32>(impl->GetSharedFontRegion(font_id).offset);  }  void PL_U::GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx) {      // Map backing memory for the font data -    Core::CurrentProcess()->vm_manager.MapMemoryBlock( -        SHARED_FONT_MEM_VADDR, shared_font, 0, SHARED_FONT_MEM_SIZE, Kernel::MemoryState::Shared); +    Core::CurrentProcess()->vm_manager.MapMemoryBlock(SHARED_FONT_MEM_VADDR, impl->shared_font, 0, +                                                      SHARED_FONT_MEM_SIZE, +                                                      Kernel::MemoryState::Shared);      // Create shared font memory object      auto& kernel = Core::System::GetInstance().Kernel(); -    shared_font_mem = Kernel::SharedMemory::Create( +    impl->shared_font_mem = Kernel::SharedMemory::Create(          kernel, Core::CurrentProcess(), SHARED_FONT_MEM_SIZE, Kernel::MemoryPermission::ReadWrite,          Kernel::MemoryPermission::Read, SHARED_FONT_MEM_VADDR, Kernel::MemoryRegion::BASE,          "PL_U:shared_font_mem"); @@ -303,7 +331,7 @@ void PL_U::GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx) {      LOG_DEBUG(Service_NS, "called");      IPC::ResponseBuilder rb{ctx, 2, 1};      rb.Push(RESULT_SUCCESS); -    rb.PushCopyObjects(shared_font_mem); +    rb.PushCopyObjects(impl->shared_font_mem);  }  void PL_U::GetSharedFontInOrderOfPriority(Kernel::HLERequestContext& ctx) { @@ -316,9 +344,9 @@ void PL_U::GetSharedFontInOrderOfPriority(Kernel::HLERequestContext& ctx) {      std::vector<u32> font_sizes;      // TODO(ogniK): Have actual priority order -    for (size_t i = 0; i < SHARED_FONT_REGIONS.size(); i++) { +    for (size_t i = 0; i < impl->shared_font_regions.size(); i++) {          font_codes.push_back(static_cast<u32>(i)); -        auto region = GetSharedFontRegion(i); +        auto region = impl->GetSharedFontRegion(i);          font_offsets.push_back(region.offset);          font_sizes.push_back(region.size);      } diff --git a/src/core/hle/service/ns/pl_u.h b/src/core/hle/service/ns/pl_u.h index 296c3db05..253f26a2a 100644 --- a/src/core/hle/service/ns/pl_u.h +++ b/src/core/hle/service/ns/pl_u.h @@ -5,7 +5,6 @@  #pragma once  #include <memory> -#include "core/hle/kernel/shared_memory.h"  #include "core/hle/service/service.h"  namespace Service::NS { @@ -23,11 +22,8 @@ private:      void GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx);      void GetSharedFontInOrderOfPriority(Kernel::HLERequestContext& ctx); -    /// Handle to shared memory region designated for a shared font -    Kernel::SharedPtr<Kernel::SharedMemory> shared_font_mem; - -    /// Backing memory for the shared font data -    std::shared_ptr<std::vector<u8>> shared_font; +    struct Impl; +    std::unique_ptr<Impl> impl;  };  } // namespace Service::NS diff --git a/src/core/hle/service/nvflinger/buffer_queue.cpp b/src/core/hle/service/nvflinger/buffer_queue.cpp index 34f98fe5a..fd98d541d 100644 --- a/src/core/hle/service/nvflinger/buffer_queue.cpp +++ b/src/core/hle/service/nvflinger/buffer_queue.cpp @@ -9,8 +9,7 @@  #include "core/core.h"  #include "core/hle/service/nvflinger/buffer_queue.h" -namespace Service { -namespace NVFlinger { +namespace Service::NVFlinger {  BufferQueue::BufferQueue(u32 id, u64 layer_id) : id(id), layer_id(layer_id) {      auto& kernel = Core::System::GetInstance().Kernel(); @@ -104,5 +103,4 @@ u32 BufferQueue::Query(QueryType type) {      return 0;  } -} // namespace NVFlinger -} // namespace Service +} // namespace Service::NVFlinger diff --git a/src/core/hle/service/nvflinger/buffer_queue.h b/src/core/hle/service/nvflinger/buffer_queue.h index 17c81928a..50b767732 100644 --- a/src/core/hle/service/nvflinger/buffer_queue.h +++ b/src/core/hle/service/nvflinger/buffer_queue.h @@ -15,8 +15,7 @@ namespace CoreTiming {  struct EventType;  } -namespace Service { -namespace NVFlinger { +namespace Service::NVFlinger {  struct IGBPBuffer {      u32_le magic; @@ -98,5 +97,4 @@ private:      Kernel::SharedPtr<Kernel::Event> buffer_wait_event;  }; -} // namespace NVFlinger -} // namespace Service +} // namespace Service::NVFlinger diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp index 85244ac3b..cf94b00e6 100644 --- a/src/core/hle/service/vi/vi.cpp +++ b/src/core/hle/service/vi/vi.cpp @@ -514,7 +514,7 @@ private:                  ctx.SleepClientThread(                      Kernel::GetCurrentThread(), "IHOSBinderDriver::DequeueBuffer", -1,                      [=](Kernel::SharedPtr<Kernel::Thread> thread, Kernel::HLERequestContext& ctx, -                        ThreadWakeupReason reason) { +                        Kernel::ThreadWakeupReason reason) {                          // Repeat TransactParcel DequeueBuffer when a buffer is available                          auto buffer_queue = nv_flinger->GetBufferQueue(id);                          boost::optional<u32> slot = buffer_queue->DequeueBuffer(width, height); diff --git a/src/core/loader/nro.cpp b/src/core/loader/nro.cpp index bb89a9da3..c49ec34ab 100644 --- a/src/core/loader/nro.cpp +++ b/src/core/loader/nro.cpp @@ -191,7 +191,7 @@ ResultStatus AppLoader_NRO::Load(Kernel::SharedPtr<Kernel::Process>& process) {      process->svc_access_mask.set();      process->resource_limit =          kernel.ResourceLimitForCategory(Kernel::ResourceLimitCategory::APPLICATION); -    process->Run(base_addr, THREADPRIO_DEFAULT, Memory::DEFAULT_STACK_SIZE); +    process->Run(base_addr, Kernel::THREADPRIO_DEFAULT, Memory::DEFAULT_STACK_SIZE);      is_loaded = true;      return ResultStatus::Success; diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index 082a95d40..3c6306818 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -157,7 +157,8 @@ ResultStatus AppLoader_NSO::Load(Kernel::SharedPtr<Kernel::Process>& process) {      process->svc_access_mask.set();      process->resource_limit =          kernel.ResourceLimitForCategory(Kernel::ResourceLimitCategory::APPLICATION); -    process->Run(Memory::PROCESS_IMAGE_VADDR, THREADPRIO_DEFAULT, Memory::DEFAULT_STACK_SIZE); +    process->Run(Memory::PROCESS_IMAGE_VADDR, Kernel::THREADPRIO_DEFAULT, +                 Memory::DEFAULT_STACK_SIZE);      is_loaded = true;      return ResultStatus::Success; diff --git a/src/core/settings.h b/src/core/settings.h index 08a16ef2c..0318d019c 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -148,6 +148,7 @@ struct Values {      // Audio      std::string sink_id; +    bool enable_audio_stretching;      std::string audio_device_id;      float volume; diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp index 3730e85b8..b0df154ca 100644 --- a/src/core/telemetry_session.cpp +++ b/src/core/telemetry_session.cpp @@ -120,6 +120,9 @@ TelemetrySession::TelemetrySession() {      Telemetry::AppendOSInfo(field_collection);      // Log user configuration information +    AddField(Telemetry::FieldType::UserConfig, "Audio_SinkId", Settings::values.sink_id); +    AddField(Telemetry::FieldType::UserConfig, "Audio_EnableAudioStretching", +             Settings::values.enable_audio_stretching);      AddField(Telemetry::FieldType::UserConfig, "Core_UseCpuJit", Settings::values.use_cpu_jit);      AddField(Telemetry::FieldType::UserConfig, "Core_UseMultiCore",               Settings::values.use_multi_core); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 4d74bb395..4e75a72ec 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,5 +1,6 @@  add_executable(tests      common/param_package.cpp +    common/ring_buffer.cpp      core/arm/arm_test_common.cpp      core/arm/arm_test_common.h      core/core_timing.cpp diff --git a/src/tests/common/ring_buffer.cpp b/src/tests/common/ring_buffer.cpp new file mode 100644 index 000000000..f3fe57839 --- /dev/null +++ b/src/tests/common/ring_buffer.cpp @@ -0,0 +1,130 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <array> +#include <cstddef> +#include <numeric> +#include <thread> +#include <vector> +#include <catch2/catch.hpp> +#include "common/ring_buffer.h" + +namespace Common { + +TEST_CASE("RingBuffer: Basic Tests", "[common]") { +    RingBuffer<char, 4, 1> buf; + +    // Pushing values into a ring buffer with space should succeed. +    for (size_t i = 0; i < 4; i++) { +        const char elem = static_cast<char>(i); +        const size_t count = buf.Push(&elem, 1); +        REQUIRE(count == 1); +    } + +    REQUIRE(buf.Size() == 4); + +    // Pushing values into a full ring buffer should fail. +    { +        const char elem = static_cast<char>(42); +        const size_t count = buf.Push(&elem, 1); +        REQUIRE(count == 0); +    } + +    REQUIRE(buf.Size() == 4); + +    // Popping multiple values from a ring buffer with values should succeed. +    { +        const std::vector<char> popped = buf.Pop(2); +        REQUIRE(popped.size() == 2); +        REQUIRE(popped[0] == 0); +        REQUIRE(popped[1] == 1); +    } + +    REQUIRE(buf.Size() == 2); + +    // Popping a single value from a ring buffer with values should succeed. +    { +        const std::vector<char> popped = buf.Pop(1); +        REQUIRE(popped.size() == 1); +        REQUIRE(popped[0] == 2); +    } + +    REQUIRE(buf.Size() == 1); + +    // Pushing more values than space available should partially suceed. +    { +        std::vector<char> to_push(6); +        std::iota(to_push.begin(), to_push.end(), 88); +        const size_t count = buf.Push(to_push); +        REQUIRE(count == 3); +    } + +    REQUIRE(buf.Size() == 4); + +    // Doing an unlimited pop should pop all values. +    { +        const std::vector<char> popped = buf.Pop(); +        REQUIRE(popped.size() == 4); +        REQUIRE(popped[0] == 3); +        REQUIRE(popped[1] == 88); +        REQUIRE(popped[2] == 89); +        REQUIRE(popped[3] == 90); +    } + +    REQUIRE(buf.Size() == 0); +} + +TEST_CASE("RingBuffer: Threaded Test", "[common]") { +    RingBuffer<char, 4, 2> buf; +    const char seed = 42; +    const size_t count = 1000000; +    size_t full = 0; +    size_t empty = 0; + +    const auto next_value = [](std::array<char, 2>& value) { +        value[0] += 1; +        value[1] += 2; +    }; + +    std::thread producer{[&] { +        std::array<char, 2> value = {seed, seed}; +        size_t i = 0; +        while (i < count) { +            if (const size_t c = buf.Push(&value[0], 1); c > 0) { +                REQUIRE(c == 1); +                i++; +                next_value(value); +            } else { +                full++; +                std::this_thread::yield(); +            } +        } +    }}; + +    std::thread consumer{[&] { +        std::array<char, 2> value = {seed, seed}; +        size_t i = 0; +        while (i < count) { +            if (const std::vector<char> v = buf.Pop(1); v.size() > 0) { +                REQUIRE(v.size() == 2); +                REQUIRE(v[0] == value[0]); +                REQUIRE(v[1] == value[1]); +                i++; +                next_value(value); +            } else { +                empty++; +                std::this_thread::yield(); +            } +        } +    }}; + +    producer.join(); +    consumer.join(); + +    REQUIRE(buf.Size() == 0); +    printf("RingBuffer: Threaded Test: full: %zu, empty: %zu\n", full, empty); +} + +} // namespace Common diff --git a/src/video_core/engines/shader_bytecode.h b/src/video_core/engines/shader_bytecode.h index 2db906ea5..58f2904ce 100644 --- a/src/video_core/engines/shader_bytecode.h +++ b/src/video_core/engines/shader_bytecode.h @@ -254,6 +254,15 @@ enum class TextureQueryType : u64 {      BorderColor = 22,  }; +enum class TextureProcessMode : u64 { +    None = 0, +    LZ = 1,  // Unknown, appears to be the same as none. +    LB = 2,  // Load Bias. +    LL = 3,  // Load LOD (LevelOfDetail) +    LBA = 6, // Load Bias. The A is unknown, does not appear to differ with LB +    LLA = 7  // Load LOD. The A is unknown, does not appear to differ with LL +}; +  enum class IpaInterpMode : u64 { Linear = 0, Perspective = 1, Flat = 2, Sc = 3 };  enum class IpaSampleMode : u64 { Default = 0, Centroid = 1, Offset = 2 }; @@ -573,6 +582,7 @@ union Instruction {          BitField<28, 1, u64> array;          BitField<29, 2, TextureType> texture_type;          BitField<31, 4, u64> component_mask; +        BitField<55, 3, TextureProcessMode> process_mode;          bool IsComponentEnabled(size_t component) const {              return ((1ull << component) & component_mask) != 0; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index c59f3af1b..7e1bba67d 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -3,6 +3,7 @@  // Refer to the license.txt file included.  #include <algorithm> +#include <array>  #include <memory>  #include <string>  #include <string_view> @@ -58,6 +59,8 @@ RasterizerOpenGL::RasterizerOpenGL(Core::Frontend::EmuWindow& window, ScreenInfo          if (extension == "GL_ARB_direct_state_access") {              has_ARB_direct_state_access = true; +        } else if (extension == "GL_ARB_multi_bind") { +            has_ARB_multi_bind = true;          } else if (extension == "GL_ARB_separate_shader_objects") {              has_ARB_separate_shader_objects = true;          } else if (extension == "GL_ARB_vertex_attrib_binding") { @@ -644,12 +647,23 @@ u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shad      const auto& shader_stage = maxwell3d.state.shader_stages[static_cast<size_t>(stage)];      const auto& entries = shader->GetShaderEntries().const_buffer_entries; +    constexpr u64 max_binds = Tegra::Engines::Maxwell3D::Regs::MaxConstBuffers; +    std::array<GLuint, max_binds> bind_buffers; +    std::array<GLintptr, max_binds> bind_offsets; +    std::array<GLsizeiptr, max_binds> bind_sizes; + +    ASSERT_MSG(entries.size() <= max_binds, "Exceeded expected number of binding points."); +      // Upload only the enabled buffers from the 16 constbuffers of each shader stage      for (u32 bindpoint = 0; bindpoint < entries.size(); ++bindpoint) {          const auto& used_buffer = entries[bindpoint];          const auto& buffer = shader_stage.const_buffers[used_buffer.GetIndex()];          if (!buffer.enabled) { +            // With disabled buffers set values as zero to unbind them +            bind_buffers[bindpoint] = 0; +            bind_offsets[bindpoint] = 0; +            bind_sizes[bindpoint] = 0;              continue;          } @@ -677,15 +691,20 @@ u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shad          GLintptr const_buffer_offset = buffer_cache.UploadMemory(              buffer.address, size, static_cast<size_t>(uniform_buffer_alignment)); -        glBindBufferRange(GL_UNIFORM_BUFFER, current_bindpoint + bindpoint, -                          buffer_cache.GetHandle(), const_buffer_offset, size); -          // Now configure the bindpoint of the buffer inside the shader          glUniformBlockBinding(shader->GetProgramHandle(),                                shader->GetProgramResourceIndex(used_buffer),                                current_bindpoint + bindpoint); + +        // Prepare values for multibind +        bind_buffers[bindpoint] = buffer_cache.GetHandle(); +        bind_offsets[bindpoint] = const_buffer_offset; +        bind_sizes[bindpoint] = size;      } +    glBindBuffersRange(GL_UNIFORM_BUFFER, current_bindpoint, static_cast<GLsizei>(entries.size()), +                       bind_buffers.data(), bind_offsets.data(), bind_sizes.data()); +      return current_bindpoint + static_cast<u32>(entries.size());  } diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h index 745c3dc0c..163412882 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.h +++ b/src/video_core/renderer_opengl/gl_rasterizer.h @@ -159,6 +159,7 @@ private:      void SyncLogicOpState();      bool has_ARB_direct_state_access = false; +    bool has_ARB_multi_bind = false;      bool has_ARB_separate_shader_objects = false;      bool has_ARB_vertex_attrib_binding = false; diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index fb56decc0..32001e44b 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -116,7 +116,7 @@ static constexpr std::array<FormatTuple, SurfaceParams::MaxPixelFormat> tex_form      {GL_RGBA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, ComponentType::UNorm, false}, // ABGR8U      {GL_RGBA8, GL_RGBA, GL_BYTE, ComponentType::SNorm, false},                     // ABGR8S      {GL_RGBA8UI, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, ComponentType::UInt, false},   // ABGR8UI -    {GL_RGB, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV, ComponentType::UNorm, false},    // B5G6R5U +    {GL_RGB8, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV, ComponentType::UNorm, false},   // B5G6R5U      {GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, ComponentType::UNorm,       false}, // A2B10G10R10U      {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV, ComponentType::UNorm, false}, // A1B5G5R5U diff --git a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp index 582c811e0..2d56370c7 100644 --- a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp +++ b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp @@ -1853,15 +1853,47 @@ private:                      coord = "vec2 coords = vec2(" + x + ", " + y + ");";                      texture_type = Tegra::Shader::TextureType::Texture2D;                  } +                // TODO: make sure coordinates are always indexed to gpr8 and gpr20 is always bias +                // or lod. +                const std::string op_c = regs.GetRegisterAsFloat(instr.gpr20);                  const std::string sampler = GetSampler(instr.sampler, texture_type, false);                  // Add an extra scope and declare the texture coords inside to prevent                  // overwriting them in case they are used as outputs of the texs instruction. +                  shader.AddLine("{");                  ++shader.scope;                  shader.AddLine(coord); -                const std::string texture = "texture(" + sampler + ", coords)"; +                std::string texture; +                switch (instr.tex.process_mode) { +                case Tegra::Shader::TextureProcessMode::None: { +                    texture = "texture(" + sampler + ", coords)"; +                    break; +                } +                case Tegra::Shader::TextureProcessMode::LZ: { +                    texture = "textureLod(" + sampler + ", coords, 0.0)"; +                    break; +                } +                case Tegra::Shader::TextureProcessMode::LB: +                case Tegra::Shader::TextureProcessMode::LBA: { +                    // TODO: Figure if A suffix changes the equation at all. +                    texture = "texture(" + sampler + ", coords, " + op_c + ')'; +                    break; +                } +                case Tegra::Shader::TextureProcessMode::LL: +                case Tegra::Shader::TextureProcessMode::LLA: { +                    // TODO: Figure if A suffix changes the equation at all. +                    texture = "textureLod(" + sampler + ", coords, " + op_c + ')'; +                    break; +                } +                default: { +                    texture = "texture(" + sampler + ", coords)"; +                    LOG_CRITICAL(HW_GPU, "Unhandled texture process mode {}", +                                 static_cast<u32>(instr.tex.process_mode.Value())); +                    UNREACHABLE(); +                } +                }                  size_t dest_elem{};                  for (size_t elem = 0; elem < 4; ++elem) {                      if (!instr.tex.IsComponentEnabled(elem)) { diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index c43e79e78..d229225b4 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -95,6 +95,8 @@ void Config::ReadValues() {      qt_config->beginGroup("Audio");      Settings::values.sink_id = qt_config->value("output_engine", "auto").toString().toStdString(); +    Settings::values.enable_audio_stretching = +        qt_config->value("enable_audio_stretching", true).toBool();      Settings::values.audio_device_id =          qt_config->value("output_device", "auto").toString().toStdString();      Settings::values.volume = qt_config->value("volume", 1).toFloat(); @@ -230,6 +232,7 @@ void Config::SaveValues() {      qt_config->beginGroup("Audio");      qt_config->setValue("output_engine", QString::fromStdString(Settings::values.sink_id)); +    qt_config->setValue("enable_audio_stretching", Settings::values.enable_audio_stretching);      qt_config->setValue("output_device", QString::fromStdString(Settings::values.audio_device_id));      qt_config->setValue("volume", Settings::values.volume);      qt_config->endGroup(); diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp index fbb813f6c..6ea59f2a3 100644 --- a/src/yuzu/configuration/configure_audio.cpp +++ b/src/yuzu/configuration/configure_audio.cpp @@ -46,6 +46,8 @@ void ConfigureAudio::setConfiguration() {      }      ui->output_sink_combo_box->setCurrentIndex(new_sink_index); +    ui->toggle_audio_stretching->setChecked(Settings::values.enable_audio_stretching); +      // The device list cannot be pre-populated (nor listed) until the output sink is known.      updateAudioDevices(new_sink_index); @@ -67,6 +69,7 @@ void ConfigureAudio::applyConfiguration() {      Settings::values.sink_id =          ui->output_sink_combo_box->itemText(ui->output_sink_combo_box->currentIndex())              .toStdString(); +    Settings::values.enable_audio_stretching = ui->toggle_audio_stretching->isChecked();      Settings::values.audio_device_id =          ui->audio_device_combo_box->itemText(ui->audio_device_combo_box->currentIndex())              .toStdString(); diff --git a/src/yuzu/configuration/configure_audio.ui b/src/yuzu/configuration/configure_audio.ui index ef67890dc..a29a0e265 100644 --- a/src/yuzu/configuration/configure_audio.ui +++ b/src/yuzu/configuration/configure_audio.ui @@ -31,6 +31,16 @@          </item>         </layout>        </item> +       <item> +         <widget class="QCheckBox" name="toggle_audio_stretching"> +           <property name="toolTip"> +             <string>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</string> +           </property> +           <property name="text"> +             <string>Enable audio stretching</string> +           </property> +         </widget> +       </item>        <item>         <layout class="QHBoxLayout">          <item> diff --git a/src/yuzu/configuration/configure_gamelist.cpp b/src/yuzu/configuration/configure_gamelist.cpp index 1ae3423cf..0be030434 100644 --- a/src/yuzu/configuration/configure_gamelist.cpp +++ b/src/yuzu/configuration/configure_gamelist.cpp @@ -2,47 +2,51 @@  // Licensed under GPLv2 or any later version  // Refer to the license.txt file included. -#include "core/core.h" +#include <array> +#include <utility> + +#include "common/common_types.h"  #include "core/settings.h"  #include "ui_configure_gamelist.h" -#include "ui_settings.h"  #include "yuzu/configuration/configure_gamelist.h" +#include "yuzu/ui_settings.h" + +namespace { +constexpr std::array<std::pair<u32, const char*>, 5> default_icon_sizes{{ +    std::make_pair(0, QT_TR_NOOP("None")), +    std::make_pair(32, QT_TR_NOOP("Small (32x32)")), +    std::make_pair(64, QT_TR_NOOP("Standard (64x64)")), +    std::make_pair(128, QT_TR_NOOP("Large (128x128)")), +    std::make_pair(256, QT_TR_NOOP("Full Size (256x256)")), +}}; + +constexpr std::array<const char*, 4> row_text_names{{ +    QT_TR_NOOP("Filename"), +    QT_TR_NOOP("Filetype"), +    QT_TR_NOOP("Title ID"), +    QT_TR_NOOP("Title Name"), +}}; +} // Anonymous namespace  ConfigureGameList::ConfigureGameList(QWidget* parent)      : QWidget(parent), ui(new Ui::ConfigureGameList) {      ui->setupUi(this); -    static const std::vector<std::pair<u32, std::string>> default_icon_sizes{ -        std::make_pair(0, "None"),        std::make_pair(32, "Small"), -        std::make_pair(64, "Standard"),   std::make_pair(128, "Large"), -        std::make_pair(256, "Full Size"), -    }; - -    for (const auto& size : default_icon_sizes) { -        ui->icon_size_combobox->addItem(QString::fromStdString(size.second + " (" + -                                                               std::to_string(size.first) + "x" + -                                                               std::to_string(size.first) + ")"), -                                        size.first); -    } - -    static const std::vector<std::string> row_text_names{ -        "Filename", -        "Filetype", -        "Title ID", -        "Title Name", -    }; - -    for (size_t i = 0; i < row_text_names.size(); ++i) { -        ui->row_1_text_combobox->addItem(QString::fromStdString(row_text_names[i]), -                                         QVariant::fromValue(i)); -        ui->row_2_text_combobox->addItem(QString::fromStdString(row_text_names[i]), -                                         QVariant::fromValue(i)); -    } +    InitializeIconSizeComboBox(); +    InitializeRowComboBoxes();      this->setConfiguration();  } -ConfigureGameList::~ConfigureGameList() {} +ConfigureGameList::~ConfigureGameList() = default; + +void ConfigureGameList::applyConfiguration() { +    UISettings::values.show_unknown = ui->show_unknown->isChecked(); +    UISettings::values.icon_size = ui->icon_size_combobox->currentData().toUInt(); +    UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); +    UISettings::values.row_2_text_id = ui->row_2_text_combobox->currentData().toUInt(); +    Settings::Apply(); +}  void ConfigureGameList::setConfiguration() {      ui->show_unknown->setChecked(UISettings::values.show_unknown); @@ -54,10 +58,39 @@ void ConfigureGameList::setConfiguration() {          ui->row_2_text_combobox->findData(UISettings::values.row_2_text_id));  } -void ConfigureGameList::applyConfiguration() { -    UISettings::values.show_unknown = ui->show_unknown->isChecked(); -    UISettings::values.icon_size = ui->icon_size_combobox->currentData().toUInt(); -    UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); -    UISettings::values.row_2_text_id = ui->row_2_text_combobox->currentData().toUInt(); -    Settings::Apply(); +void ConfigureGameList::changeEvent(QEvent* event) { +    if (event->type() == QEvent::LanguageChange) { +        RetranslateUI(); +        return; +    } + +    QWidget::changeEvent(event); +} + +void ConfigureGameList::RetranslateUI() { +    ui->retranslateUi(this); + +    for (int i = 0; i < ui->icon_size_combobox->count(); i++) { +        ui->icon_size_combobox->setItemText(i, tr(default_icon_sizes[i].second)); +    } + +    for (int i = 0; i < ui->row_1_text_combobox->count(); i++) { +        const QString name = tr(row_text_names[i]); + +        ui->row_1_text_combobox->setItemText(i, name); +        ui->row_2_text_combobox->setItemText(i, name); +    } +} + +void ConfigureGameList::InitializeIconSizeComboBox() { +    for (const auto& size : default_icon_sizes) { +        ui->icon_size_combobox->addItem(size.second, size.first); +    } +} + +void ConfigureGameList::InitializeRowComboBoxes() { +    for (size_t i = 0; i < row_text_names.size(); ++i) { +        ui->row_1_text_combobox->addItem(row_text_names[i], QVariant::fromValue(i)); +        ui->row_2_text_combobox->addItem(row_text_names[i], QVariant::fromValue(i)); +    }  } diff --git a/src/yuzu/configuration/configure_gamelist.h b/src/yuzu/configuration/configure_gamelist.h index 94fba6373..ff7406c60 100644 --- a/src/yuzu/configuration/configure_gamelist.h +++ b/src/yuzu/configuration/configure_gamelist.h @@ -23,6 +23,11 @@ public:  private:      void setConfiguration(); -private: +    void changeEvent(QEvent*) override; +    void RetranslateUI(); + +    void InitializeIconSizeComboBox(); +    void InitializeRowComboBoxes(); +      std::unique_ptr<Ui::ConfigureGameList> ui;  }; diff --git a/src/yuzu/debugger/wait_tree.cpp b/src/yuzu/debugger/wait_tree.cpp index 6c2cd967e..dc1023113 100644 --- a/src/yuzu/debugger/wait_tree.cpp +++ b/src/yuzu/debugger/wait_tree.cpp @@ -213,35 +213,35 @@ QString WaitTreeThread::GetText() const {      const auto& thread = static_cast<const Kernel::Thread&>(object);      QString status;      switch (thread.status) { -    case ThreadStatus::Running: +    case Kernel::ThreadStatus::Running:          status = tr("running");          break; -    case ThreadStatus::Ready: +    case Kernel::ThreadStatus::Ready:          status = tr("ready");          break; -    case ThreadStatus::WaitHLEEvent: +    case Kernel::ThreadStatus::WaitHLEEvent:          status = tr("waiting for HLE return");          break; -    case ThreadStatus::WaitSleep: +    case Kernel::ThreadStatus::WaitSleep:          status = tr("sleeping");          break; -    case ThreadStatus::WaitIPC: +    case Kernel::ThreadStatus::WaitIPC:          status = tr("waiting for IPC reply");          break; -    case ThreadStatus::WaitSynchAll: -    case ThreadStatus::WaitSynchAny: +    case Kernel::ThreadStatus::WaitSynchAll: +    case Kernel::ThreadStatus::WaitSynchAny:          status = tr("waiting for objects");          break; -    case ThreadStatus::WaitMutex: +    case Kernel::ThreadStatus::WaitMutex:          status = tr("waiting for mutex");          break; -    case ThreadStatus::WaitArb: +    case Kernel::ThreadStatus::WaitArb:          status = tr("waiting for address arbiter");          break; -    case ThreadStatus::Dormant: +    case Kernel::ThreadStatus::Dormant:          status = tr("dormant");          break; -    case ThreadStatus::Dead: +    case Kernel::ThreadStatus::Dead:          status = tr("dead");          break;      } @@ -254,23 +254,23 @@ QString WaitTreeThread::GetText() const {  QColor WaitTreeThread::GetColor() const {      const auto& thread = static_cast<const Kernel::Thread&>(object);      switch (thread.status) { -    case ThreadStatus::Running: +    case Kernel::ThreadStatus::Running:          return QColor(Qt::GlobalColor::darkGreen); -    case ThreadStatus::Ready: +    case Kernel::ThreadStatus::Ready:          return QColor(Qt::GlobalColor::darkBlue); -    case ThreadStatus::WaitHLEEvent: -    case ThreadStatus::WaitIPC: +    case Kernel::ThreadStatus::WaitHLEEvent: +    case Kernel::ThreadStatus::WaitIPC:          return QColor(Qt::GlobalColor::darkRed); -    case ThreadStatus::WaitSleep: +    case Kernel::ThreadStatus::WaitSleep:          return QColor(Qt::GlobalColor::darkYellow); -    case ThreadStatus::WaitSynchAll: -    case ThreadStatus::WaitSynchAny: -    case ThreadStatus::WaitMutex: -    case ThreadStatus::WaitArb: +    case Kernel::ThreadStatus::WaitSynchAll: +    case Kernel::ThreadStatus::WaitSynchAny: +    case Kernel::ThreadStatus::WaitMutex: +    case Kernel::ThreadStatus::WaitArb:          return QColor(Qt::GlobalColor::red); -    case ThreadStatus::Dormant: +    case Kernel::ThreadStatus::Dormant:          return QColor(Qt::GlobalColor::darkCyan); -    case ThreadStatus::Dead: +    case Kernel::ThreadStatus::Dead:          return QColor(Qt::GlobalColor::gray);      default:          return WaitTreeItem::GetColor(); @@ -284,13 +284,13 @@ std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeThread::GetChildren() const {      QString processor;      switch (thread.processor_id) { -    case ThreadProcessorId::THREADPROCESSORID_DEFAULT: +    case Kernel::ThreadProcessorId::THREADPROCESSORID_DEFAULT:          processor = tr("default");          break; -    case ThreadProcessorId::THREADPROCESSORID_0: -    case ThreadProcessorId::THREADPROCESSORID_1: -    case ThreadProcessorId::THREADPROCESSORID_2: -    case ThreadProcessorId::THREADPROCESSORID_3: +    case Kernel::ThreadProcessorId::THREADPROCESSORID_0: +    case Kernel::ThreadProcessorId::THREADPROCESSORID_1: +    case Kernel::ThreadProcessorId::THREADPROCESSORID_2: +    case Kernel::ThreadProcessorId::THREADPROCESSORID_3:          processor = tr("core %1").arg(thread.processor_id);          break;      default: @@ -314,8 +314,8 @@ std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeThread::GetChildren() const {      else          list.push_back(std::make_unique<WaitTreeText>(tr("not waiting for mutex"))); -    if (thread.status == ThreadStatus::WaitSynchAny || -        thread.status == ThreadStatus::WaitSynchAll) { +    if (thread.status == Kernel::ThreadStatus::WaitSynchAny || +        thread.status == Kernel::ThreadStatus::WaitSynchAll) {          list.push_back(std::make_unique<WaitTreeObjectList>(thread.wait_objects,                                                              thread.IsSleepingOnWaitAll()));      } diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 05a4a55e8..22a317737 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -449,6 +449,8 @@ QStringList GMainWindow::GetUnsupportedGLExtensions() {          unsupported_ext.append("ARB_base_instance");      if (!GLAD_GL_ARB_texture_storage)          unsupported_ext.append("ARB_texture_storage"); +    if (!GLAD_GL_ARB_multi_bind) +        unsupported_ext.append("ARB_multi_bind");      // Extensions required to support some texture formats.      if (!GLAD_GL_EXT_texture_compression_s3tc) diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index f00b5a66b..991abda2e 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -108,6 +108,8 @@ void Config::ReadValues() {      // Audio      Settings::values.sink_id = sdl2_config->Get("Audio", "output_engine", "auto"); +    Settings::values.enable_audio_stretching = +        sdl2_config->GetBoolean("Audio", "enable_audio_stretching", true);      Settings::values.audio_device_id = sdl2_config->Get("Audio", "output_device", "auto");      Settings::values.volume = sdl2_config->GetReal("Audio", "volume", 1); diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h index 6ed9e7962..002a4ec15 100644 --- a/src/yuzu_cmd/default_ini.h +++ b/src/yuzu_cmd/default_ini.h @@ -150,6 +150,12 @@ swap_screen =  # auto (default): Auto-select, null: No audio output, cubeb: Cubeb audio engine (if available)  output_engine = +# Whether or not to enable the audio-stretching post-processing effect. +# This effect adjusts audio speed to match emulation speed and helps prevent audio stutter, +# at the cost of increasing audio latency. +# 0: No, 1 (default): Yes +enable_audio_stretching = +  # Which audio device to use.  # auto (default): Auto-select  output_device = diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp index d213929bd..0733301b2 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp @@ -96,6 +96,8 @@ bool EmuWindow_SDL2::SupportsRequiredGLExtensions() {          unsupported_ext.push_back("ARB_base_instance");      if (!GLAD_GL_ARB_texture_storage)          unsupported_ext.push_back("ARB_texture_storage"); +    if (!GLAD_GL_ARB_multi_bind) +        unsupported_ext.push_back("ARB_multi_bind");      // Extensions required to support some texture formats.      if (!GLAD_GL_EXT_texture_compression_s3tc)  | 
