Merge branch 'bmd-scaling' into 'master'

Added support for `Video -> resolution scale = 0.1 to 1.0`.

See merge request OpenMW/openmw!4896
This commit is contained in:
bmdhacks 2025-09-20 14:43:43 +00:00
commit 0f2a79fa79
15 changed files with 138 additions and 19 deletions

View File

@ -96,6 +96,10 @@ namespace MWLua
api["setViewDistance"]
= [renderingManager](const FiniteFloat d) { renderingManager->setViewDistance(d, true); };
api["getResolutionScale"] = [renderingManager]() { return renderingManager->getResolutionScale(); };
api["setResolutionScale"]
= [renderingManager](const FiniteFloat scale) { renderingManager->setResolutionScale(scale); };
api["getViewTransform"] = [camera]() { return LuaUtil::TransformM{ camera->getViewMatrix() }; };
api["viewportToWorldVector"] = [camera, renderingManager](osg::Vec2f pos) -> osg::Vec3f {

View File

@ -125,6 +125,8 @@ namespace MWRender
, mSamples(Settings::video().mAntialiasing)
, mPingPongCull(new PingPongCull(this))
, mDistortionCallback(new DistortionCallback)
, mScaledViewportStateSet(new osg::StateSet)
, mScaledViewport(new osg::Viewport)
{
auto& shaderManager = mRendering.getResourceSystem()->getSceneManager()->getShaderManager();
@ -148,6 +150,8 @@ namespace MWRender
mHUDCamera->setCullCallback(new HUDCullCallback);
mViewer->getCamera()->addCullCallback(mPingPongCull);
mScaledViewportStateSet->setAttribute(mScaledViewport);
// resolves the multisampled depth buffer and optionally draws an additional depth postpass
mTransparentDepthPostPass
= new TransparentDepthBinCallback(mRendering.getResourceSystem()->getSceneManager()->getShaderManager(),
@ -276,13 +280,26 @@ namespace MWRender
void PostProcessor::traverse(osg::NodeVisitor& nv)
{
size_t frameId = nv.getTraversalNumber() % 2;
bool pushedStateSet = false;
if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR)
cull(frameId, static_cast<osgUtil::CullVisitor*>(&nv));
{
osgUtil::CullVisitor* cv = static_cast<osgUtil::CullVisitor*>(&nv);
mScaledViewport->setViewport(0, 0, renderWidth(), renderHeight());
cv->pushStateSet(mScaledViewportStateSet.get());
pushedStateSet = true;
cull(frameId, cv);
}
else if (nv.getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR)
update(frameId);
osg::Group::traverse(nv);
if (pushedStateSet)
{
static_cast<osgUtil::CullVisitor*>(&nv)->popStateSet();
}
}
void PostProcessor::cull(size_t frameId, osgUtil::CullVisitor* cv)
@ -567,8 +584,11 @@ namespace MWRender
std::vector<Fx::Types::RenderTarget> attachmentsToDirty;
for (const auto& technique : mTechniques)
// Filter techniques to only include valid ones
std::vector<std::shared_ptr<Fx::Technique>> validTechniques;
for (auto techIt = mTechniques.begin(); techIt != mTechniques.end(); ++techIt)
{
const auto& technique = *techIt;
if (!technique->isValid())
continue;
@ -578,6 +598,12 @@ namespace MWRender
<< technique->getGLSLVersion() << " which is unsupported by your hardware.";
continue;
}
validTechniques.push_back(technique);
}
for (size_t techIdx = 0; techIdx < validTechniques.size(); ++techIdx)
{
const auto& technique = validTechniques[techIdx];
Fx::DispatchNode node;
@ -635,8 +661,10 @@ namespace MWRender
uniform->mName.c_str(), *type, uniform->getNumElements()));
}
for (const auto& pass : technique->getPasses())
const auto& passes = technique->getPasses();
for (size_t passIdx = 0; passIdx < passes.size(); ++passIdx)
{
const auto& pass = passes[passIdx];
int subTexUnit = texUnit;
Fx::DispatchNode::SubPass subPass;
@ -644,6 +672,8 @@ namespace MWRender
node.mHandle = technique;
bool isFinalPass = (techIdx == validTechniques.size() - 1 && passIdx == passes.size() - 1);
if (!pass->getTarget().empty())
{
auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()];
@ -676,6 +706,10 @@ namespace MWRender
attachmentsToDirty.push_back(Fx::Types::RenderTarget(renderTarget));
}
}
else if (!isFinalPass)
{
subPass.mStateSet->setAttribute(new osg::Viewport(0, 0, renderWidth(), renderHeight()));
}
for (const auto& name : pass->getRenderTargets())
{
@ -858,16 +892,16 @@ namespace MWRender
int PostProcessor::renderWidth() const
{
if (Stereo::getStereo())
return Stereo::Manager::instance().eyeResolution().x();
return mWidth;
float scale = static_cast<float>(Settings::video().mResolutionScale);
int baseWidth = Stereo::getStereo() ? Stereo::Manager::instance().eyeResolution().x() : mWidth;
return std::max(1, static_cast<int>(baseWidth * scale));
}
int PostProcessor::renderHeight() const
{
if (Stereo::getStereo())
return Stereo::Manager::instance().eyeResolution().y();
return mHeight;
float scale = static_cast<float>(Settings::video().mResolutionScale);
int baseHeight = Stereo::getStereo() ? Stereo::Manager::instance().eyeResolution().y() : mHeight;
return std::max(1, static_cast<int>(baseHeight * scale));
}
void PostProcessor::triggerShaderReload()

View File

@ -268,6 +268,8 @@ namespace MWRender
std::array<osg::ref_ptr<PingPongCanvas>, 2> mCanvases;
osg::ref_ptr<TransparentDepthBinCallback> mTransparentDepthPostPass;
osg::ref_ptr<DistortionCallback> mDistortionCallback;
osg::ref_ptr<osg::StateSet> mScaledViewportStateSet;
osg::ref_ptr<osg::Viewport> mScaledViewport;
Fx::DispatchArray mTemplateData;
};

View File

@ -1,5 +1,6 @@
#include "renderingmanager.hpp"
#include <algorithm>
#include <cstdlib>
#include <limits>
@ -1382,15 +1383,18 @@ namespace MWRender
mSharedUniformStateUpdater->setNear(mNearClip);
mSharedUniformStateUpdater->setFar(mViewDistance);
float scale = static_cast<float>(Settings::video().mResolutionScale);
if (Stereo::getStereo())
{
auto res = Stereo::Manager::instance().eyeResolution();
mSharedUniformStateUpdater->setScreenRes(res.x(), res.y());
mSharedUniformStateUpdater->setScreenRes(res.x() * scale, res.y() * scale);
Stereo::Manager::instance().setMasterProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix());
}
else
{
mSharedUniformStateUpdater->setScreenRes(width, height);
mSharedUniformStateUpdater->setScreenRes(width * scale, height * scale);
}
// Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may
@ -1520,6 +1524,10 @@ namespace MWRender
{
updateProjection = true;
}
else if (it->first == "Video" && it->second == "resolution scale")
{
mPostProcessor->resize();
}
else if (it->first == "Camera" && it->second == "viewing distance")
{
setViewDistance(Settings::camera().mViewingDistance);
@ -1610,6 +1618,22 @@ namespace MWRender
updateProjectionMatrix();
}
float RenderingManager::getResolutionScale() const
{
return Settings::video().mResolutionScale;
}
void RenderingManager::setResolutionScale(float scale)
{
scale = std::clamp(scale, 0.1f, 1.0f);
Settings::video().mResolutionScale.set(scale);
if (mPostProcessor)
{
mPostProcessor->resize();
}
}
float RenderingManager::getTerrainHeightAt(const osg::Vec3f& pos, ESM::RefId worldspace)
{
return getWorldspaceChunkMgr(worldspace).mTerrain->getHeightAt(pos);

View File

@ -230,6 +230,9 @@ namespace MWRender
void setViewDistance(float distance, bool delay = false);
float getResolutionScale() const;
void setResolutionScale(float scale);
float getTerrainHeightAt(const osg::Vec3f& pos, ESM::RefId worldspace);
// camera stuff

View File

@ -22,6 +22,8 @@ namespace Settings
SettingValue<int> mResolutionX{ mIndex, "Video", "resolution x", makeMaxSanitizerInt(1) };
SettingValue<int> mResolutionY{ mIndex, "Video", "resolution y", makeMaxSanitizerInt(1) };
SettingValue<float> mResolutionScale{ mIndex, "Video", "resolution scale",
makeClampStrictMaxSanitizerFloat(0.1, 1.0) };
SettingValue<WindowMode> mWindowMode{ mIndex, "Video", "window mode" };
SettingValue<int> mScreen{ mIndex, "Video", "screen", makeMaxSanitizerInt(0) };
SettingValue<bool> mMinimizeOnFocusLoss{ mIndex, "Video", "minimize on focus loss" };

View File

@ -23,6 +23,23 @@ Video Settings
Larger values produce more detailed images within the constraints of your graphics hardware,
but may reduce the frame rate.
.. omw-setting::
:title: resolution scale
:type: float
:range: 0.1 to 1.0
:default: 1.0
:location: :bdg-info:`In Game > Options > Video`
This setting controls the internal rendering resolution as a percentage of the window resolution.
Values below 1.0 render the game at a lower resolution and upscale to the window size.
For example, 0.5 on a 2560x1440 display would render at 1280x720 internally,
providing substantial performance improvements on GPU-limited systems while maintaining UI elements at full resolution.
.. note::
This setting is primarily intended for low-powered devices with high-DPI displays
(such as retro handhelds or low-powered Android phones). On desktop systems with adequate GPUs,
this should typically remain at 1.0 for optimal image quality.
.. omw-setting::
:title: window mode
:type: int

View File

@ -176,6 +176,7 @@ ReflectionShaderDetailTerrain: "Terrain"
ReflectionShaderDetailWorld: "Welt"
Refraction: "Lichtbrechung"
ResetControls: "Tastenbelegungen zurücksetzen"
ResolutionScale: "Auflösungsskalierung"
Screenshot: "Screenshot"
Scripts: "Skripte"
ScriptsDisabled: "Laden Sie einen Spielstand, um auf die Skripteinstellungen zugreifen zu können."

View File

@ -176,6 +176,7 @@ ReflectionShaderDetailTerrain: "Terrain"
ReflectionShaderDetailWorld: "World"
Refraction: "Refraction"
ResetControls: "Reset Controls"
ResolutionScale: "Resolution Scale"
Screenshot: "Screenshot"
Scripts: "Scripts"
ScriptsDisabled: "Load a game to access script settings."

View File

@ -176,6 +176,7 @@ ReflectionShaderDetailTerrain: "Terrain"
ReflectionShaderDetailWorld: "Monde"
Refraction: "Réfraction"
ResetControls: "Réinitialiser les contrôles"
ResolutionScale: "Échelle de résolution"
Screenshot: "Capture d'écran"
Scripts: "Scripts"
ScriptsDisabled: "Chargez une sauvegarde pour accéder aux paramètres des scripts."

View File

@ -176,6 +176,7 @@ ReflectionShaderDetailTerrain: "Teren"
ReflectionShaderDetailWorld: "Świat"
Refraction: "Załamanie światła"
ResetControls: "Przywróć sterowanie"
ResolutionScale: "Skalowanie rozdzielczości"
Screenshot: "Zrzut ekranu"
Scripts: "Skrypty"
ScriptsDisabled: "Wczytaj grę, aby uzyskać dostęp do ustawień skryptów."

View File

@ -176,6 +176,7 @@ ReflectionShaderDetailTerrain: "Ландшафт"
ReflectionShaderDetailWorld: "Мир"
Refraction: "Рефракция"
ResetControls: "Сбросить"
ResolutionScale: "Масштаб разрешения"
Screenshot: "Снимок экрана"
Scripts: "Скрипты"
ScriptsDisabled: "Загрузите игру, чтобы получить доступ к настройкам скриптов."

View File

@ -177,6 +177,7 @@ ReflectionShaderDetailTerrain: "Terräng"
ReflectionShaderDetailWorld: "Värld"
Refraction: "Refraktion"
ResetControls: "Återställ kontroller"
ResolutionScale: "Upplösningsskala"
Screenshot: "Skärmdump"
Scripts: "Skript"
ScriptsDisabled: "Ladda ett spel för att nå skriptinställningar."

View File

@ -327,9 +327,33 @@
<Property key="Caption" value="#{OMWEngine:WindowModeHint}"/>
</Widget>
<Widget type="TextBox" skin="NormalText" position="0 238 352 18" align="Left Top" name="FovText">
<Widget type="TextBox" skin="NormalText" position="0 258 352 18" align="Left Top" name="ResolutionScaleText">
<Property key="Caption" value="#{OMWEngine:ResolutionScale}"/>
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="0 262 352 18" align="HStretch Top">
<Widget type="ScrollBar" skin="MW_HScroll" position="0 282 352 18" align="HStretch Top">
<Property key="Range" value="10000"/>
<Property key="Page" value="100"/>
<UserString key="SettingType" value="Slider"/>
<UserString key="SettingCategory" value="Video"/>
<UserString key="SettingName" value="resolution scale"/>
<UserString key="SettingValueType" value="Float"/>
<UserString key="SettingMin" value="0.1"/>
<UserString key="SettingMax" value="1.0"/>
<UserString key="SettingLabelWidget" value="ResolutionScaleText"/>
<UserString key="SettingLabelCaption" value="#{OMWEngine:ResolutionScale} (%s)"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 306 352 18" align="Left Top">
<Property key="Caption" value="10%"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 306 352 18" align="Right Top">
<Property key="Caption" value="100%"/>
<Property key="TextAlign" value="Right"/>
</Widget>
<Widget type="TextBox" skin="NormalText" position="0 334 352 18" align="Left Top" name="FovText">
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="0 358 352 18" align="HStretch Top">
<Property key="Range" value="81"/>
<Property key="Page" value="1"/>
<UserString key="SettingType" value="Slider"/>
@ -341,18 +365,18 @@
<UserString key="SettingLabelWidget" value="FovText"/>
<UserString key="SettingLabelCaption" value="#{OMWEngine:FieldOfView} (%s)"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 286 352 18" align="Left Top">
<Widget type="TextBox" skin="SandText" position="0 382 352 18" align="Left Top">
<Property key="Caption" value="#{OMWEngine:FieldOfViewLow}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 286 352 18" align="Right Top">
<Widget type="TextBox" skin="SandText" position="0 382 352 18" align="Right Top">
<Property key="Caption" value="#{OMWEngine:FieldOfViewHigh}"/>
<Property key="TextAlign" value="Right"/>
</Widget>
<Widget type="TextBox" skin="NormalText" position="0 308 352 18" align="Left Top" name="GammaText">
<Widget type="TextBox" skin="NormalText" position="0 404 352 18" align="Left Top" name="GammaText">
<Property key="Caption" value="#{OMWEngine:GammaCorrection}"/>
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="0 332 352 18" align="HStretch Top" name="GammaSlider">
<Widget type="ScrollBar" skin="MW_HScroll" position="0 428 352 18" align="HStretch Top" name="GammaSlider">
<Property key="Range" value="10000"/>
<Property key="Page" value="300"/>
<UserString key="SettingType" value="Slider"/>
@ -362,11 +386,11 @@
<UserString key="SettingMin" value="0.1"/>
<UserString key="SettingMax" value="3.0"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 356 352 18" align="Left Top" name="GammaTextDark">
<Widget type="TextBox" skin="SandText" position="0 452 352 18" align="Left Top" name="GammaTextDark">
<Property key="Caption" value="#{OMWEngine:GammaDark}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 356 352 18" align="Right Top" name="GammaTextLight">
<Widget type="TextBox" skin="SandText" position="0 452 352 18" align="Right Top" name="GammaTextLight">
<Property key="Caption" value="#{OMWEngine:GammaLight}"/>
<Property key="TextAlign" value="Right"/>
</Widget>

View File

@ -639,6 +639,9 @@ doppler factor = 0.25
resolution x = 800
resolution y = 600
# Resolution scaling
resolution scale = 1.0
# Specify the window mode.
# 0 = Fullscreen, 1 = Windowed Fullscreen, 2 = Windowed
window mode = 2