Skip to content
Open
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
5 changes: 4 additions & 1 deletion code/def_files/data/effects/fxaa-v.sdr
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ void main() {
}
#else
in vec4 vertPosition;
in vec4 vertTexCoord;

out vec2 v_rcpFrame;
noperspective out vec2 v_pos;
Expand All @@ -20,6 +21,8 @@ layout (std140) uniform genericData {
void main() {
gl_Position = vertPosition;
v_rcpFrame = vec2(1.0/rt_w, 1.0/rt_h);
v_pos = vertPosition.xy*0.5 + 0.5;
// Use the real texcoord rather than deriving it from vertPosition: the draw call may ask for a
// sub-rectangle of the source texture, which the clip-space formula ignored. Matches post-v.sdr.
v_pos = vertTexCoord.xy;
}
#endif
73 changes: 69 additions & 4 deletions code/graphics/2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,29 @@ bool gr_is_smaa_mode(AntiAliasMode mode) {
return mode == AntiAliasMode::SMAA_Low || mode == AntiAliasMode::SMAA_Medium || mode == AntiAliasMode::SMAA_High || mode == AntiAliasMode::SMAA_Ultra;
}

SCP_vector<float> gr_get_supported_anisotropy_levels()
{
float max;
if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
return {};
}

if (max <= 2.0f) {
return {};
}

SCP_vector<float> out;

// We assume here that the anisotropy levels are powers of two...
float current = 1.0f;
while (current <= max) {
out.push_back(current);
current *= 2.0f;
}

return out;
}

static void parse_post_processing_func()
{
bool value;
Expand Down Expand Up @@ -1641,6 +1664,13 @@ void gr_screen_resize(int width, int height)
gr_screen.save_max_h_unscaled_zoomed = gr_screen.max_h_unscaled_zoomed;

gr_setup_viewport();

// Whatever the backend sized to the old gr_screen is now wrong; let it catch up before anything
// renders at the new size. This can discard the frame in progress -- see the warning on the
// declaration of this function.
if (gr_screen.gf_viewport_size_changed) {
gr_screen.gf_viewport_size_changed();
}
}

int gr_get_resolution_class(int width, int height)
Expand Down Expand Up @@ -2123,11 +2153,15 @@ bool gr_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps, GraphicsAPI
center_aspect_ratio = -1.0f;
}

// FRED doesn't support Vulkan yet (see qtfred/README.md for what's needed to change that), so it always
// falls back to OpenGL regardless of what was requested. This must happen before gr_init_function_pointers()
// below, since that's what binds gr_screen's gf_* dispatch table to the chosen API; doing the override any
// Vulkan needs more from the windowing implementation than an OpenGL context does, and not every
// implementation can provide it -- the MFC editor can't, and neither can a qtFRED built against a
// Qt without Vulkan support or running on a platform plugin we have no surface extension for.
// Fall back rather than fail. This must happen before gr_init_function_pointers() below, since
// that's what binds gr_screen's gf_* dispatch table to the chosen API; doing the override any
// later (e.g. in gr_init_sub()) would leave the dispatch table pointing at the wrong backend.
if (Fred_running) {
if (mode == GraphicsAPI::Vulkan && (graphicsOps == nullptr || graphicsOps->getVulkanSupport() == nullptr)) {
mprintf(("Vulkan was requested but this windowing implementation cannot present through it; "
"falling back to OpenGL.\n"));
mode = GraphicsAPI::OpenGL;
}

Expand Down Expand Up @@ -3266,6 +3300,37 @@ static void uniform_buffer_managers_retire_buffers()
UniformBufferManager->onFrameEnd();
}

bool gr_read_render_target(ubyte* out_rgba, int width, int height)
{
if (out_rgba == nullptr || width <= 0 || height <= 0) {
return false;
}

if (!gr_screen.gf_read_render_target) {
return false;
}

return gr_screen.gf_read_render_target(out_rgba, width, height);
}

void gr_end_offscreen_frame()
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}

// Same two things gr_flip() does for a presented frame, minus the presentation: retire the
// uniform segments so the next frame starts writing at offset 0 again, then let the backend
// recycle whatever per-frame pools it keeps. Order matters -- the backend rewinding its
// allocator while the engine still thinks it is part-way through a segment would just make
// the next allocation larger than the last.
uniform_buffer_managers_retire_buffers();

if (gr_screen.gf_end_offscreen_frame) {
gr_screen.gf_end_offscreen_frame();
}
}

graphics::util::UniformBuffer gr_get_uniform_buffer(uniform_block_type type, size_t num_elements, size_t element_size_override)
{
return UniformBufferManager->getUniformBuffer(type, num_elements, element_size_override);
Expand Down
76 changes: 76 additions & 0 deletions code/graphics/2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,14 @@ typedef struct screen {
// dumps the current screen to a html blob string
std::function<SCP_string()> gf_blob_screen;

// reads the currently bound render target back into a caller-provided RGBA8 buffer.
// Optional: backends that can't read a render target back leave this unset.
std::function<bool(ubyte* out_rgba, int width, int height)> gf_read_render_target;

// recycles per-frame backend state after an off-screen render that never reaches gr_flip().
// Optional: backends that keep no per-frame pools leave this unset.
std::function<void()> gf_end_offscreen_frame;

// transforms and dumps the current environment map to a file
std::function<void(const char* filename)> gf_dump_envmap;

Expand Down Expand Up @@ -861,6 +869,16 @@ typedef struct screen {
std::function<void()> gf_scene_texture_end;
std::function<void()> gf_copy_effect_texture;

// The viewport is now gr_screen.max_w x max_h; bring whatever the backend sized to the old one
// into line. Called from gr_screen_resize(); see the precondition documented there, which is
// stricter than it looks -- what a backend does here can include throwing away the frame in
// progress. Optional: a backend with nothing sized to the viewport leaves it unset.
//
// OpenGL grows the scene/post-processing render targets. Vulkan rebuilds the swap chain and
// everything sized to it, and restarts the frame; that is the only point at which it can notice
// the window and the swap chain have diverged (see VulkanRenderer::syncToSurfaceExtent()).
std::function<void()> gf_viewport_size_changed;

std::function<void(int zbias)> gf_zbias;

std::function<void(int)> gf_set_fill_mode;
Expand Down Expand Up @@ -972,6 +990,12 @@ typedef struct screen {
std::unique_ptr<os::Viewport> (*gf_create_viewport)(const os::ViewPortProperties& props);
std::function<void(os::Viewport* view)> gf_use_viewport;

//! Optional. Backends that keep per-viewport GPU resources (Vulkan holds a surface, swap chain
//! and everything sized to it) get told here that a viewport is about to be destroyed, while
//! the device and the viewport's window are both still alive. Left unset by backends with
//! nothing to release.
std::function<void(os::Viewport* view)> gf_release_viewport;

std::function<void(uniform_block_type bind_point, size_t offset, size_t size, gr_buffer_handle buffer)>
gf_bind_uniform_buffer;

Expand Down Expand Up @@ -1054,6 +1078,16 @@ extern const char *Resolution_prefixes[GR_NUM_RESOLUTIONS];
extern bool gr_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps, GraphicsAPI d_mode = GraphicsAPI::Default,
int d_width = GR_DEFAULT, int d_height = GR_DEFAULT, int d_depth = GR_DEFAULT);

/**
* @brief Tell the engine the viewport is now @p width x @p height.
*
* @warning Call this between frames, never once drawing has started. It runs
* gf_viewport_size_changed, and what a backend does there is not limited to reallocating: the
* Vulkan backend discards the frame in progress and restarts it at the new size, so anything
* already recorded into it is lost. OpenGL asserts rather than tear down a framebuffer it is
* rendering into. Both are fine at the top of a frame, which is where every caller sits today --
* an SDL resize event, or qtFRED's per-frame viewport sync.
*/
extern void gr_screen_resize(int width, int height);
extern int gr_get_resolution_class(int width, int height);

Expand Down Expand Up @@ -1133,6 +1167,36 @@ bool gr_is_screenshot_requested();
//#define gr_flip GR_CALL(gr_screen.gf_flip)
void gr_flip(bool execute_scripting = true);

/**
* @brief Read the currently bound render target back into @p out_rgba.
*
* For callers that composed into a render target (bm_set_render_target()) and want the pixels
* rather than a file or a data URL -- qtFRED's briefing map. gr_blob_screen() reads the same source
* but PNG-encodes and base64-wraps it, which is pure overhead when the destination is a bitmap
* again.
*
* @param out_rgba Receives @p width * @p height * 4 bytes, RGBA order, rows top-down. Must be at
* least that large.
* @param width Expected width of the bound target, in pixels
* @param height Expected height of the bound target, in pixels
* @return false if no target is bound, if it isn't the size the caller expected, or if the backend
* can't read one back at all. @p out_rgba is untouched in that case.
*/
bool gr_read_render_target(ubyte* out_rgba, int width, int height);

/**
* @brief End a frame's worth of rendering that never reaches gr_flip().
*
* For off-screen renderers that compose into a render target and read the result back rather than
* presenting -- qtFRED's briefing map. gr_flip() is what retires the engine's per-frame uniform
* segments and what makes the backend recycle its per-frame pools; a renderer that never calls it
* accumulates both for as long as it runs.
*
* Only call this once the frame's GPU work has actually completed -- after a readback that
* host-waits, which is the case for gr_blob_screen() on a bound render target.
*/
void gr_end_offscreen_frame();

inline void gr_setup_frame() {
gr_screen.gf_setup_frame();
}
Expand Down Expand Up @@ -1374,6 +1438,12 @@ inline bool gr_get_property(gr_property property, void* destination)
return gr_screen.gf_get_property(property, destination);
}

// Anisotropic filtering levels the current hardware supports: 1.0 (off), then powers of two up to
// the reported maximum. Empty if anisotropy is unavailable or the hardware caps out below 4x, in
// which case there is nothing meaningful to offer. Backs both the in-game option's enumerator and
// qtFRED's Preferences combo, so the two can't drift.
SCP_vector<float> gr_get_supported_anisotropy_levels();

inline void gr_push_debug_group(const char* name)
{
gr_screen.gf_push_debug_group(name);
Expand Down Expand Up @@ -1417,6 +1487,12 @@ inline void gr_use_viewport(os::Viewport* view)
{
gr_screen.gf_use_viewport(view);
}
inline void gr_release_viewport(os::Viewport* view)
{
if (gr_screen.gf_release_viewport) {
gr_screen.gf_release_viewport(view);
}
}
inline void gr_set_viewport(int x, int y, int width, int height)
{
gr_screen.gf_set_viewport(x, y, width, height);
Expand Down
40 changes: 39 additions & 1 deletion code/graphics/opengl/gropengl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,42 @@ SCP_string gr_opengl_blob_screen()
return "data:image/png;base64," + result;
}

bool gr_opengl_read_render_target(ubyte* out_rgba, int width, int height)
{
const GLuint render_target = opengl_get_rtt_framebuffer();
if (render_target == 0) {
return false;
}

// The caller sized its buffer from the bitmap it bound, so a disagreement means it is reading
// something other than what it thinks. Refuse rather than overrun or return a wrong-shaped image.
if (width != gr_screen.max_w || height != gr_screen.max_h) {
nprintf(("OpenGL", "gr_opengl_read_render_target: caller expected %dx%d but the bound target "
"is %dx%d\n", width, height, gr_screen.max_w, gr_screen.max_h));
return false;
}

GL_state.PushFramebufferState();
GL_state.BindFrameBuffer(render_target, GL_FRAMEBUFFER);
glReadBuffer(GL_COLOR_ATTACHMENT0);

// Row 0 first, which for a render target FSO composed into is the top row -- matching the
// top-down order gr_read_render_target() promises. Deliberately not the flip gr_blob_screen()
// applies: that one exists to make the PNG come out upright, and there is no PNG here.
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, out_rgba);
glFlush();

GL_state.PopFramebufferState();

// Reported, not returned. glGetError drains one global queue, so an entry left by anything
// earlier in the frame is not evidence about this readback -- and callers use the return value
// to decide whether the frame's work has completed (gr_end_offscreen_frame()). Failing on
// somebody else's error would silently skip that.
opengl_check_for_errors("gr_opengl_read_render_target");

return true;
}

void gr_opengl_dump_envmap(const char* filename)
{
char tmp[MAX_PATH_LEN];
Expand Down Expand Up @@ -1066,6 +1102,7 @@ void gr_opengl_init_function_pointers()

gr_screen.gf_print_screen = gr_opengl_print_screen;
gr_screen.gf_blob_screen = gr_opengl_blob_screen;
gr_screen.gf_read_render_target = gr_opengl_read_render_target;
gr_screen.gf_dump_envmap = gr_opengl_dump_envmap;
gr_screen.gf_calculate_irrmap = gr_opengl_calculate_irrmap;

Expand Down Expand Up @@ -1126,6 +1163,7 @@ void gr_opengl_init_function_pointers()
gr_screen.gf_scene_texture_begin = gr_opengl_scene_texture_begin;
gr_screen.gf_scene_texture_end = gr_opengl_scene_texture_end;
gr_screen.gf_copy_effect_texture = gr_opengl_copy_effect_texture;
gr_screen.gf_viewport_size_changed = gr_opengl_resize_render_targets;

gr_screen.gf_deferred_lighting_begin = gr_opengl_deferred_lighting_begin;
gr_screen.gf_deferred_lighting_msaa = gr_opengl_deferred_lighting_msaa;
Expand Down Expand Up @@ -1513,7 +1551,7 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
opengl_shader_init();

// post processing effects, after shaders are initialized
opengl_setup_scene_textures();
opengl_setup_scene_textures(gr_screen.max_w, gr_screen.max_h);
opengl_post_process_init();

// must be called after extensions are setup
Expand Down
23 changes: 18 additions & 5 deletions code/graphics/opengl/gropengldeferred.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void gr_opengl_deferred_lighting_begin(bool clearNonColorBufs)
Current_shader->program->Uniforms.setTextureUniform("tex", 0);
GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE);
GL_state.SetZbufferType(ZBUFFER_TYPE_NONE);
opengl_draw_full_screen_textured(0, 0, 1, 1);
opengl_draw_full_screen_scene_texture();
} else {
// Copy the existing color data into the emissive part of the G-buffer since everything that already existed is
// treated as emissive
Expand Down Expand Up @@ -159,7 +159,9 @@ void gr_opengl_deferred_lighting_msaa()
});
GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE);
GL_state.SetZbufferType(ZBUFFER_TYPE_WRITE);
opengl_draw_full_screen_textured(0, 0, 1, 1);
// msaa-f.sdr resolves via ivec2(textureSize(texColor) * fragTexCoord), so the texcoords have to
// stay inside the rendered sub-rectangle of the multisampled G-buffer.
opengl_draw_full_screen_scene_texture();
}

void gr_opengl_deferred_lighting_end()
Expand Down Expand Up @@ -321,8 +323,12 @@ void gr_opengl_deferred_lighting_finish()
shadow_cascade_params_bind(offset, count);
}

header->invScreenWidth = 1.0f / gr_screen.max_w;
header->invScreenHeight = 1.0f / gr_screen.max_h;
// deferred-f.sdr turns gl_FragCoord into a G-buffer texture coordinate with these, so they
// have to normalize against the G-buffer's own dimensions. Those only equal gr_screen while
// the viewport exactly fills the scene textures -- not after a shrink, and not when the
// allocation was clamped by GL_max_renderbuffer_size.
header->invScreenWidth = 1.0f / Scene_texture_width;
header->invScreenHeight = 1.0f / Scene_texture_height;
header->nearPlane = gr_near_plane;

{
Expand Down Expand Up @@ -557,7 +563,8 @@ void gr_opengl_deferred_lighting_finish()
data->clip_dist = Neb2_fog_clip_distance;
});

opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
// fog-f.sdr samples the composite and depth targets straight off fragTexCoord.
opengl_draw_full_screen_scene_texture();

if (bDrawNebVolumetrics) {
glReadBuffer(GL_COLOR_ATTACHMENT0);
Expand Down Expand Up @@ -653,6 +660,12 @@ void gr_opengl_deferred_lighting_finish()

{
GR_DEBUG_SCOPE("Volumetric Nebulae Draw");
// Deliberately unscaled. volumetric-f.sdr uses fragTexCoord for two incompatible
// things: reconstructing an eye-space ray direction, which needs the full 0..1 range
// across the viewport, and sampling composite/depth/emissive, which needs the
// rendered sub-rectangle. Scaling here would fix the sampling and skew every ray.
// Separating the two needs a second varying (or a scale uniform) in the shader; until
// then volumetrics are only correct while the targets exactly match the viewport.
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
}
GL_state.Texture.Enable(Scene_emissive_texture);
Expand Down
Loading
Loading