Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ int main(int argc, char* argv[])
config.musicEnabled = args.getString("music", "true") != "false";
config.shadows = args.getString("shadows", "true") != "false";
config.vehicleShadows = args.getString("vehicle-shadows", "true") != "false";
config.screenScale = std::clamp(args.getUint("screen-scale", 1), 1u, 4u);
config.scrollSpeed = std::clamp(args.getUint("scroll-speed", 100), 25u, 200u);
config.soundMode = std::min(2u, args.getUint("sound-mode", 2));
config.unitSpeech = std::min(2u, args.getUint("unit-speech", 2));
Expand Down
10 changes: 8 additions & 2 deletions src/rwe/CursorService.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "CursorService.h"
#include <algorithm>
#include <rwe/sdl/SdlContext.h>

namespace rwe
Expand Down Expand Up @@ -35,8 +36,8 @@ namespace rwe
float fy;

sdlContext->getMouseState(&fx, &fy);
int x = static_cast<int>(fx);
int y = static_cast<int>(fy);
int x = static_cast<int>(fx / static_cast<float>(screenScale));
int y = static_cast<int>(fy / static_cast<float>(screenScale));

auto timeInMillis = timeService->getTicks();
const auto& frames = currentCursor->sprites;
Expand All @@ -47,4 +48,9 @@ namespace rwe

renderer.drawSprite(x, y, *(frames[frameIndex]));
}

void CursorService::setScreenScale(unsigned int scale)
{
screenScale = std::max(1u, scale);
}
}
6 changes: 6 additions & 0 deletions src/rwe/CursorService.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,11 @@ namespace rwe
std::shared_ptr<SpriteSeries> getCursor(CursorType type) const;

void render(UiRenderService& renderer) const;

/** See GlobalConfig::screenScale: the mouse is in window pixels, the frame is not. */
void setScreenScale(unsigned int scale);

private:
unsigned int screenScale{1};
};
}
14 changes: 14 additions & 0 deletions src/rwe/GlobalConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ namespace rwe
*/
bool vehicleShadows{true};

/**
* How many screen pixels one game pixel is drawn as, 1 to 4. The
* original ran at 640x480 and, stretched across a modern display,
* shows each of its pixels about two screen pixels wide; RWE draws
* one to one, so its one-pixel wireframe and selection box read as
* thinner than TA's. At 2 the whole frame -- world, interface and
* cursor -- is rendered at half the window size and blown up with
* nearest-neighbour sampling, and mouse input is mapped back
* through the same factor. The world's own 2x supersample and the
* building halo filter happen inside that frame, so they look the
* same as at 1, only larger. An rwe.cfg key, screen-scale.
*/
unsigned int screenScale{1};

/** Screen scroll speed percentage, 25 to 200; 100 is the old fixed rate. */
unsigned int scrollSpeed{100};

Expand Down
4 changes: 3 additions & 1 deletion src/rwe/game/GameScene_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ namespace rwe
float fx;
float fy;
sceneContext.sdl->getMouseState(&fx, &fy);
return Point(static_cast<int>(fx), static_cast<int>(fy));
// Window pixels to frame pixels; see GlobalConfig::screenScale.
auto scale = static_cast<float>(std::max(1u, sceneContext.globalConfig->screenScale));
return Point(static_cast<int>(fx / scale), static_cast<int>(fy / scale));
}

std::optional<UnitId> GameScene::getFirstCollidingUnit(const Ray3f& ray) const
Expand Down
13 changes: 13 additions & 0 deletions src/rwe/render/GraphicsContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,19 @@ namespace rwe

void GraphicsContext::unbindFrameBuffer()
{
glBindFramebuffer(GL_FRAMEBUFFER, presentationFrameBuffer);
}

void GraphicsContext::setPresentationFrameBuffer(std::optional<FrameBufferIdentifier> frameBuffer)
{
presentationFrameBuffer = frameBuffer ? frameBuffer->value : 0;
}

void GraphicsContext::blitFrameBufferToWindow(FrameBufferIdentifier source, int sourceWidth, int sourceHeight, int windowWidth, int windowHeight)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, source.value);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, 0, windowWidth, windowHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

Expand Down
19 changes: 19 additions & 0 deletions src/rwe/render/GraphicsContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <rwe/geometry/CollisionMesh.h>
#include <rwe/grid/Grid.h>
#include <rwe/math/Vector3f.h>
#include <optional>
#include <rwe/render/FrameBufferHandle.h>
#include <rwe/render/GlMesh.h>
#include <rwe/render/RenderBufferHandle.h>
Expand Down Expand Up @@ -225,6 +226,23 @@ namespace rwe

void bindFrameBuffer(FrameBufferIdentifier frameBuffer);

/**
* The framebuffer that unbindFrameBuffer returns to: the window's
* own by default, or a presentation buffer while the frame is being
* drawn at a scale (see SceneManager). Scenes never need to know
* which, which is the point.
*/
void setPresentationFrameBuffer(std::optional<FrameBufferIdentifier> frameBuffer);

/**
* Copies the colour of `source`, `sourceWidth` by `sourceHeight`
* pixels, onto the window's own framebuffer stretched to
* `windowWidth` by `windowHeight`, sampling nearest so a whole-number
* scale keeps every pixel square. Leaves the window's framebuffer
* bound.
*/
void blitFrameBufferToWindow(FrameBufferIdentifier source, int sourceWidth, int sourceHeight, int windowWidth, int windowHeight);

void unbindFrameBuffer();

void enableBlending();
Expand Down Expand Up @@ -279,6 +297,7 @@ namespace rwe
void setActiveTextureSlot3();

private:
unsigned int presentationFrameBuffer{0};
ShaderHandle compileShader(GLenum shaderType, const std::string& source);

VboHandle genBuffer();
Expand Down
78 changes: 60 additions & 18 deletions src/rwe/scene/SceneManager.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "SceneManager.h"
#include <algorithm>
#include <rwe/render/render_prof.h>
#include <rwe/scene/Screenshot.h>
#include <rwe/sim/SimTicksPerSecond.h>
Expand Down Expand Up @@ -45,14 +46,22 @@ namespace rwe
viewport(viewport),
requestedExit(false)
{
// At a scale above 1 the scenes see a viewport of the window's size
// divided by the scale, draw into a buffer of that size, and the
// buffer is blown up onto the window at the end of the frame. The
// mouse is mapped back through the same factor on the way in.
screenScale = std::clamp(globalConfig->screenScale, 1u, 4u);
sdl->getWindowSize(window, &windowWidth, &windowHeight);
viewport->setDimensions(windowWidth / static_cast<int>(screenScale), windowHeight / static_cast<int>(screenScale));
cursorService->setScreenScale(screenScale);
}

void SceneManager::setNextScene(std::shared_ptr<Scene> scene)
{
nextScene = std::move(scene);
}

void dispatchToScene(const SDL_Event& event, Scene& currentScene)
void dispatchToScene(const SDL_Event& event, Scene& currentScene, float screenScale)
{
switch (event.type)
{
Expand All @@ -70,7 +79,7 @@ namespace rwe
break;
}

MouseButtonEvent e(event.button.x, event.button.y, *button);
MouseButtonEvent e(static_cast<int>(event.button.x / screenScale), static_cast<int>(event.button.y / screenScale), *button);
currentScene.onMouseDown(e);
break;
}
Expand All @@ -82,13 +91,13 @@ namespace rwe
break;
}

MouseButtonEvent e(event.button.x, event.button.y, *button);
MouseButtonEvent e(static_cast<int>(event.button.x / screenScale), static_cast<int>(event.button.y / screenScale), *button);
currentScene.onMouseUp(e);
break;
}
case SDL_EVENT_MOUSE_MOTION:
{
MouseMoveEvent e(event.motion.x, event.motion.y);
MouseMoveEvent e(static_cast<int>(event.motion.x / screenScale), static_cast<int>(event.motion.y / screenScale));
currentScene.onMouseMove(e);
break;
}
Expand Down Expand Up @@ -164,17 +173,19 @@ namespace rwe

if (event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED && event.window.windowID == sdl->getWindowId(window))
{
viewport->setDimensions(event.window.data1, event.window.data2);
windowWidth = event.window.data1;
windowHeight = event.window.data2;
viewport->setDimensions(windowWidth / static_cast<int>(screenScale), windowHeight / static_cast<int>(screenScale));
// The GL viewport does not follow the window by itself,
// and only the game scene ever sets it per frame -- the
// menu and the movie player draw through the default one,
// so without this a resized window kept rendering into a
// corner sized like the old window.
graphics->setViewport(0, 0, event.window.data1, event.window.data2);
graphics->setViewport(0, 0, viewport->width(), viewport->height());
continue;
}

dispatchToScene(event, *currentScene);
dispatchToScene(event, *currentScene, static_cast<float>(screenScale));
}

if (!headless)
Expand Down Expand Up @@ -209,25 +220,62 @@ namespace rwe
imGuiContext->render();

setCrashPhase(CrashPhase::Render);
if (screenScale > 1)
{
// The frame goes into a buffer of the scaled size. Scenes
// that bind buffers of their own unbind back to it rather
// than to the window, which is what the presentation target
// on the graphics context is for.
auto wantedWidth = static_cast<int>(viewport->width());
auto wantedHeight = static_cast<int>(viewport->height());
if (!presentationBuffer || presentationBufferWidth != wantedWidth || presentationBufferHeight != wantedHeight)
{
presentationBuffer = graphics->createFrameBuffer(wantedWidth, wantedHeight);
presentationBufferWidth = wantedWidth;
presentationBufferHeight = wantedHeight;
}
graphics->setPresentationFrameBuffer(presentationBuffer->frameBuffer.get());
graphics->bindFrameBuffer(presentationBuffer->frameBuffer.get());
}
graphics->clear();
// The game scene points the GL viewport at its own buffers as
// it works (the supersampled world among them), and the menu and
// movie scenes draw through whatever is current: without this
// reset, leaving a game left the front end rendering into a
// stale sub-rectangle while the mouse math used the window.
// corner sized like the old window.
graphics->setViewport(0, 0, viewport->width(), viewport->height());
currentScene->render();

// Taken here, after the scene and before the cursor and the debug
if (!imGuiContext->io->WantCaptureMouse)
{
sdl->hideCursor();
cursorService->render(uiRenderService);
}

if (screenScale > 1)
{
// Nearest-neighbour, so at a whole-number scale every game
// pixel is a square block of screen pixels. The world's own
// supersample and the building halo were resolved inside
// the frame, before this, as the original's own pixels
// would have been before a monitor stretched them.
graphics->setPresentationFrameBuffer(std::nullopt);
graphics->blitFrameBufferToWindow(presentationBuffer->frameBuffer.get(), presentationBufferWidth, presentationBufferHeight, windowWidth, windowHeight);
graphics->setViewport(0, 0, windowWidth, windowHeight);
}

// Taken here, after the scene and the cursor and before the debug
// windows, so the picture is the game with nothing of RWE's own on
// top of it. Whether the original's included its cursor is not
// decoded.
// top of it. It reads the window's back buffer, which at a scale
// above 1 has the frame on it only once it has been blown up. The
// cursor is in it now, where it was not before the scale work; the
// original's own screenshots are not decoded on that point.
if (screenshotRequested)
{
screenshotRequested = false;
if (auto dataPath = getLocalDataPath())
{
auto written = saveScreenshot(*dataPath / "screenshots", static_cast<unsigned int>(viewport->width()), static_cast<unsigned int>(viewport->height()));
auto written = saveScreenshot(*dataPath / "screenshots", static_cast<unsigned int>(windowWidth), static_cast<unsigned int>(windowHeight));
if (written)
{
LOG_INFO << "Screenshot saved to " << written->string();
Expand All @@ -239,12 +287,6 @@ namespace rwe
}
}

if (!imGuiContext->io->WantCaptureMouse)
{
sdl->hideCursor();
cursorService->render(uiRenderService);
}

{
RWE_RENDERPROF("imgui");
imGuiContext->renderDrawData();
Expand Down
14 changes: 14 additions & 0 deletions src/rwe/scene/SceneManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <array>
#include <memory>
#include <optional>
#include <rwe/CursorService.h>
#include <rwe/GlobalConfig.h>
#include <rwe/ImGuiContext.h>
Expand Down Expand Up @@ -43,6 +44,19 @@ namespace rwe
*/
bool screenshotRequested{false};

/**
* GlobalConfig::screenScale, and what it needs: the window's own
* size in pixels, which the viewport the scenes read no longer
* holds at a scale above 1, and the buffer the frame is drawn into
* before it is blown up onto the window.
*/
unsigned int screenScale{1};
int windowWidth{0};
int windowHeight{0};
std::optional<FrameBufferInfo> presentationBuffer;
int presentationBufferWidth{0};
int presentationBufferHeight{0};

unsigned int lastFrameStartTime{0};

std::array<float, 500> frameTimes;
Expand Down