mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-17 20:01:26 +02:00
250 lines
7.9 KiB
C++
250 lines
7.9 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "ui/soft_canvas.h"
|
|
|
|
#include <cstring>
|
|
|
|
extern "C" {
|
|
#include <libswscale/swscale.h>
|
|
}
|
|
|
|
namespace ui {
|
|
|
|
namespace {
|
|
|
|
int clamp_int(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
|
|
|
|
// Source position in 1/256 of a pixel of the center of destination pixel d
|
|
// (GPU convention: pixel centers aligned).
|
|
int source_pos_256(int d, int src_len, int dst_len) {
|
|
const int64_t num = static_cast<int64_t>(2 * d + 1) * src_len * 256;
|
|
const int pos = static_cast<int>(num / (2 * static_cast<int64_t>(dst_len))) - 128;
|
|
return clamp_int(pos, 0, (src_len - 1) * 256);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
SoftCanvas::SoftCanvas(int width, int height)
|
|
: width_(width), height_(height),
|
|
pixels_(static_cast<size_t>(width) * height * 4, 0) {
|
|
clear();
|
|
}
|
|
|
|
SoftCanvas::~SoftCanvas() {
|
|
if (sws_) {
|
|
sws_freeContext(sws_);
|
|
}
|
|
}
|
|
|
|
void SoftCanvas::clear() {
|
|
for (size_t i = 0; i < pixels_.size(); i += 4) {
|
|
pixels_[i] = pixels_[i + 1] = pixels_[i + 2] = 0;
|
|
pixels_[i + 3] = 255;
|
|
}
|
|
}
|
|
|
|
uint32_t SoftCanvas::pixel_rgb(int x, int y) const {
|
|
const uint8_t* p = &pixels_[(static_cast<size_t>(y) * width_ + x) * 4];
|
|
return (static_cast<uint32_t>(p[0]) << 16) | (static_cast<uint32_t>(p[1]) << 8) | p[2];
|
|
}
|
|
|
|
TextureId SoftCanvas::create_texture(int w, int h, PixelFormat format) {
|
|
if (w <= 0 || h <= 0) {
|
|
return 0;
|
|
}
|
|
Texture t;
|
|
t.w = w;
|
|
t.h = h;
|
|
t.format = format;
|
|
t.data.assign(static_cast<size_t>(w) * h * (format == PixelFormat::Rgba8 ? 4 : 1), 0);
|
|
const TextureId id = next_id_++;
|
|
textures_[id] = t;
|
|
return id;
|
|
}
|
|
|
|
bool SoftCanvas::update_texture(TextureId id, int x, int y, int w, int h,
|
|
const uint8_t* pixels, int pitch) {
|
|
auto it = textures_.find(id);
|
|
if (it == textures_.end() || !pixels || x < 0 || y < 0 || w <= 0 || h <= 0 ||
|
|
x + w > it->second.w || y + h > it->second.h) {
|
|
return false;
|
|
}
|
|
Texture& t = it->second;
|
|
const int bpp = t.format == PixelFormat::Rgba8 ? 4 : 1;
|
|
for (int row = 0; row < h; ++row) {
|
|
std::memcpy(&t.data[(static_cast<size_t>(y + row) * t.w + x) * bpp],
|
|
pixels + static_cast<size_t>(row) * pitch, static_cast<size_t>(w) * bpp);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void SoftCanvas::destroy_texture(TextureId id) { textures_.erase(id); }
|
|
|
|
void SoftCanvas::set_clip(const Rect* clip) {
|
|
has_clip_ = clip != nullptr;
|
|
if (clip) {
|
|
clip_ = *clip;
|
|
}
|
|
}
|
|
|
|
Rect SoftCanvas::clip_area() const {
|
|
const Rect full = rect(0, 0, width_, height_);
|
|
return has_clip_ ? intersect(full, clip_) : full;
|
|
}
|
|
|
|
void SoftCanvas::blend(uint8_t* dst, int r, int g, int b, int a) {
|
|
if (a >= 255) {
|
|
dst[0] = static_cast<uint8_t>(r);
|
|
dst[1] = static_cast<uint8_t>(g);
|
|
dst[2] = static_cast<uint8_t>(b);
|
|
return;
|
|
}
|
|
if (a <= 0) {
|
|
return;
|
|
}
|
|
const int inv = 255 - a;
|
|
dst[0] = static_cast<uint8_t>((r * a + dst[0] * inv + 127) / 255);
|
|
dst[1] = static_cast<uint8_t>((g * a + dst[1] * inv + 127) / 255);
|
|
dst[2] = static_cast<uint8_t>((b * a + dst[2] * inv + 127) / 255);
|
|
}
|
|
|
|
void SoftCanvas::fill_rect(const Rect& r, Color color) {
|
|
const Rect area = intersect(r, clip_area());
|
|
if (area.w <= 0 || area.h <= 0) {
|
|
return; // entirely outside the image or the clip: nothing to write
|
|
}
|
|
for (int y = area.y; y < area.y + area.h; ++y) {
|
|
uint8_t* line = pixels_.data() + (static_cast<size_t>(y) * width_ + area.x) * 4;
|
|
for (int x = 0; x < area.w; ++x) {
|
|
blend(line + x * 4, color.r, color.g, color.b, color.a);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bilinear sample (premultiplied alpha during interpolation, to
|
|
// avoid dark fringes on transparent edges). out = R, G, B, A.
|
|
void SoftCanvas::sample(const Texture& t, const Rect& src, int fx, int fy,
|
|
int out[4]) const {
|
|
const int x0 = fx >> 8;
|
|
const int y0 = fy >> 8;
|
|
const int wx = fx & 255;
|
|
const int wy = fy & 255;
|
|
const int x1 = x0 + 1 < src.w ? x0 + 1 : x0;
|
|
const int y1 = y0 + 1 < src.h ? y0 + 1 : y0;
|
|
const int xs[4] = {x0, x1, x0, x1};
|
|
const int ys[4] = {y0, y0, y1, y1};
|
|
const int ws[4] = {(256 - wx) * (256 - wy), wx * (256 - wy), (256 - wx) * wy, wx * wy};
|
|
int64_t acc[4] = {0, 0, 0, 0};
|
|
for (int k = 0; k < 4; ++k) {
|
|
if (ws[k] == 0) {
|
|
continue;
|
|
}
|
|
const size_t idx = static_cast<size_t>(src.y + ys[k]) * t.w + (src.x + xs[k]);
|
|
int r = 255, g = 255, b = 255, a;
|
|
if (t.format == PixelFormat::Rgba8) {
|
|
const uint8_t* p = &t.data[idx * 4];
|
|
r = p[0], g = p[1], b = p[2], a = p[3];
|
|
} else {
|
|
a = t.data[idx];
|
|
}
|
|
acc[0] += static_cast<int64_t>(r) * a * ws[k];
|
|
acc[1] += static_cast<int64_t>(g) * a * ws[k];
|
|
acc[2] += static_cast<int64_t>(b) * a * ws[k];
|
|
acc[3] += static_cast<int64_t>(a) * ws[k];
|
|
}
|
|
if (acc[3] == 0) {
|
|
out[0] = out[1] = out[2] = out[3] = 0;
|
|
return;
|
|
}
|
|
out[0] = static_cast<int>(acc[0] / acc[3]);
|
|
out[1] = static_cast<int>(acc[1] / acc[3]);
|
|
out[2] = static_cast<int>(acc[2] / acc[3]);
|
|
out[3] = static_cast<int>((acc[3] + 32768) >> 16);
|
|
}
|
|
|
|
void SoftCanvas::draw_texture(TextureId id, const Rect& src, const Rect& dst,
|
|
Color tint) {
|
|
auto it = textures_.find(id);
|
|
if (it != textures_.end()) {
|
|
draw_texture_data(it->second, src, dst, tint);
|
|
}
|
|
}
|
|
|
|
void SoftCanvas::draw_texture_data(const Texture& t, const Rect& src_in, const Rect& dst,
|
|
Color tint) {
|
|
if (dst.w <= 0 || dst.h <= 0) {
|
|
return;
|
|
}
|
|
const Rect src = intersect(src_in, rect(0, 0, t.w, t.h));
|
|
if (src.w <= 0 || src.h <= 0) {
|
|
return;
|
|
}
|
|
const Rect area = intersect(dst, clip_area());
|
|
if (area.w <= 0 || area.h <= 0) {
|
|
// Entirely outside the image or the clip: area.x / area.y may
|
|
// then fall outside the image, no row address must even be computed.
|
|
return;
|
|
}
|
|
std::vector<int> col_pos(static_cast<size_t>(area.w));
|
|
for (int x = 0; x < area.w; ++x) {
|
|
col_pos[x] = source_pos_256(area.x + x - dst.x, src.w, dst.w);
|
|
}
|
|
int texel[4];
|
|
for (int y = area.y; y < area.y + area.h; ++y) {
|
|
const int fy = source_pos_256(y - dst.y, src.h, dst.h);
|
|
uint8_t* line = pixels_.data() + (static_cast<size_t>(y) * width_ + area.x) * 4;
|
|
for (int x = 0; x < area.w; ++x) {
|
|
sample(t, src, col_pos[x], fy, texel);
|
|
const int a = texel[3] * tint.a / 255;
|
|
blend(line + x * 4, texel[0] * tint.r / 255, texel[1] * tint.g / 255,
|
|
texel[2] * tint.b / 255, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SoftCanvas::submit_video_yuv(const uint8_t* y, const uint8_t* u, const uint8_t* v,
|
|
int stride_y, int stride_u, int stride_v, int w,
|
|
int h) {
|
|
if (!y || !u || !v || w <= 0 || h <= 0) {
|
|
return false;
|
|
}
|
|
sws_ = sws_getCachedContext(sws_, w, h, AV_PIX_FMT_YUV420P, w, h, AV_PIX_FMT_RGBA,
|
|
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
|
if (!sws_) {
|
|
return false;
|
|
}
|
|
video_.w = w;
|
|
video_.h = h;
|
|
video_.format = PixelFormat::Rgba8;
|
|
video_.data.resize(static_cast<size_t>(w) * h * 4);
|
|
const uint8_t* planes[3] = {y, u, v};
|
|
const int strides[3] = {stride_y, stride_u, stride_v};
|
|
uint8_t* out[1] = {video_.data.data()};
|
|
const int out_stride[1] = {w * 4};
|
|
sws_scale(sws_, planes, strides, 0, h, out, out_stride);
|
|
return true;
|
|
}
|
|
|
|
void SoftCanvas::draw_video(const Rect& area) {
|
|
if (video_.data.empty() || area.w <= 0 || area.h <= 0) {
|
|
return;
|
|
}
|
|
// Aspect ratio kept, centered (same rules as platform_sdl::fit_rect).
|
|
int w = area.w;
|
|
int h = static_cast<int>(static_cast<int64_t>(area.w) * video_.h / video_.w);
|
|
if (h > area.h) {
|
|
h = area.h;
|
|
w = static_cast<int>(static_cast<int64_t>(area.h) * video_.w / video_.h);
|
|
}
|
|
draw_texture_data(video_, rect(0, 0, video_.w, video_.h),
|
|
rect(area.x + (area.w - w) / 2, area.y + (area.h - h) / 2, w, h),
|
|
rgba(255, 255, 255));
|
|
}
|
|
|
|
void SoftCanvas::clear_video() {
|
|
video_.data.clear();
|
|
video_.w = video_.h = 0;
|
|
}
|
|
|
|
} // namespace ui
|