Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #9956 from Pokechu22/non-power-of-2-wrap-2
VideoCommon: Manually handle texture wrapping and sampling
  • Loading branch information
JMC47 committed Nov 18, 2021
2 parents 8b57aad + 95b9941 commit 6f4bbac
Show file tree
Hide file tree
Showing 46 changed files with 749 additions and 326 deletions.
Expand Up @@ -198,6 +198,8 @@ public enum BooleanSetting implements AbstractBooleanSetting
GFX_HACK_EFB_EMULATE_FORMAT_CHANGES(Settings.FILE_GFX, Settings.SECTION_GFX_HACKS,
"EFBEmulateFormatChanges", false),
GFX_HACK_VERTEX_ROUDING(Settings.FILE_GFX, Settings.SECTION_GFX_HACKS, "VertexRounding", false),
GFX_HACK_FAST_TEXTURE_SAMPLING(Settings.FILE_GFX, Settings.SECTION_GFX_HACKS,
"FastTextureSampling", true),

LOGGER_WRITE_TO_FILE(Settings.FILE_LOGGER, Settings.SECTION_LOGGER_OPTIONS, "WriteToFile", false),

Expand Down
Expand Up @@ -744,6 +744,8 @@ private void addAdvancedGraphicsSettings(ArrayList<SettingsItem> sl)
R.string.backend_multithreading, R.string.backend_multithreading_description));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.GFX_HACK_EFB_DEFER_INVALIDATION,
R.string.defer_efb_invalidation, R.string.defer_efb_invalidation_description));
sl.add(new InvertedCheckBoxSetting(mContext, BooleanSetting.GFX_HACK_FAST_TEXTURE_SAMPLING,
R.string.manual_texture_sampling, R.string.manual_texture_sampling_description));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.GFX_INTERNAL_RESOLUTION_FRAME_DUMPS,
R.string.internal_resolution_dumps, R.string.internal_resolution_dumps_description));

Expand Down
2 changes: 2 additions & 0 deletions Source/Android/app/src/main/res/values/strings.xml
Expand Up @@ -303,6 +303,8 @@
<string name="backend_multithreading_description">Enables graphics backend multithreading (Vulkan only). May affect performance. If unsure, leave this unchecked.</string>
<string name="defer_efb_invalidation">Defer EFB Cache Invalidation</string>
<string name="defer_efb_invalidation_description">Defers invalidation of the EFB access cache until a GPU synchronization command is executed. May improve performance in some games at the cost of stability. If unsure, leave this unchecked.</string>
<string name="manual_texture_sampling">Manual Texture Sampling</string>
<string name="manual_texture_sampling_description">Use a manual implementation of texture sampling instead of the graphics backend\'s built-in functionality.</string>
<string name="internal_resolution_dumps">Dump Frames at Internal Resolution</string>
<string name="internal_resolution_dumps_description">Creates frame dumps and screenshots at the internal resolution of the renderer, rather than the size of the window it is displayed within. If the aspect ratio is widescreen, the output image will be scaled horizontally to preserve the vertical resolution.</string>
<string name="debugging">Debugging</string>
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/Common/BitField.h
Expand Up @@ -149,6 +149,7 @@ struct BitField

constexpr T Value() const { return Value(std::is_signed<T>()); }
constexpr operator T() const { return Value(); }
static constexpr bool IsSigned() { return std::is_signed<T>(); }
static constexpr std::size_t StartBit() { return position; }
static constexpr std::size_t NumBits() { return bits; }

Expand Down Expand Up @@ -244,6 +245,7 @@ struct BitFieldArray
BitFieldArray& operator=(const BitFieldArray&) = delete;

public:
constexpr bool IsSigned() const { return std::is_signed<T>(); }
constexpr std::size_t StartBit() const { return position; }
constexpr std::size_t NumBits() const { return bits; }
constexpr std::size_t Size() const { return size; }
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/Core/Config/GraphicsSettings.cpp
Expand Up @@ -150,6 +150,8 @@ const Info<bool> GFX_HACK_EFB_EMULATE_FORMAT_CHANGES{
const Info<bool> GFX_HACK_VERTEX_ROUDING{{System::GFX, "Hacks", "VertexRounding"}, false};
const Info<u32> GFX_HACK_MISSING_COLOR_VALUE{{System::GFX, "Hacks", "MissingColorValue"},
0xFFFFFFFF};
const Info<bool> GFX_HACK_FAST_TEXTURE_SAMPLING{{System::GFX, "Hacks", "FastTextureSampling"},
true};

// Graphics.GameSpecific

Expand Down
1 change: 1 addition & 0 deletions Source/Core/Core/Config/GraphicsSettings.h
Expand Up @@ -123,6 +123,7 @@ extern const Info<bool> GFX_HACK_COPY_EFB_SCALED;
extern const Info<bool> GFX_HACK_EFB_EMULATE_FORMAT_CHANGES;
extern const Info<bool> GFX_HACK_VERTEX_ROUDING;
extern const Info<u32> GFX_HACK_MISSING_COLOR_VALUE;
extern const Info<bool> GFX_HACK_FAST_TEXTURE_SAMPLING;

// Graphics.GameSpecific

Expand Down
1 change: 0 additions & 1 deletion Source/Core/DolphinLib.props
Expand Up @@ -644,7 +644,6 @@
<ClInclude Include="VideoCommon\PostProcessing.h" />
<ClInclude Include="VideoCommon\RenderBase.h" />
<ClInclude Include="VideoCommon\RenderState.h" />
<ClInclude Include="VideoCommon\SamplerCommon.h" />
<ClInclude Include="VideoCommon\ShaderCache.h" />
<ClInclude Include="VideoCommon\ShaderGenCommon.h" />
<ClInclude Include="VideoCommon\Statistics.h" />
Expand Down
15 changes: 15 additions & 0 deletions Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp
Expand Up @@ -138,8 +138,11 @@ void AdvancedWidget::CreateWidgets()

m_defer_efb_access_invalidation =
new GraphicsBool(tr("Defer EFB Cache Invalidation"), Config::GFX_HACK_EFB_DEFER_INVALIDATION);
m_manual_texture_sampling =
new GraphicsBool(tr("Manual Texture Sampling"), Config::GFX_HACK_FAST_TEXTURE_SAMPLING, true);

experimental_layout->addWidget(m_defer_efb_access_invalidation, 0, 0);
experimental_layout->addWidget(m_manual_texture_sampling, 0, 1);

main_layout->addWidget(debugging_box);
main_layout->addWidget(utility_box);
Expand Down Expand Up @@ -266,6 +269,17 @@ void AdvancedWidget::AddDescriptions()
"<br><br>May improve performance in some games which rely on CPU EFB Access at the cost "
"of stability.<br><br><dolphin_emphasis>If unsure, leave this "
"unchecked.</dolphin_emphasis>");
static const char TR_MANUAL_TEXTURE_SAMPLING_DESCRIPTION[] = QT_TR_NOOP(
"Use a manual implementation of texture sampling instead of the graphics backend's built-in "
"functionality.<br><br>"
"This setting can fix graphical issues in some games on certain GPUs, most commonly vertical "
"lines on FMVs. In addition to this, enabling Manual Texture Sampling will allow for correct "
"emulation of texture wrapping special cases (at 1x IR or when scaled EFB is disabled, and "
"with custom textures disabled) and better emulates Level of Detail calculation.<br><br>"
"This comes at the cost of potentially worse performance, especially at higher internal "
"resolutions; additionally, Anisotropic Filtering is currently incompatible with Manual "
"Texture Sampling.<br><br>"
"<dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");

#ifdef _WIN32
static const char TR_BORDERLESS_FULLSCREEN_DESCRIPTION[] = QT_TR_NOOP(
Expand Down Expand Up @@ -299,4 +313,5 @@ void AdvancedWidget::AddDescriptions()
m_borderless_fullscreen->SetDescription(tr(TR_BORDERLESS_FULLSCREEN_DESCRIPTION));
#endif
m_defer_efb_access_invalidation->SetDescription(tr(TR_DEFER_EFB_ACCESS_INVALIDATION_DESCRIPTION));
m_manual_texture_sampling->SetDescription(tr(TR_MANUAL_TEXTURE_SAMPLING_DESCRIPTION));
}
1 change: 1 addition & 0 deletions Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.h
Expand Up @@ -61,4 +61,5 @@ class AdvancedWidget final : public GraphicsWidget

// Experimental
GraphicsBool* m_defer_efb_access_invalidation;
GraphicsBool* m_manual_texture_sampling;
};
2 changes: 1 addition & 1 deletion Source/Core/DolphinQt/Config/Graphics/HacksWidget.h
Expand Up @@ -26,6 +26,7 @@ class HacksWidget final : public GraphicsWidget
GraphicsBool* m_skip_efb_cpu;
GraphicsBool* m_ignore_format_changes;
GraphicsBool* m_store_efb_copies;
GraphicsBool* m_defer_efb_copies;

// Texture Cache
QLabel* m_accuracy_label;
Expand All @@ -42,7 +43,6 @@ class HacksWidget final : public GraphicsWidget
GraphicsBool* m_disable_bounding_box;
GraphicsBool* m_vertex_rounding;
GraphicsBool* m_save_texture_cache_state;
GraphicsBool* m_defer_efb_copies;

void CreateWidgets();
void ConnectWidgets();
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/VideoBackends/D3D/D3DMain.cpp
Expand Up @@ -106,6 +106,8 @@ void VideoBackend::FillBackendInfo()
g_Config.backend_info.bSupportsSSAA = true;
g_Config.backend_info.bSupportsShaderBinaries = true;
g_Config.backend_info.bSupportsPipelineCacheData = false;
g_Config.backend_info.bSupportsCoarseDerivatives = true;
g_Config.backend_info.bSupportsTextureQueryLevels = true;
g_Config.backend_info.bSupportsLogicOp = D3D::SupportsLogicOp(g_Config.iAdapter);

g_Config.backend_info.Adapters = D3DCommon::GetAdapterNames();
Expand Down
30 changes: 15 additions & 15 deletions Source/Core/VideoBackends/D3D/D3DState.cpp
Expand Up @@ -303,43 +303,43 @@ StateCache::~StateCache() = default;
ID3D11SamplerState* StateCache::Get(SamplerState state)
{
std::lock_guard<std::mutex> guard(m_lock);
auto it = m_sampler.find(state.hex);
auto it = m_sampler.find(state);
if (it != m_sampler.end())
return it->second.Get();

D3D11_SAMPLER_DESC sampdc = CD3D11_SAMPLER_DESC(CD3D11_DEFAULT());
if (state.mipmap_filter == SamplerState::Filter::Linear)
if (state.tm0.mipmap_filter == FilterMode::Linear)
{
if (state.min_filter == SamplerState::Filter::Linear)
sampdc.Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
if (state.tm0.min_filter == FilterMode::Linear)
sampdc.Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D11_FILTER_MIN_MAG_MIP_LINEAR :
D3D11_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR;
else
sampdc.Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
sampdc.Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D11_FILTER_MIN_POINT_MAG_MIP_LINEAR :
D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR;
}
else
{
if (state.min_filter == SamplerState::Filter::Linear)
sampdc.Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
if (state.tm0.min_filter == FilterMode::Linear)
sampdc.Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT :
D3D11_FILTER_MIN_LINEAR_MAG_MIP_POINT;
else
sampdc.Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
sampdc.Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT :
D3D11_FILTER_MIN_MAG_MIP_POINT;
}

static constexpr std::array<D3D11_TEXTURE_ADDRESS_MODE, 3> address_modes = {
{D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_TEXTURE_ADDRESS_WRAP, D3D11_TEXTURE_ADDRESS_MIRROR}};
sampdc.AddressU = address_modes[static_cast<u32>(state.wrap_u.Value())];
sampdc.AddressV = address_modes[static_cast<u32>(state.wrap_v.Value())];
sampdc.MaxLOD = state.max_lod / 16.f;
sampdc.MinLOD = state.min_lod / 16.f;
sampdc.MipLODBias = (s32)state.lod_bias / 256.f;
sampdc.AddressU = address_modes[static_cast<u32>(state.tm0.wrap_u.Value())];
sampdc.AddressV = address_modes[static_cast<u32>(state.tm0.wrap_v.Value())];
sampdc.MaxLOD = state.tm1.max_lod / 16.f;
sampdc.MinLOD = state.tm1.min_lod / 16.f;
sampdc.MipLODBias = state.tm0.lod_bias / 256.f;

if (state.anisotropic_filtering)
if (state.tm0.anisotropic_filtering)
{
sampdc.Filter = D3D11_FILTER_ANISOTROPIC;
sampdc.MaxAnisotropy = 1u << g_ActiveConfig.iMaxAnisotropy;
Expand All @@ -348,7 +348,7 @@ ID3D11SamplerState* StateCache::Get(SamplerState state)
ComPtr<ID3D11SamplerState> res;
HRESULT hr = D3D::device->CreateSamplerState(&sampdc, res.GetAddressOf());
CHECK(SUCCEEDED(hr), "Creating D3D sampler state failed");
return m_sampler.emplace(state.hex, std::move(res)).first->second.Get();
return m_sampler.emplace(state, std::move(res)).first->second.Get();
}

ID3D11BlendState* StateCache::Get(BlendingState state)
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/VideoBackends/D3D/D3DState.h
Expand Up @@ -37,7 +37,7 @@ class StateCache
std::unordered_map<u32, ComPtr<ID3D11DepthStencilState>> m_depth;
std::unordered_map<u32, ComPtr<ID3D11RasterizerState>> m_raster;
std::unordered_map<u32, ComPtr<ID3D11BlendState>> m_blend;
std::unordered_map<SamplerState::StorageType, ComPtr<ID3D11SamplerState>> m_sampler;
std::unordered_map<SamplerState, ComPtr<ID3D11SamplerState>> m_sampler;
std::mutex m_lock;
};

Expand Down
30 changes: 15 additions & 15 deletions Source/Core/VideoBackends/D3D12/DescriptorHeapManager.cpp
Expand Up @@ -85,32 +85,32 @@ SamplerHeapManager::~SamplerHeapManager() = default;

static void GetD3DSamplerDesc(D3D12_SAMPLER_DESC* desc, const SamplerState& state)
{
if (state.mipmap_filter == SamplerState::Filter::Linear)
if (state.tm0.mipmap_filter == FilterMode::Linear)
{
if (state.min_filter == SamplerState::Filter::Linear)
if (state.tm0.min_filter == FilterMode::Linear)
{
desc->Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
desc->Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D12_FILTER_MIN_MAG_MIP_LINEAR :
D3D12_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR;
}
else
{
desc->Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
desc->Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D12_FILTER_MIN_POINT_MAG_MIP_LINEAR :
D3D12_FILTER_MIN_MAG_POINT_MIP_LINEAR;
}
}
else
{
if (state.min_filter == SamplerState::Filter::Linear)
if (state.tm0.min_filter == FilterMode::Linear)
{
desc->Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
desc->Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT :
D3D12_FILTER_MIN_LINEAR_MAG_MIP_POINT;
}
else
{
desc->Filter = (state.mag_filter == SamplerState::Filter::Linear) ?
desc->Filter = (state.tm0.mag_filter == FilterMode::Linear) ?
D3D12_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT :
D3D12_FILTER_MIN_MAG_MIP_POINT;
}
Expand All @@ -119,15 +119,15 @@ static void GetD3DSamplerDesc(D3D12_SAMPLER_DESC* desc, const SamplerState& stat
static constexpr std::array<D3D12_TEXTURE_ADDRESS_MODE, 3> address_modes = {
{D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_WRAP,
D3D12_TEXTURE_ADDRESS_MODE_MIRROR}};
desc->AddressU = address_modes[static_cast<u32>(state.wrap_u.Value())];
desc->AddressV = address_modes[static_cast<u32>(state.wrap_v.Value())];
desc->AddressU = address_modes[static_cast<u32>(state.tm0.wrap_u.Value())];
desc->AddressV = address_modes[static_cast<u32>(state.tm0.wrap_v.Value())];
desc->AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
desc->MaxLOD = state.max_lod / 16.f;
desc->MinLOD = state.min_lod / 16.f;
desc->MipLODBias = static_cast<s32>(state.lod_bias) / 256.f;
desc->MaxLOD = state.tm1.max_lod / 16.f;
desc->MinLOD = state.tm1.min_lod / 16.f;
desc->MipLODBias = static_cast<s32>(state.tm0.lod_bias) / 256.f;
desc->ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;

if (state.anisotropic_filtering)
if (state.tm0.anisotropic_filtering)
{
desc->Filter = D3D12_FILTER_ANISOTROPIC;
desc->MaxAnisotropy = 1u << g_ActiveConfig.iMaxAnisotropy;
Expand All @@ -136,7 +136,7 @@ static void GetD3DSamplerDesc(D3D12_SAMPLER_DESC* desc, const SamplerState& stat

bool SamplerHeapManager::Lookup(const SamplerState& ss, D3D12_CPU_DESCRIPTOR_HANDLE* handle)
{
const auto it = m_sampler_map.find(ss.hex);
const auto it = m_sampler_map.find(ss);
if (it != m_sampler_map.end())
{
*handle = it->second;
Expand All @@ -158,7 +158,7 @@ bool SamplerHeapManager::Lookup(const SamplerState& ss, D3D12_CPU_DESCRIPTOR_HAN
m_current_offset * m_descriptor_increment_size};
g_dx_context->GetDevice()->CreateSampler(&desc, new_handle);

m_sampler_map.emplace(ss.hex, new_handle);
m_sampler_map.emplace(ss, new_handle);
m_current_offset++;
*handle = new_handle;
return true;
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/VideoBackends/D3D12/DescriptorHeapManager.h
Expand Up @@ -68,6 +68,6 @@ class SamplerHeapManager final

D3D12_CPU_DESCRIPTOR_HANDLE m_heap_base_cpu{};

std::unordered_map<SamplerState::StorageType, D3D12_CPU_DESCRIPTOR_HANDLE> m_sampler_map;
std::unordered_map<SamplerState, D3D12_CPU_DESCRIPTOR_HANDLE> m_sampler_map;
};
} // namespace DX12
2 changes: 2 additions & 0 deletions Source/Core/VideoBackends/D3D12/VideoBackend.cpp
Expand Up @@ -82,6 +82,8 @@ void VideoBackend::FillBackendInfo()
g_Config.backend_info.AAModes = DXContext::GetAAModes(g_Config.iAdapter);
g_Config.backend_info.bSupportsShaderBinaries = true;
g_Config.backend_info.bSupportsPipelineCacheData = true;
g_Config.backend_info.bSupportsCoarseDerivatives = true;
g_Config.backend_info.bSupportsTextureQueryLevels = true;

// We can only check texture support once we have a device.
if (g_dx_context)
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/VideoBackends/Null/NullBackend.cpp
Expand Up @@ -55,6 +55,8 @@ void VideoBackend::InitBackendInfo()
g_Config.backend_info.bSupportsPartialDepthCopies = false;
g_Config.backend_info.bSupportsShaderBinaries = false;
g_Config.backend_info.bSupportsPipelineCacheData = false;
g_Config.backend_info.bSupportsCoarseDerivatives = false;
g_Config.backend_info.bSupportsTextureQueryLevels = false;

// aamodes: We only support 1 sample, so no MSAA
g_Config.backend_info.Adapters.clear();
Expand Down
4 changes: 3 additions & 1 deletion Source/Core/VideoBackends/OGL/OGLMain.cpp
Expand Up @@ -99,14 +99,16 @@ void VideoBackend::InitBackendInfo()
g_Config.backend_info.bSupportsGPUTextureDecoding = true;
g_Config.backend_info.bSupportsBBox = true;

// Overwritten in Render.cpp later
// Overwritten in OGLRender.cpp later
g_Config.backend_info.bSupportsDualSourceBlend = true;
g_Config.backend_info.bSupportsPrimitiveRestart = true;
g_Config.backend_info.bSupportsPaletteConversion = true;
g_Config.backend_info.bSupportsClipControl = true;
g_Config.backend_info.bSupportsDepthClamp = true;
g_Config.backend_info.bSupportsST3CTextures = false;
g_Config.backend_info.bSupportsBPTCTextures = false;
g_Config.backend_info.bSupportsCoarseDerivatives = false;
g_Config.backend_info.bSupportsTextureQueryLevels = false;

g_Config.backend_info.Adapters.clear();

Expand Down
4 changes: 4 additions & 0 deletions Source/Core/VideoBackends/OGL/OGLRender.cpp
Expand Up @@ -483,6 +483,10 @@ Renderer::Renderer(std::unique_ptr<GLContext> main_gl_context, float backbuffer_
GLExtensions::Supports("GL_EXT_texture_compression_s3tc");
g_Config.backend_info.bSupportsBPTCTextures =
GLExtensions::Supports("GL_ARB_texture_compression_bptc");
g_Config.backend_info.bSupportsCoarseDerivatives =
GLExtensions::Supports("GL_ARB_derivative_control") || GLExtensions::Version() >= 450;
g_Config.backend_info.bSupportsTextureQueryLevels =
GLExtensions::Supports("GL_ARB_texture_query_levels") || GLExtensions::Version() >= 430;

if (m_main_gl_context->IsGLES())
{
Expand Down
8 changes: 8 additions & 0 deletions Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp
Expand Up @@ -747,6 +747,8 @@ void ProgramShaderCache::CreateHeader()
"%s\n" // shader image load store
"%s\n" // shader framebuffer fetch
"%s\n" // shader thread shuffle
"%s\n" // derivative control
"%s\n" // query levels

// Precision defines for GLSL ES
"%s\n"
Expand Down Expand Up @@ -826,6 +828,12 @@ void ProgramShaderCache::CreateHeader()
"#extension GL_ARB_shader_image_load_store : enable" :
"",
framebuffer_fetch_string.c_str(), shader_shuffle_string.c_str(),
g_ActiveConfig.backend_info.bSupportsCoarseDerivatives ?
"#extension GL_ARB_derivative_control : enable" :
"",
g_ActiveConfig.backend_info.bSupportsTextureQueryLevels ?
"#extension GL_ARB_texture_query_levels : enable" :
"",
is_glsles ? "precision highp float;" : "", is_glsles ? "precision highp int;" : "",
is_glsles ? "precision highp sampler2DArray;" : "",
(is_glsles && g_ActiveConfig.backend_info.bSupportsPaletteConversion) ?
Expand Down

0 comments on commit 6f4bbac

Please sign in to comment.