diff --git a/Core/Config.cpp b/Core/Config.cpp index 8e11eab12a..bc15c6c1f2 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -144,6 +144,7 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) { graphics->Get("RenderingMode", &iRenderingMode, renderingModeDefault); graphics->Get("SoftwareRendering", &bSoftwareRendering, false); graphics->Get("HardwareTransform", &bHardwareTransform, true); + graphics->Get("SoftwareSkinning", &bSoftwareSkinning, false); graphics->Get("TextureFiltering", &iTexFiltering, 1); // Auto on Windows, 1x elsewhere. Maybe change to 2x on large screens? #ifdef _WIN32 @@ -401,6 +402,7 @@ void Config::Save() { graphics->Set("RenderingMode", iRenderingMode); graphics->Set("SoftwareRendering", bSoftwareRendering); graphics->Set("HardwareTransform", bHardwareTransform); + graphics->Set("SoftwareSkinning", bSoftwareSkinning); graphics->Set("TextureFiltering", iTexFiltering); graphics->Set("InternalResolution", iInternalResolution); graphics->Set("FrameSkip", iFrameSkip); diff --git a/Core/Config.h b/Core/Config.h index b5a4073b3f..2922d4c005 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -64,6 +64,8 @@ public: // GFX bool bSoftwareRendering; bool bHardwareTransform; // only used in the GLES backend + bool bSoftwareSkinning; // may speed up some games + int iRenderingMode; // 0 = non-buffered rendering 1 = buffered rendering 2 = Read Framebuffer to memory (CPU) 3 = Read Framebuffer to memory (GPU) int iTexFiltering; // 1 = off , 2 = nearest , 3 = linear , 4 = linear(CG) #ifdef BLACKBERRY diff --git a/GPU/GLES/GLES_GPU.cpp b/GPU/GLES/GLES_GPU.cpp index 66a5223254..19343dc1db 100644 --- a/GPU/GLES/GLES_GPU.cpp +++ b/GPU/GLES/GLES_GPU.cpp @@ -430,6 +430,12 @@ GLES_GPU::GLES_GPU() commandFlags_[GE_CMD_TEXOFFSETV] &= ~FLAG_FLUSHBEFOREONCHANGE; } + // TODO: Can't turn this optimization on until we don't decode everything in one go + // but instead decode for every draw call when sw skinning + if (g_Config.bSoftwareSkinning) { + // commandFlags_[GE_CMD_VERTEXTYPE] &= ~FLAG_FLUSHBEFOREONCHANGE; + } + BuildReportingInfo(); } @@ -868,8 +874,21 @@ void GLES_GPU::ExecuteOpInternal(u32 op, u32 diff) { break; case GE_CMD_VERTEXTYPE: - if (diff) - shaderManager_->DirtyUniform(DIRTY_UVSCALEOFFSET); + if (diff) { + if (!g_Config.bSoftwareSkinning) { + shaderManager_->DirtyUniform(DIRTY_UVSCALEOFFSET); + } else if (false) { + // TODO: Can't turn this optimization on until we don't decode everything in one go + // but instead decode for every draw call when sw skinning + if (diff & ~GE_VTYPE_WEIGHTCOUNT_MASK) { + // Restore and flush + gstate.vertType ^= diff; + Flush(); + gstate.vertType ^= diff; + shaderManager_->DirtyUniform(DIRTY_UVSCALEOFFSET); + } + } + } break; case GE_CMD_REGION1: @@ -1375,9 +1394,14 @@ void GLES_GPU::ExecuteOpInternal(u32 op, u32 diff) { int num = gstate.boneMatrixNumber & 0x7F; float newVal = getFloat24(data); if (num < 96 && newVal != gstate.boneMatrix[num]) { - Flush(); + // Bone matrices should NOT flush when software skinning is enabled! + // TODO: Also check for morph... + // TODO: Can't turn this optimizatoin on until we decode per drawcall when sw skinning. + if (true || !g_Config.bSoftwareSkinning) { + Flush(); + shaderManager_->DirtyUniform(DIRTY_BONEMATRIX0 << (num / 12)); + } gstate.boneMatrix[num] = newVal; - shaderManager_->DirtyUniform(DIRTY_BONEMATRIX0 << (num / 12)); } num++; gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (num & 0x7F); diff --git a/GPU/GLES/TransformPipeline.cpp b/GPU/GLES/TransformPipeline.cpp index b6de946821..d1f70a69f7 100644 --- a/GPU/GLES/TransformPipeline.cpp +++ b/GPU/GLES/TransformPipeline.cpp @@ -524,8 +524,14 @@ void TransformDrawEngine::DoFlush() { int vertexCount = 0; int maxIndex = 0; bool useElements = true; + // Cannot cache vertex data with morph enabled. - if (g_Config.bVertexCache && !(lastVType_ & GE_VTYPE_MORPHCOUNT_MASK)) { + bool useCache = g_Config.bVertexCache && !(lastVType_ & GE_VTYPE_MORPHCOUNT_MASK); + // Also avoid caching when software skinning. + if (g_Config.bSoftwareSkinning && (lastVType_ & GE_VTYPE_WEIGHT_MASK)) + useCache = false; + + if (useCache) { u32 id = ComputeFastDCID(); auto iter = vai_.find(id); VertexArrayInfo *vai; diff --git a/GPU/GLES/VertexDecoder.cpp b/GPU/GLES/VertexDecoder.cpp index abe37a98ac..6e848708a5 100644 --- a/GPU/GLES/VertexDecoder.cpp +++ b/GPU/GLES/VertexDecoder.cpp @@ -15,11 +15,10 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include "math/lin/matrix4x4.h" - #include "Core/Config.h" #include "Core/MemMap.h" #include "GPU/ge_constants.h" +#include "GPU/Math3D.h" #include "VertexDecoder.h" #include "VertexShaderGenerator.h" @@ -36,6 +35,10 @@ static const u8 nrmsize[4] = {0,3,6,12}, nrmalign[4] = {0,1,2,4}; static const u8 possize[4] = {0,3,6,12}, posalign[4] = {0,1,2,4}; static const u8 wtsize[4] = {0,1,2,4}, wtalign[4] = {0,1,2,4}; +// When software skinning. This should be stored in registers instead of memory +// when jitting. +float skinMatrix[12]; + inline int align(int n, int align) { return (n + (align - 1)) & ~(align - 1); } @@ -112,6 +115,57 @@ void VertexDecoder::Step_WeightsFloat() const wt[j++] = 0.0f; } +void VertexDecoder::Step_WeightsU8Skin() const +{ + memset(skinMatrix, 0, sizeof(skinMatrix)); + u8 *wt = (u8 *)(decoded_ + decFmt.w0off); + const u8 *wdata = (const u8*)(ptr_); + for (int j = 0; j < nweights; j++) { + const float *bone = &gstate.boneMatrix[j * 12]; + if (wdata[j] != 0) { + float weight = wdata[j] / 128.0f; + for (int i = 0; i < 12; i++) { + skinMatrix[i] += weight * bone[i]; + } + } + } +} + +void VertexDecoder::Step_WeightsU16Skin() const +{ + memset(skinMatrix, 0, sizeof(skinMatrix)); + u16 *wt = (u16 *)(decoded_ + decFmt.w0off); + const u16 *wdata = (const u16*)(ptr_); + for (int j = 0; j < nweights; j++) { + const float *bone = &gstate.boneMatrix[j * 12]; + if (wdata[j] != 0) { + float weight = wdata[j] / 32768.0f; + for (int i = 0; i < 12; i++) { + skinMatrix[i] += weight * bone[i]; + } + } + } +} + +// Float weights should be uncommon, we can live with having to multiply these by 2.0 +// to avoid special checks in the vertex shader generator. +// (PSP uses 0.0-2.0 fixed point numbers for weights) +void VertexDecoder::Step_WeightsFloatSkin() const +{ + memset(skinMatrix, 0, sizeof(skinMatrix)); + float *wt = (float *)(decoded_ + decFmt.w0off); + const float *wdata = (const float*)(ptr_); + for (int j = 0; j < nweights; j++) { + const float *bone = &gstate.boneMatrix[j * 12]; + float weight = wdata[j]; + if (weight > 0.0) { + for (int i = 0; i < 12; i++) { + skinMatrix[i] += weight * bone[i]; + } + } + } +} + void VertexDecoder::Step_TcU8() const { // u32 to write two bytes of zeroes for free. @@ -318,6 +372,29 @@ void VertexDecoder::Step_NormalFloat() const normal[j] = fv[j]; } +void VertexDecoder::Step_NormalS8Skin() const +{ + float *normal = (float *)(decoded_ + decFmt.nrmoff); + const s8 *sv = (const s8*)(ptr_ + nrmoff); + const float fn[3] = { sv[0] / 128.0f, sv[1] / 128.0f, sv[2] / 128.0f }; + Norm3ByMatrix43(normal, fn, skinMatrix); +} + +void VertexDecoder::Step_NormalS16Skin() const +{ + float *normal = (float *)(decoded_ + decFmt.nrmoff); + const s16 *sv = (const s16*)(ptr_ + nrmoff); + const float fn[3] = { sv[0] / 32768.0f, sv[1] / 32768.0f, sv[2] / 32768.0f }; + Norm3ByMatrix43(normal, fn, skinMatrix); +} + +void VertexDecoder::Step_NormalFloatSkin() const +{ + float *normal = (float *)(decoded_ + decFmt.nrmoff); + const float *fn = (const float *)(ptr_ + nrmoff); + Norm3ByMatrix43(normal, fn, skinMatrix); +} + void VertexDecoder::Step_NormalS8Morph() const { float *normal = (float *)(decoded_ + decFmt.nrmoff); @@ -382,6 +459,29 @@ void VertexDecoder::Step_PosFloat() const memcpy(v, fv, 12); } +void VertexDecoder::Step_PosS8Skin() const +{ + float *pos = (float *)(decoded_ + decFmt.posoff); + const s8 *sv = (const s8*)(ptr_ + posoff); + const float fn[3] = { sv[0] / 128.0f, sv[1] / 128.0f, sv[2] / 128.0f }; + Vec3ByMatrix43(pos, fn, skinMatrix); +} + +void VertexDecoder::Step_PosS16Skin() const +{ + float *pos = (float *)(decoded_ + decFmt.posoff); + const s16 *sv = (const s16*)(ptr_ + posoff); + const float fn[3] = { sv[0] / 32768.0f, sv[1] / 32768.0f, sv[2] / 32768.0f }; + Vec3ByMatrix43(pos, fn, skinMatrix); +} + +void VertexDecoder::Step_PosFloatSkin() const +{ + float *pos = (float *)(decoded_ + decFmt.posoff); + const float *fn = (const float *)(ptr_ + posoff); + Vec3ByMatrix43(pos, fn, skinMatrix); +} + void VertexDecoder::Step_PosS8Through() const { float *v = (float *)(decoded_ + decFmt.posoff); @@ -449,6 +549,13 @@ static const StepFunction wtstep[4] = { &VertexDecoder::Step_WeightsFloat, }; +static const StepFunction wtstep_skin[4] = { + 0, + &VertexDecoder::Step_WeightsU8Skin, + &VertexDecoder::Step_WeightsU16Skin, + &VertexDecoder::Step_WeightsFloatSkin, +}; + static const StepFunction tcstep[4] = { 0, &VertexDecoder::Step_TcU8, @@ -510,6 +617,13 @@ static const StepFunction nrmstep[4] = { &VertexDecoder::Step_NormalFloat, }; +static const StepFunction nrmstep_skin[4] = { + 0, + &VertexDecoder::Step_NormalS8Skin, + &VertexDecoder::Step_NormalS16Skin, + &VertexDecoder::Step_NormalFloatSkin, +}; + static const StepFunction nrmstep_morph[4] = { 0, &VertexDecoder::Step_NormalS8Morph, @@ -524,6 +638,13 @@ static const StepFunction posstep[4] = { &VertexDecoder::Step_PosFloat, }; +static const StepFunction posstep_skin[4] = { + 0, + &VertexDecoder::Step_PosS8Skin, + &VertexDecoder::Step_PosS16Skin, + &VertexDecoder::Step_PosFloatSkin, +}; + static const StepFunction posstep_morph[4] = { 0, &VertexDecoder::Step_PosS8Morph, @@ -564,6 +685,8 @@ void VertexDecoder::SetVertexType(u32 fmt, VertexDecoderJitCache *jitCache) { DEBUG_LOG(G3D,"VTYPE: THRU=%i TC=%i COL=%i POS=%i NRM=%i WT=%i NW=%i IDX=%i MC=%i", (int)throughmode, tc,col,pos,nrm,weighttype,nweights,idx,morphcount); } + bool skinInDecode = weighttype != 0 && g_Config.bSoftwareSkinning && morphcount == 1; + if (weighttype) { // && nweights? weightoff = size; //size = align(size, wtalign[weighttype]); unnecessary @@ -571,30 +694,35 @@ void VertexDecoder::SetVertexType(u32 fmt, VertexDecoderJitCache *jitCache) { if (wtalign[weighttype] > biggest) biggest = wtalign[weighttype]; - steps_[numSteps_++] = wtstep[weighttype]; - - int fmtBase = DEC_FLOAT_1; - if (weighttype == GE_VTYPE_WEIGHT_8BIT >> GE_VTYPE_WEIGHT_SHIFT) { - fmtBase = DEC_U8_1; - } else if (weighttype == GE_VTYPE_WEIGHT_16BIT >> GE_VTYPE_WEIGHT_SHIFT) { - fmtBase = DEC_U16_1; - } else if (weighttype == GE_VTYPE_WEIGHT_FLOAT >> GE_VTYPE_WEIGHT_SHIFT) { - fmtBase = DEC_FLOAT_1; - } - - int numWeights = TranslateNumBones(nweights); - - if (numWeights <= 4) { - decFmt.w0off = decOff; - decFmt.w0fmt = fmtBase + numWeights - 1; - decOff += DecFmtSize(decFmt.w0fmt); + if (skinInDecode) { + steps_[numSteps_++] = wtstep_skin[weighttype]; + // No visible output } else { - decFmt.w0off = decOff; - decFmt.w0fmt = fmtBase + 3; - decOff += DecFmtSize(decFmt.w0fmt); - decFmt.w1off = decOff; - decFmt.w1fmt = fmtBase + numWeights - 5; - decOff += DecFmtSize(decFmt.w1fmt); + steps_[numSteps_++] = wtstep[weighttype]; + + int fmtBase = DEC_FLOAT_1; + if (weighttype == GE_VTYPE_WEIGHT_8BIT >> GE_VTYPE_WEIGHT_SHIFT) { + fmtBase = DEC_U8_1; + } else if (weighttype == GE_VTYPE_WEIGHT_16BIT >> GE_VTYPE_WEIGHT_SHIFT) { + fmtBase = DEC_U16_1; + } else if (weighttype == GE_VTYPE_WEIGHT_FLOAT >> GE_VTYPE_WEIGHT_SHIFT) { + fmtBase = DEC_FLOAT_1; + } + + int numWeights = TranslateNumBones(nweights); + + if (numWeights <= 4) { + decFmt.w0off = decOff; + decFmt.w0fmt = fmtBase + numWeights - 1; + decOff += DecFmtSize(decFmt.w0fmt); + } else { + decFmt.w0off = decOff; + decFmt.w0fmt = fmtBase + 3; + decOff += DecFmtSize(decFmt.w0fmt); + decFmt.w1off = decOff; + decFmt.w1fmt = fmtBase + numWeights - 5; + decOff += DecFmtSize(decFmt.w1fmt); + } } } @@ -656,26 +784,29 @@ void VertexDecoder::SetVertexType(u32 fmt, VertexDecoderJitCache *jitCache) { if (nrmalign[nrm] > biggest) biggest = nrmalign[nrm]; - steps_[numSteps_++] = morphcount == 1 ? nrmstep[nrm] : nrmstep_morph[nrm]; - - if (morphcount == 1) { - // The normal formats match the gl formats perfectly, let's use 'em. - switch (nrm) { - case GE_VTYPE_NRM_8BIT >> GE_VTYPE_NRM_SHIFT: decFmt.nrmfmt = DEC_S8_3; break; - case GE_VTYPE_NRM_16BIT >> GE_VTYPE_NRM_SHIFT: decFmt.nrmfmt = DEC_S16_3; break; - case GE_VTYPE_NRM_FLOAT >> GE_VTYPE_NRM_SHIFT: decFmt.nrmfmt = DEC_FLOAT_3; break; - } - } else { + if (skinInDecode) { + steps_[numSteps_++] = nrmstep_skin[nrm]; + // After skinning, we always have three floats. decFmt.nrmfmt = DEC_FLOAT_3; - } + } else { + steps_[numSteps_++] = morphcount == 1 ? nrmstep[nrm] : nrmstep_morph[nrm]; - // Actually, temporarily let's not. + if (morphcount == 1) { + // The normal formats match the gl formats perfectly, let's use 'em. + switch (nrm) { + case GE_VTYPE_NRM_8BIT >> GE_VTYPE_NRM_SHIFT: decFmt.nrmfmt = DEC_S8_3; break; + case GE_VTYPE_NRM_16BIT >> GE_VTYPE_NRM_SHIFT: decFmt.nrmfmt = DEC_S16_3; break; + case GE_VTYPE_NRM_FLOAT >> GE_VTYPE_NRM_SHIFT: decFmt.nrmfmt = DEC_FLOAT_3; break; + } + } else { + decFmt.nrmfmt = DEC_FLOAT_3; + } + } decFmt.nrmoff = decOff; decOff += DecFmtSize(decFmt.nrmfmt); } - if (pos) // there's always a position - { + if (pos) { // there's always a position size = align(size, posalign[pos]); posoff = size; size += possize[pos]; @@ -686,18 +817,23 @@ void VertexDecoder::SetVertexType(u32 fmt, VertexDecoderJitCache *jitCache) { steps_[numSteps_++] = posstep_through[pos]; decFmt.posfmt = DEC_FLOAT_3; } else { - steps_[numSteps_++] = morphcount == 1 ? posstep[pos] : posstep_morph[pos]; - - if (morphcount == 1) { - // The non-through-mode position formats match the gl formats perfectly, let's use 'em. - switch (pos) { - case GE_VTYPE_POS_8BIT >> GE_VTYPE_POS_SHIFT: decFmt.posfmt = DEC_S8_3; break; - case GE_VTYPE_POS_16BIT >> GE_VTYPE_POS_SHIFT: decFmt.posfmt = DEC_S16_3; break; - case GE_VTYPE_POS_FLOAT >> GE_VTYPE_POS_SHIFT: decFmt.posfmt = DEC_FLOAT_3; break; - } - } else { - // Actually, temporarily let's not. + if (skinInDecode) { + steps_[numSteps_++] = posstep_skin[pos]; decFmt.posfmt = DEC_FLOAT_3; + } else { + steps_[numSteps_++] = morphcount == 1 ? posstep[pos] : posstep_morph[pos]; + + if (morphcount == 1) { + // The non-through-mode position formats match the gl formats perfectly, let's use 'em. + switch (pos) { + case GE_VTYPE_POS_8BIT >> GE_VTYPE_POS_SHIFT: decFmt.posfmt = DEC_S8_3; break; + case GE_VTYPE_POS_16BIT >> GE_VTYPE_POS_SHIFT: decFmt.posfmt = DEC_S16_3; break; + case GE_VTYPE_POS_FLOAT >> GE_VTYPE_POS_SHIFT: decFmt.posfmt = DEC_FLOAT_3; break; + } + } else { + // Actually, temporarily let's not. + decFmt.posfmt = DEC_FLOAT_3; + } } } decFmt.posoff = decOff; diff --git a/GPU/GLES/VertexDecoder.h b/GPU/GLES/VertexDecoder.h index dfa248140c..1ba24af027 100644 --- a/GPU/GLES/VertexDecoder.h +++ b/GPU/GLES/VertexDecoder.h @@ -64,6 +64,10 @@ public: void Step_WeightsU16() const; void Step_WeightsFloat() const; + void Step_WeightsU8Skin() const; + void Step_WeightsU16Skin() const; + void Step_WeightsFloatSkin() const; + void Step_TcU8() const; void Step_TcU16() const; void Step_TcFloat() const; @@ -91,6 +95,10 @@ public: void Step_NormalS16() const; void Step_NormalFloat() const; + void Step_NormalS8Skin() const; + void Step_NormalS16Skin() const; + void Step_NormalFloatSkin() const; + void Step_NormalS8Morph() const; void Step_NormalS16Morph() const; void Step_NormalFloatMorph() const; @@ -99,6 +107,10 @@ public: void Step_PosS16() const; void Step_PosFloat() const; + void Step_PosS8Skin() const; + void Step_PosS16Skin() const; + void Step_PosFloatSkin() const; + void Step_PosS8Morph() const; void Step_PosS16Morph() const; void Step_PosFloatMorph() const; diff --git a/GPU/GPUState.cpp b/GPU/GPUState.cpp index fad43058c7..1a089db3ab 100644 --- a/GPU/GPUState.cpp +++ b/GPU/GPUState.cpp @@ -28,10 +28,14 @@ #include "GPU/Directx9/GPU_DX9.h" #endif #include "Core/CoreParameter.h" +#include "Core/Config.h" #include "Core/System.h" -GPUgstate gstate; -GPUStateCache gstate_c; +// This must be aligned so that the matrices within are aligned. +GPUgstate MEMORY_ALIGNED16(gstate); +// Let's align this one too for good measure. +GPUStateCache MEMORY_ALIGNED16(gstate_c); + GPUInterface *gpu; GPUDebugInterface *gpuDebug; GPUStatistics gpuStats; @@ -200,3 +204,10 @@ void GPUgstate::Restore(u32_le *ptr) { memcpy(projMatrix, matrices, sizeof(projMatrix)); matrices += sizeof(projMatrix); memcpy(tgenMatrix, matrices, sizeof(tgenMatrix)); matrices += sizeof(tgenMatrix); } + +bool vertTypeIsSkinningEnabled(u32 vertType) { + if (g_Config.bSoftwareSkinning && ((vertType & GE_VTYPE_MORPHCOUNT_MASK) == 0)) + return false; + else + return ((vertType & GE_VTYPE_WEIGHT_MASK) != GE_VTYPE_WEIGHT_NONE); +} diff --git a/GPU/GPUState.h b/GPU/GPUState.h index 5e00234693..f1c8aca408 100644 --- a/GPU/GPUState.h +++ b/GPU/GPUState.h @@ -18,6 +18,7 @@ #pragma once #include + #include "../Globals.h" #include "ge_constants.h" #include "Common/Common.h" @@ -406,7 +407,8 @@ enum SkipDrawReasonFlags { SKIPDRAW_BAD_FB_TEXTURE = 4, }; -inline bool vertTypeIsSkinningEnabled(u32 vertType) { return ((vertType & GE_VTYPE_WEIGHT_MASK) != GE_VTYPE_WEIGHT_NONE); } +bool vertTypeIsSkinningEnabled(u32 vertType); + inline int vertTypeGetNumBoneWeights(u32 vertType) { return 1 + ((vertType & GE_VTYPE_WEIGHTCOUNT_MASK) >> GE_VTYPE_WEIGHTCOUNT_SHIFT); } inline int vertTypeGetWeightMask(u32 vertType) { return vertType & GE_VTYPE_WEIGHT_MASK; } inline int vertTypeGetTexCoordMask(u32 vertType) { return vertType & GE_VTYPE_TC_MASK; } diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index be8044c839..22ce9bf5b1 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -143,10 +143,13 @@ void GameSettingsScreen::CreateViews() { graphicsSettings->Add(new CheckBox(&g_Config.bVSync, gs->T("VSync"))); #endif graphicsSettings->Add(new CheckBox(&g_Config.bHardwareTransform, gs->T("Hardware Transform"))); + CheckBox *swSkin = graphicsSettings->Add(new CheckBox(&g_Config.bSoftwareSkinning, gs->T("Software Skinning"))); graphicsSettings->Add(new CheckBox(&g_Config.bVertexCache, gs->T("Vertex Cache"))); CheckBox *vtxJit = graphicsSettings->Add(new CheckBox(&g_Config.bVertexDecoderJit, gs->T("Vertex Decoder JIT"))); - if (PSP_IsInited()) + if (PSP_IsInited()) { + swSkin->SetEnabled(false); vtxJit->SetEnabled(false); + } graphicsSettings->Add(new CheckBox(&g_Config.bLowQualitySplineBezier, gs->T("LowCurves", "Low quality spline/bezier curves")));