mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-18 16:01:07 +02:00
456 lines
17 KiB
C++
456 lines
17 KiB
C++
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
#include "gl_upscale.hpp"
|
|
|
|
#ifdef __APPLE__
|
|
#include <OpenGL/gl3.h>
|
|
#elif defined(PROSPERO)
|
|
#include <GL/glcorearb.h>
|
|
#else
|
|
#define GL_GLEXT_PROTOTYPES
|
|
#include <SDL2/SDL_opengl.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iterator>
|
|
|
|
namespace {
|
|
const char* const model_names[] = {"artcnn", "anime4k-s", "anime4k-m", "anime4k-l", "anime4k-vl", "anime4k-ul"};
|
|
} // namespace
|
|
|
|
namespace upscalers {
|
|
int find(const char* name) {
|
|
for (int i = 0; i < count(); ++i)
|
|
if (name && !std::strcmp(name, model_names[i])) return i;
|
|
return kNone;
|
|
}
|
|
const char* name(int model) { return model >= 0 && model < count() ? model_names[model] : ""; }
|
|
int count() { return int(std::size(model_names)); }
|
|
std::string names() {
|
|
std::string out;
|
|
for (const char* name : model_names) out += (out.empty() ? "" : ", ") + std::string(name);
|
|
return out;
|
|
}
|
|
} // namespace upscalers
|
|
|
|
// Without compute shaders (macOS's GL 4.1) there is nothing to run: init() fails.
|
|
#ifdef GL_COMPUTE_SHADER
|
|
namespace {
|
|
// In the order of model_names.
|
|
const char* const model_sources[] = {
|
|
#include "upscalers/ArtCNN_C4F16.glsl.inc"
|
|
,
|
|
#include "upscalers/Anime4K_Upscale_CNN_x2_S.glsl.inc"
|
|
,
|
|
#include "upscalers/Anime4K_Upscale_CNN_x2_M.glsl.inc"
|
|
,
|
|
#include "upscalers/Anime4K_Upscale_CNN_x2_L.glsl.inc"
|
|
,
|
|
#include "upscalers/Anime4K_Upscale_CNN_x2_VL.glsl.inc"
|
|
,
|
|
#include "upscalers/Anime4K_Upscale_CNN_x2_UL.glsl.inc"
|
|
};
|
|
static_assert(std::size(model_sources) == std::size(model_names));
|
|
|
|
// What libplacebo defines for a user shader pass, as far as these shaders use it. Reads outside a
|
|
// texture return zero, the padding the networks were trained with (GL leaves them undefined).
|
|
const char* prelude = R"(
|
|
uniform vec2 out_size;
|
|
vec4 fetch_zero(sampler2D s, ivec2 p, int lod) {
|
|
return all(greaterThanEqual(p, ivec2(0))) && all(lessThan(p, textureSize(s, 0))) ? texelFetch(s, p, lod) : vec4(0.0);
|
|
}
|
|
)";
|
|
// A fragment pass (`vec4 hook()`) runs as a compute pass of one invocation per output pixel.
|
|
constexpr int kFragmentGroup = 16;
|
|
const char* fragment_main = R"(
|
|
void main() {
|
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
|
if (any(greaterThanEqual(p, ivec2(out_size)))) return;
|
|
imageStore(out_image, p, hook());
|
|
}
|
|
)";
|
|
|
|
unsigned int compile(const std::string& source, const std::string& name) {
|
|
GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
const char* text = source.c_str();
|
|
glShaderSource(shader, 1, &text, nullptr);
|
|
glCompileShader(shader);
|
|
GLint ok = 0;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
|
if (!ok) {
|
|
char log[2048] = {};
|
|
glGetShaderInfoLog(shader, sizeof log, nullptr, log);
|
|
std::fprintf(stderr, "Upscale pass %s failed to compile: %s\n", name.c_str(), log);
|
|
glDeleteShader(shader);
|
|
return 0;
|
|
}
|
|
GLuint program = glCreateProgram();
|
|
glAttachShader(program, shader);
|
|
glLinkProgram(program);
|
|
glDeleteShader(shader);
|
|
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
|
if (!ok) {
|
|
char log[2048] = {};
|
|
glGetProgramInfoLog(program, sizeof log, nullptr, log);
|
|
std::fprintf(stderr, "Upscale pass %s failed to link: %s\n", name.c_str(), log);
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
std::size_t occurrences(const std::string& text, const char* what) {
|
|
std::size_t n = 0;
|
|
for (std::size_t at = 0; (at = text.find(what, at)) != std::string::npos; at += std::strlen(what)) ++n;
|
|
return n;
|
|
}
|
|
} // namespace
|
|
#endif
|
|
|
|
bool GlUpscale::init(int model) {
|
|
release();
|
|
#ifdef GL_COMPUTE_SHADER
|
|
if (model < 0 || model >= upscalers::count()) return false;
|
|
GLint major = 0, minor = 0;
|
|
glGetIntegerv(GL_MAJOR_VERSION, &major);
|
|
glGetIntegerv(GL_MINOR_VERSION, &minor);
|
|
if (major < 4 || (major == 4 && minor < 3)) return false;
|
|
|
|
// One pass per run of "//!" directives followed by its body.
|
|
struct Parsed {
|
|
std::string desc, hook, save, body, width, height, when;
|
|
std::vector<std::string> binds;
|
|
int compute[4] = {};
|
|
};
|
|
std::vector<Parsed> parsed;
|
|
bool in_body = true;
|
|
for (const char* at = model_sources[model]; *at;) {
|
|
const char* end = std::strchr(at, '\n');
|
|
std::string line(at, end ? end : at + std::strlen(at));
|
|
at = end ? end + 1 : at + line.size();
|
|
if (line.rfind("//!", 0) == 0) {
|
|
if (in_body) parsed.emplace_back();
|
|
in_body = false;
|
|
Parsed& pass = parsed.back();
|
|
const std::size_t space = line.find(' ', 3), value = line.find_first_not_of(' ', space);
|
|
const std::string key = line.substr(3, space == std::string::npos ? std::string::npos : space - 3);
|
|
const std::string rest = value == std::string::npos ? "" : line.substr(value);
|
|
if (key == "DESC") pass.desc = rest;
|
|
else if (key == "HOOK") pass.hook = rest;
|
|
else if (key == "BIND") pass.binds.push_back(rest);
|
|
else if (key == "SAVE") pass.save = rest;
|
|
else if (key == "WIDTH") pass.width = rest;
|
|
else if (key == "HEIGHT") pass.height = rest;
|
|
else if (key == "WHEN") pass.when = rest;
|
|
else if (key == "COMPUTE") std::sscanf(rest.c_str(), "%d %d %d %d", &pass.compute[0], &pass.compute[1], &pass.compute[2], &pass.compute[3]);
|
|
continue;
|
|
}
|
|
in_body = true;
|
|
if (parsed.empty() || line.rfind("#extension", 0) == 0) continue; // before any non-preprocessor token
|
|
for (std::size_t found = 0; (found = line.find("texelFetch(", found)) != std::string::npos; found += 11)
|
|
line.replace(found, 11, "fetch_zero(");
|
|
parsed.back().body += line + '\n';
|
|
}
|
|
if (parsed.empty()) return false;
|
|
hook_ = parsed.front().hook;
|
|
rgb_ = hook_ == "MAIN";
|
|
// "OUTPUT.w MAIN.w / 1.200 > ...": the ratio a doubler waits for.
|
|
if (float ratio = 0; std::sscanf(parsed.front().when.c_str(), "%*s %*s / %f", &ratio) == 1 && ratio > 1)
|
|
threshold_ = ratio;
|
|
|
|
// A size is "NAME.w" or "NAME.w k *": the named texture's size, times k. Sources are 1.
|
|
auto factor = [&](const std::string& expression) -> float {
|
|
char base[64] = {}, op[4] = {};
|
|
float k = 1;
|
|
const int fields = std::sscanf(expression.c_str(), "%63s %f %3s", base, &k, op);
|
|
if (fields < 1 || (fields > 1 && (fields != 3 || std::strcmp(op, "*")))) return 0;
|
|
std::string name(base);
|
|
name = name.substr(0, name.rfind('.'));
|
|
if (name == hook_) return fields == 3 ? k : 1;
|
|
for (const Pass& pass : passes_)
|
|
if (pass.save == name) return pass.scale * (fields == 3 ? k : 1);
|
|
return 0;
|
|
};
|
|
for (std::size_t i = 0; i < parsed.size(); ++i) {
|
|
const Parsed& p = parsed[i];
|
|
const bool last = i + 1 == parsed.size();
|
|
Pass pass;
|
|
pass.scale = factor(p.width);
|
|
pass.binds = p.binds;
|
|
pass.save = p.save == hook_ ? "" : p.save; // a pass saving the hooked plane replaces it: the result
|
|
if (p.hook != hook_ || pass.scale <= 0 || pass.scale != factor(p.height) || (pass.save.empty() && !last)) {
|
|
std::fprintf(stderr, "Upscale pass %s: unsupported hook, size or output\n", p.desc.c_str());
|
|
release();
|
|
return false;
|
|
}
|
|
const bool compute = p.compute[0] > 0;
|
|
const int lx = compute ? p.compute[2] : kFragmentGroup, ly = compute ? p.compute[3] : kFragmentGroup;
|
|
pass.block_w = compute ? p.compute[0] : kFragmentGroup;
|
|
pass.block_h = compute ? p.compute[1] : kFragmentGroup;
|
|
// Multiply-adds per invocation times invocations per output pixel, per source pixel.
|
|
const double per_pixel = double(lx) * ly / (double(pass.block_w) * pass.block_h);
|
|
pass.cost = double(std::max<std::size_t>(occurrences(p.body, " += "), 1)) * per_pixel * pass.scale * pass.scale;
|
|
// The result of a luma doubler is one channel; everything else keeps four.
|
|
const char* format = last && !rgb_ ? "r16f" : "rgba16f";
|
|
std::string source = "#version 430 core\nlayout(local_size_x = " + std::to_string(lx) + ", local_size_y = " +
|
|
std::to_string(ly) + ") in;\nlayout(" + format +
|
|
", binding = 0) writeonly uniform image2D out_image;\n" + prelude;
|
|
for (const std::string& name : p.binds) {
|
|
source += "uniform sampler2D " + name + "_raw;\nuniform vec2 " + name + "_size;\n#define " + name + "_mul 1.0\n";
|
|
source += "#define " + name + "_pt (vec2(1.0) / " + name + "_size)\n";
|
|
source += "#define " + name + "_pos ((vec2(gl_GlobalInvocationID.xy) + 0.5) / out_size)\n";
|
|
source += "#define " + name + "_tex(p) texture(" + name + "_raw, p)\n";
|
|
source += "#define " + name + "_texOff(off) " + name + "_tex(" + name + "_pos + " + name + "_pt * (off))\n";
|
|
}
|
|
source += p.body + (compute ? "void main() { hook(); }\n" : fragment_main);
|
|
pass.program = compile(source, p.desc);
|
|
if (!pass.program) {
|
|
release();
|
|
return false;
|
|
}
|
|
passes_.push_back(std::move(pass));
|
|
}
|
|
for (std::size_t i = 0; i < passes_.size(); ++i)
|
|
for (std::size_t j = i + 1; j < passes_.size(); ++j)
|
|
if (!passes_[i].save.empty() && std::count(passes_[j].binds.begin(), passes_[j].binds.end(), passes_[i].save))
|
|
passes_[i].last_use = int(j);
|
|
total_cost_ = 0;
|
|
for (const Pass& pass : passes_) total_cost_ += pass.cost;
|
|
next_pass_ = passes_.size();
|
|
|
|
glGenQueries(kTimers, timers_);
|
|
timing_ = glGetError() == GL_NO_ERROR;
|
|
return true;
|
|
#else
|
|
(void)model;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void GlUpscale::release() {
|
|
#ifdef GL_COMPUTE_SHADER
|
|
for (Pass& pass : passes_) glDeleteProgram(pass.program);
|
|
if (timing_) glDeleteQueries(kTimers, timers_);
|
|
#endif
|
|
free_textures();
|
|
passes_.clear();
|
|
hook_.clear();
|
|
rgb_ = false;
|
|
threshold_ = 1.3f;
|
|
next_pass_ = 0;
|
|
std::fill(std::begin(timer_pending_), std::end(timer_pending_), false);
|
|
timing_ = false;
|
|
}
|
|
|
|
void GlUpscale::free_textures() {
|
|
#ifdef GL_COMPUTE_SHADER
|
|
for (Texture& texture : pool_) glDeleteTextures(1, &texture.id);
|
|
for (Texture& texture : results_)
|
|
if (texture.id) glDeleteTextures(1, &texture.id);
|
|
#endif
|
|
pool_.clear();
|
|
outputs_.clear();
|
|
results_[0] = results_[1] = {};
|
|
result_ready_[0] = result_ready_[1] = false;
|
|
width_ = height_ = 0;
|
|
++generation_;
|
|
}
|
|
|
|
// A texture for each pass's output, shared by passes whose lifetimes do not overlap, and two results.
|
|
bool GlUpscale::allocate(int w, int h) {
|
|
#ifdef GL_COMPUTE_SHADER
|
|
free_textures();
|
|
auto make = [](int tw, int th, GLenum format) {
|
|
Texture texture{0, tw, th, 0};
|
|
glGenTextures(1, &texture.id);
|
|
glBindTexture(GL_TEXTURE_2D, texture.id);
|
|
glTexStorage2D(GL_TEXTURE_2D, 1, format, tw, th);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
return texture;
|
|
};
|
|
for (std::size_t i = 0; i < passes_.size(); ++i) {
|
|
const int ow = int(std::lround(w * passes_[i].scale)), oh = int(std::lround(h * passes_[i].scale));
|
|
if (passes_[i].save.empty()) {
|
|
outputs_.push_back(-1);
|
|
for (Texture& result : results_) result = make(ow, oh, rgb_ ? GL_RGBA16F : GL_R16F);
|
|
continue;
|
|
}
|
|
int found = -1;
|
|
for (std::size_t t = 0; t < pool_.size() && found < 0; ++t)
|
|
if (pool_[t].w == ow && pool_[t].h == oh && pool_[t].free_after < int(i)) found = int(t);
|
|
if (found < 0) {
|
|
pool_.push_back(make(ow, oh, GL_RGBA16F));
|
|
found = int(pool_.size()) - 1;
|
|
}
|
|
pool_[found].free_after = passes_[i].last_use;
|
|
outputs_.push_back(found);
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
if (glGetError() != GL_NO_ERROR) {
|
|
std::fprintf(stderr, "Upscale: no textures for %dx%d\n", w, h);
|
|
free_textures();
|
|
return false;
|
|
}
|
|
width_ = w;
|
|
height_ = h;
|
|
return true;
|
|
#else
|
|
(void)w;
|
|
(void)h;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool GlUpscale::start(unsigned int source, int w, int h, int slot) {
|
|
if (!ready()) return false;
|
|
if ((w != width_ || h != height_) && !allocate(w, h)) return false;
|
|
source_ = source;
|
|
slot_ = slot & 1;
|
|
result_ready_[slot_] = false;
|
|
next_pass_ = 0;
|
|
done_cost_ = 0;
|
|
dumping_ = !dump_prefix_.empty() && ++total_runs_ == dump_run_;
|
|
if (dumping_) dump(source, w, h, dump_prefix_ + "-in.pgm");
|
|
return true;
|
|
}
|
|
|
|
void GlUpscale::run_pass(std::size_t index) {
|
|
#ifdef GL_COMPUTE_SHADER
|
|
const Pass& pass = passes_[index];
|
|
const Texture& out = outputs_[index] < 0 ? results_[slot_] : pool_[std::size_t(outputs_[index])];
|
|
glUseProgram(pass.program);
|
|
for (std::size_t b = 0; b < pass.binds.size(); ++b) {
|
|
const std::string& name = pass.binds[b];
|
|
unsigned int texture = source_;
|
|
float tw = float(width_), th = float(height_);
|
|
for (std::size_t j = index; j-- > 0;) {
|
|
if (passes_[j].save != name) continue;
|
|
const Texture& saved = pool_[std::size_t(outputs_[j])];
|
|
texture = saved.id;
|
|
tw = float(saved.w);
|
|
th = float(saved.h);
|
|
break;
|
|
}
|
|
glActiveTexture(GL_TEXTURE0 + GLenum(b));
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glUniform1i(glGetUniformLocation(pass.program, (name + "_raw").c_str()), GLint(b));
|
|
glUniform2f(glGetUniformLocation(pass.program, (name + "_size").c_str()), tw, th);
|
|
}
|
|
glUniform2f(glGetUniformLocation(pass.program, "out_size"), float(out.w), float(out.h));
|
|
const GLenum format = outputs_[index] < 0 && !rgb_ ? GL_R16F : GL_RGBA16F;
|
|
glBindImageTexture(0, out.id, 0, GL_FALSE, 0, GL_WRITE_ONLY, format);
|
|
glDispatchCompute(GLuint((out.w + pass.block_w - 1) / pass.block_w), GLuint((out.h + pass.block_h - 1) / pass.block_h), 1);
|
|
glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
|
for (std::size_t b = pass.binds.size(); b-- > 0;) {
|
|
glActiveTexture(GL_TEXTURE0 + GLenum(b));
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
}
|
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, format);
|
|
#else
|
|
(void)index;
|
|
#endif
|
|
}
|
|
|
|
bool GlUpscale::step(float share) {
|
|
if (!running()) return true;
|
|
#ifdef GL_COMPUTE_SHADER
|
|
collect_timers();
|
|
const int timer = next_timer_;
|
|
const bool timed = timing_ && !timer_pending_[timer];
|
|
if (timed) glBeginQuery(GL_TIME_ELAPSED, timers_[timer]);
|
|
do {
|
|
run_pass(next_pass_);
|
|
done_cost_ += passes_[next_pass_].cost;
|
|
++next_pass_;
|
|
} while (running() && done_cost_ < share * total_cost_);
|
|
if (timed) {
|
|
glEndQuery(GL_TIME_ELAPSED);
|
|
timer_pending_[timer] = true;
|
|
next_timer_ = (timer + 1) % kTimers;
|
|
}
|
|
#else
|
|
(void)share;
|
|
#endif
|
|
if (running()) return false;
|
|
result_ready_[slot_] = true;
|
|
++stats_.runs;
|
|
if (dumping_) {
|
|
dump(results_[slot_].id, results_[slot_].w, results_[slot_].h, dump_prefix_ + "-out.pgm");
|
|
dump_prefix_.clear();
|
|
dumping_ = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
unsigned int GlUpscale::result(int slot, std::uint64_t generation) const {
|
|
slot &= 1;
|
|
return generation == generation_ && result_ready_[slot] ? results_[slot].id : 0;
|
|
}
|
|
|
|
void GlUpscale::collect_timers() {
|
|
#ifdef GL_COMPUTE_SHADER
|
|
for (int i = 0; i < kTimers; ++i) {
|
|
if (!timer_pending_[i]) continue;
|
|
GLint available = 0;
|
|
glGetQueryObjectiv(timers_[i], GL_QUERY_RESULT_AVAILABLE, &available);
|
|
if (!available) continue;
|
|
GLuint64 ns = 0;
|
|
glGetQueryObjectui64v(timers_[i], GL_QUERY_RESULT, &ns);
|
|
stats_.gpu_ns += ns;
|
|
stats_.max_step_ns = std::max<std::uint64_t>(stats_.max_step_ns, ns);
|
|
++stats_.steps;
|
|
timer_pending_[i] = false;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
GlUpscale::Stats GlUpscale::take_stats() {
|
|
collect_timers();
|
|
return std::exchange(stats_, Stats{});
|
|
}
|
|
|
|
// The red channel, through a framebuffer: GL 4.3 core and the PS5's GL both read those back.
|
|
void GlUpscale::dump(unsigned int texture, int w, int h, const std::string& path) const {
|
|
#ifdef GL_COMPUTE_SHADER
|
|
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT | GL_PIXEL_BUFFER_BARRIER_BIT);
|
|
GLint previous = 0;
|
|
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous);
|
|
GLuint fbo = 0;
|
|
glGenFramebuffers(1, &fbo);
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
|
std::vector<float> pixels(std::size_t(w) * h * 4);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, pixels.data());
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous));
|
|
glDeleteFramebuffers(1, &fbo);
|
|
FILE* file = std::fopen(path.c_str(), "wb");
|
|
if (!file) {
|
|
std::fprintf(stderr, "Upscale dump %s: %s\n", path.c_str(), std::strerror(errno));
|
|
return;
|
|
}
|
|
std::fprintf(file, "P5\n%d %d\n255\n", w, h);
|
|
std::vector<unsigned char> row(static_cast<std::size_t>(w));
|
|
for (int y = 0; y < h; ++y) {
|
|
for (int x = 0; x < w; ++x)
|
|
row[std::size_t(x)] = static_cast<unsigned char>(std::clamp(pixels[(std::size_t(y) * w + x) * 4], 0.0f, 1.0f) * 255 + 0.5f);
|
|
std::fwrite(row.data(), 1, row.size(), file);
|
|
}
|
|
std::fclose(file);
|
|
#else
|
|
(void)texture;
|
|
(void)w;
|
|
(void)h;
|
|
(void)path;
|
|
#endif
|
|
}
|