VSE: Faster timeline thumbnail drawing
VSE timeline, when many (hundreds/thousands) of thumbnails were visible, was very slow to redraw. This PR makes them 3-10x faster to redraw, by stopping doing things that are slow :) Part of #126087 thumbnail improvements task. - No longer do mute semitransparency or corner rounding on the CPU, do it in shader instead. - Stop creating a separate GPU texture for each thumbnail, on every repaint, and drawing each thumbnail as a separate draw call. Instead, put thumbnails into a single texture atlas (using a simple shelf packing algorithm), and draw them in batch, passing data via UBO. The atlas is still re-created every frame, but that does not seem to be a performance issue. Thumbnails are cropped horizontally based on how much of their parts are visible (e.g. a narrow strip on screen), so realistically the atlas size is kinda proportional to screen size, and ends up being just several megabytes of data transfer between CPU -> GPU each frame. On this Sprite Fright edit timeline view (612 visible thumbnails), time taken to repaint the timeline window: - Mac (M1 Max, Metal): 68.1ms -> 4.7ms - Windows (Ryzen 5950X, RTX 3080Ti, OpenGL): 23.7ms -> 6.8ms This also fixes a visual issue with thumbnails, where when strips are very tall, the "rounded corners" that were poked right into the thumbnail bitmap on the CPU were showing up due to actual bitmap being scaled up a lot. Pull Request: https://projects.blender.org/blender/blender/pulls/126972
This commit is contained in:
committed by
Aras Pranckevicius
parent
3cb95b0be9
commit
4c8f22447f
@@ -52,6 +52,8 @@ struct ListBase;
|
||||
|
||||
namespace blender::ed::seq {
|
||||
|
||||
class StripsDrawBatch;
|
||||
|
||||
struct SpaceSeq_Runtime : public NonCopyable {
|
||||
int rename_channel_index = 0;
|
||||
float timeline_clamp_custom_range = 0;
|
||||
@@ -158,16 +160,9 @@ ImBuf *sequencer_ibuf_get(const bContext *C,
|
||||
|
||||
/* `sequencer_thumbnails.cc` */
|
||||
|
||||
void draw_seq_strip_thumbnail(View2D *v2d,
|
||||
const bContext *C,
|
||||
Scene *scene,
|
||||
Sequence *seq,
|
||||
float y1,
|
||||
float y2,
|
||||
float y_top,
|
||||
float pixelx,
|
||||
float pixely,
|
||||
float round_radius);
|
||||
void draw_strip_thumbnails(TimelineDrawContext *ctx,
|
||||
blender::ed::seq::StripsDrawBatch &strips_batch,
|
||||
const blender::Vector<StripDrawContext> &strips);
|
||||
|
||||
/* sequencer_draw_channels.c */
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ class StripsDrawBatch {
|
||||
|
||||
void flush_batch();
|
||||
|
||||
private:
|
||||
/* Same math as `UI_view2d_view_to_region_*` but avoiding divisions,
|
||||
* and without relying on View2D data type. */
|
||||
inline float pos_to_pixel_space_x(float x) const
|
||||
@@ -74,6 +73,11 @@ class StripsDrawBatch {
|
||||
{
|
||||
return x * view_cur_inv_size_.x * view_mask_size_.x;
|
||||
}
|
||||
|
||||
GPUUniformBuf *get_ubo_context() const
|
||||
{
|
||||
return ubo_context_;
|
||||
}
|
||||
};
|
||||
|
||||
uint color_pack(const uchar rgba[4]);
|
||||
|
||||
@@ -6,65 +6,79 @@
|
||||
* \ingroup spseq
|
||||
*/
|
||||
|
||||
#include "BLI_math_base.h"
|
||||
#include "BLI_math_base.hh"
|
||||
#include "BLI_math_vector.hh"
|
||||
#include "BLI_math_vector_types.hh"
|
||||
#include "BLI_rect.h"
|
||||
|
||||
#include "BKE_context.hh"
|
||||
|
||||
#include "BLI_array.hh"
|
||||
|
||||
#include "IMB_imbuf.hh"
|
||||
|
||||
#include "DNA_sequence_types.h"
|
||||
#include "DNA_space_types.h"
|
||||
#include "DNA_view2d_types.h"
|
||||
|
||||
#include "BIF_glutil.hh"
|
||||
#include "GPU_batch_presets.hh"
|
||||
#include "GPU_matrix.hh"
|
||||
#include "GPU_shader_shared.hh"
|
||||
#include "GPU_texture.hh"
|
||||
|
||||
#include "IMB_colormanagement.hh"
|
||||
|
||||
#include "SEQ_channels.hh"
|
||||
#include "SEQ_render.hh"
|
||||
#include "SEQ_sequencer.hh"
|
||||
#include "SEQ_thumbnail_cache.hh"
|
||||
#include "SEQ_time.hh"
|
||||
|
||||
#include "sequencer_intern.hh" /* Own include. */
|
||||
#include "WM_api.hh"
|
||||
|
||||
#include "sequencer_intern.hh"
|
||||
#include "sequencer_strips_batch.hh"
|
||||
|
||||
using namespace blender;
|
||||
|
||||
static float thumb_calc_first_timeline_frame(const Scene *scene,
|
||||
Sequence *seq,
|
||||
/* Information for one thumbnail picture in the timeline. Note that a single
|
||||
* strip could have multiple thumbnails. */
|
||||
struct SeqThumbInfo {
|
||||
ImBuf *ibuf;
|
||||
/* Strip coordinates in timeline space (X: frames, Y: channels). */
|
||||
float left_handle, right_handle, bottom, top;
|
||||
/* Thumbnail coordinates in timeline space. */
|
||||
float x1, x2, y1, y2;
|
||||
/* Horizontal cropping of thumbnail image, in pixels. Often a thumbnail
|
||||
* does not have to be cropped, in which case these are 0 and ibuf->x-1. */
|
||||
float cropx_min, cropx_max;
|
||||
bool is_muted;
|
||||
};
|
||||
|
||||
static float thumb_calc_first_timeline_frame(const Sequence *seq,
|
||||
float left_handle,
|
||||
float frame_step,
|
||||
const rctf *view_area)
|
||||
{
|
||||
int first_drawable_frame = max_iii(
|
||||
SEQ_time_left_handle_frame_get(scene, seq), seq->start, view_area->xmin);
|
||||
int first_drawable_frame = max_iii(left_handle, seq->start, view_area->xmin);
|
||||
|
||||
/* First frame should correspond to handle position. */
|
||||
if (first_drawable_frame == SEQ_time_left_handle_frame_get(scene, seq)) {
|
||||
return SEQ_time_left_handle_frame_get(scene, seq);
|
||||
if (first_drawable_frame == left_handle) {
|
||||
return left_handle;
|
||||
}
|
||||
|
||||
float aligned_frame_offset = int((first_drawable_frame - seq->start) / frame_step) * frame_step;
|
||||
return seq->start + aligned_frame_offset;
|
||||
}
|
||||
|
||||
static float thumb_calc_next_timeline_frame(const Scene *scene,
|
||||
Sequence *seq,
|
||||
static float thumb_calc_next_timeline_frame(const Sequence *seq,
|
||||
float left_handle,
|
||||
float last_frame,
|
||||
float frame_step)
|
||||
{
|
||||
float next_frame = last_frame + frame_step;
|
||||
|
||||
/* If handle position was displayed, align next frame with `seq->start`. */
|
||||
if (last_frame == SEQ_time_left_handle_frame_get(scene, seq)) {
|
||||
if (last_frame == left_handle) {
|
||||
next_frame = seq->start + (int((last_frame - seq->start) / frame_step) + 1) * frame_step;
|
||||
}
|
||||
|
||||
return next_frame;
|
||||
}
|
||||
|
||||
static void seq_get_thumb_image_dimensions(Sequence *seq,
|
||||
static void seq_get_thumb_image_dimensions(const Sequence *seq,
|
||||
float pixelx,
|
||||
float pixely,
|
||||
float *r_thumb_width,
|
||||
@@ -92,145 +106,57 @@ static void seq_get_thumb_image_dimensions(Sequence *seq,
|
||||
float thumb_width = aspect_ratio * thumb_h_px * pixelx;
|
||||
|
||||
*r_thumb_width = thumb_width;
|
||||
if (r_image_width && r_image_height) {
|
||||
*r_image_width = image_width;
|
||||
*r_image_height = image_height;
|
||||
}
|
||||
*r_image_width = image_width;
|
||||
*r_image_height = image_height;
|
||||
}
|
||||
|
||||
static void make_ibuf_semitransparent(ImBuf *ibuf)
|
||||
static void get_seq_strip_thumbnails(const View2D *v2d,
|
||||
const bContext *C,
|
||||
Scene *scene,
|
||||
const StripDrawContext &strip,
|
||||
float pixelx,
|
||||
float pixely,
|
||||
bool is_muted,
|
||||
Vector<SeqThumbInfo> &r_thumbs)
|
||||
{
|
||||
const uchar alpha = 120;
|
||||
if (ibuf->byte_buffer.data) {
|
||||
uchar *buf = ibuf->byte_buffer.data;
|
||||
for (int pixel = ibuf->x * ibuf->y; pixel--; buf += 4) {
|
||||
buf[3] = alpha;
|
||||
}
|
||||
}
|
||||
if (ibuf->float_buffer.data) {
|
||||
float *buf = ibuf->float_buffer.data;
|
||||
for (int pixel = ibuf->x * ibuf->y; pixel--; buf += ibuf->channels) {
|
||||
buf[3] = (alpha / 255.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Signed distance to rounded box, centered at origin.
|
||||
* Reference: https://iquilezles.org/articles/distfunctions2d/ */
|
||||
static float sdf_rounded_box(float2 pos, float2 size, float radius)
|
||||
{
|
||||
float2 q = math::abs(pos) - size + radius;
|
||||
return math::min(math::max(q.x, q.y), 0.0f) + math::length(math::max(q, float2(0.0f))) - radius;
|
||||
}
|
||||
|
||||
static void eval_round_corners_pixel(
|
||||
ImBuf *ibuf, float radius, float2 bmin, float2 bmax, float2 pos)
|
||||
{
|
||||
int ix = int(pos.x);
|
||||
int iy = int(pos.y);
|
||||
if (ix < 0 || ix >= ibuf->x || iy < 0 || iy >= ibuf->y) {
|
||||
return;
|
||||
}
|
||||
float2 center = (bmin + bmax) * 0.5f;
|
||||
float2 size = (bmax - bmin) * 0.5f;
|
||||
float d = sdf_rounded_box(pos - center, size, radius);
|
||||
if (d <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
/* Outside of rounded rectangle, set pixel alpha to zero. */
|
||||
if (ibuf->byte_buffer.data != nullptr) {
|
||||
int64_t ofs = (int64_t(iy) * ibuf->x + ix) * 4;
|
||||
ibuf->byte_buffer.data[ofs + 3] = 0;
|
||||
}
|
||||
if (ibuf->float_buffer.data != nullptr) {
|
||||
int64_t ofs = (int64_t(iy) * ibuf->x + ix) * ibuf->channels;
|
||||
ibuf->float_buffer.data[ofs + 3] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static void make_ibuf_round_corners(ImBuf *ibuf, float radius, float2 bmin, float2 bmax)
|
||||
{
|
||||
/* Evaluate radius*radius squares at corners. */
|
||||
for (int by = 0; by < radius; by++) {
|
||||
for (int bx = 0; bx < radius; bx++) {
|
||||
eval_round_corners_pixel(ibuf, radius, bmin, bmax, float2(bmin.x + bx, bmin.y + by));
|
||||
eval_round_corners_pixel(ibuf, radius, bmin, bmax, float2(bmax.x - bx, bmin.y + by));
|
||||
eval_round_corners_pixel(ibuf, radius, bmin, bmax, float2(bmin.x + bx, bmax.y - by));
|
||||
eval_round_corners_pixel(ibuf, radius, bmin, bmax, float2(bmax.x - bx, bmax.y - by));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_seq_strip_thumbnail(View2D *v2d,
|
||||
const bContext *C,
|
||||
Scene *scene,
|
||||
Sequence *seq,
|
||||
float y1,
|
||||
float y2,
|
||||
float y_top,
|
||||
float pixelx,
|
||||
float pixely,
|
||||
float round_radius)
|
||||
{
|
||||
SpaceSeq *sseq = CTX_wm_space_seq(C);
|
||||
if ((sseq->flag & SEQ_SHOW_OVERLAY) == 0 ||
|
||||
(sseq->timeline_overlay.flag & SEQ_TIMELINE_SHOW_THUMBNAILS) == 0 ||
|
||||
!ELEM(seq->type, SEQ_TYPE_MOVIE, SEQ_TYPE_IMAGE))
|
||||
{
|
||||
if (!seq::strip_can_have_thumbnail(scene, strip.seq)) {
|
||||
return;
|
||||
}
|
||||
|
||||
StripElem *se = seq->strip->stripdata;
|
||||
if (se->orig_height == 0 || se->orig_width == 0) {
|
||||
/* No thumbnails is height of the strip is too small. */
|
||||
const float thumb_height = strip.strip_content_top - strip.bottom;
|
||||
if (thumb_height / pixely <= 20 * UI_SCALE_FAC) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* If width of the strip too small ignore drawing thumbnails. */
|
||||
if ((y2 - y1) / pixely <= 20 * UI_SCALE_FAC) {
|
||||
return;
|
||||
}
|
||||
|
||||
Editing *ed = SEQ_editing_get(scene);
|
||||
ListBase *channels = ed ? SEQ_channels_displayed_get(ed) : nullptr;
|
||||
|
||||
float thumb_width, image_width, image_height;
|
||||
const float thumb_height = y2 - y1;
|
||||
seq_get_thumb_image_dimensions(
|
||||
seq, pixelx, pixely, &thumb_width, thumb_height, &image_width, &image_height);
|
||||
strip.seq, pixelx, pixely, &thumb_width, thumb_height, &image_width, &image_height);
|
||||
|
||||
const float zoom_y = thumb_height / image_height;
|
||||
const float crop_x_multiplier = 1.0f / pixelx / (zoom_y / pixely);
|
||||
const float crop_x_multiplier = 1.0f / pixelx / (thumb_height / image_height / pixely);
|
||||
|
||||
float thumb_y_end = y1 + thumb_height;
|
||||
|
||||
const float seq_left_handle = SEQ_time_left_handle_frame_get(scene, seq);
|
||||
const float seq_right_handle = SEQ_time_right_handle_frame_get(scene, seq);
|
||||
|
||||
float upper_thumb_bound = SEQ_time_has_right_still_frames(scene, seq) ?
|
||||
SEQ_time_content_end_frame_get(scene, seq) :
|
||||
seq_right_handle;
|
||||
if (seq->type == SEQ_TYPE_IMAGE) {
|
||||
upper_thumb_bound = seq_right_handle;
|
||||
float upper_thumb_bound = min_ff(strip.right_handle, strip.content_end);
|
||||
if (strip.seq->type == SEQ_TYPE_IMAGE) {
|
||||
upper_thumb_bound = strip.right_handle;
|
||||
}
|
||||
|
||||
float timeline_frame = thumb_calc_first_timeline_frame(scene, seq, thumb_width, &v2d->cur);
|
||||
float timeline_frame = thumb_calc_first_timeline_frame(
|
||||
strip.seq, strip.left_handle, thumb_width, &v2d->cur);
|
||||
|
||||
/* Start drawing. */
|
||||
/* Start going over the strip length. */
|
||||
while (timeline_frame < upper_thumb_bound) {
|
||||
float thumb_x_end = timeline_frame + thumb_width;
|
||||
bool clipped = false;
|
||||
|
||||
/* Checks to make sure that thumbs are loaded only when in view and within the confines of the
|
||||
* strip. Some may not be required but better to have conditions for safety as x1 here is
|
||||
* point to start caching from and not drawing. */
|
||||
/* Reached end of view, no more thumbnails needed. */
|
||||
if (timeline_frame > v2d->cur.xmax) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Set the clipping bound to show the left handle moving over thumbs and not shift thumbs. */
|
||||
float cut_off = 0.0f;
|
||||
if (seq_left_handle > timeline_frame && seq_left_handle < thumb_x_end) {
|
||||
cut_off = seq_left_handle - timeline_frame;
|
||||
if (strip.left_handle > timeline_frame && strip.left_handle < thumb_x_end) {
|
||||
cut_off = strip.left_handle - timeline_frame;
|
||||
clipped = true;
|
||||
}
|
||||
|
||||
@@ -240,79 +166,232 @@ void draw_seq_strip_thumbnail(View2D *v2d,
|
||||
clipped = true;
|
||||
}
|
||||
|
||||
int cropx_min = int(cut_off * crop_x_multiplier);
|
||||
int cropx_max = int((thumb_x_end - timeline_frame) * crop_x_multiplier);
|
||||
if (cropx_max < 1) {
|
||||
float cropx_min = cut_off * crop_x_multiplier;
|
||||
float cropx_max = (thumb_x_end - timeline_frame) * crop_x_multiplier;
|
||||
if (cropx_max < 1.0f) {
|
||||
break;
|
||||
}
|
||||
rcti crop;
|
||||
BLI_rcti_init(&crop, cropx_min, cropx_max - 1, 0, int(image_height) - 1);
|
||||
|
||||
/* Get the thumbnail image. */
|
||||
ImBuf *ibuf = seq::thumbnail_cache_get(C, scene, seq, timeline_frame);
|
||||
if (ibuf && clipped) {
|
||||
/* Crop it to the part needed by the timeline view. */
|
||||
ImBuf *ibuf_cropped = IMB_dupImBuf(ibuf);
|
||||
if (crop.xmin < 0 || crop.ymin < 0) {
|
||||
crop.xmin = 0;
|
||||
crop.ymin = 0;
|
||||
}
|
||||
if (crop.xmax >= ibuf->x || crop.ymax >= ibuf->y) {
|
||||
crop.xmax = ibuf->x - 1;
|
||||
crop.ymax = ibuf->y - 1;
|
||||
}
|
||||
IMB_rect_crop(ibuf_cropped, &crop);
|
||||
IMB_freeImBuf(ibuf);
|
||||
ibuf = ibuf_cropped;
|
||||
}
|
||||
|
||||
/* If there is no image still, abort. */
|
||||
if (!ibuf) {
|
||||
ImBuf *ibuf = seq::thumbnail_cache_get(C, scene, strip.seq, timeline_frame);
|
||||
if (ibuf == nullptr) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Transparency on mute. */
|
||||
bool muted = channels ? SEQ_render_is_muted(channels, seq) : false;
|
||||
if (muted) {
|
||||
/* Work on a copy of the thumbnail image, so that transparency
|
||||
* is not stored into the thumbnail cache. */
|
||||
ImBuf *copy = IMB_dupImBuf(ibuf);
|
||||
IMB_freeImBuf(ibuf);
|
||||
ibuf = copy;
|
||||
make_ibuf_semitransparent(ibuf);
|
||||
SeqThumbInfo thumb = {};
|
||||
thumb.ibuf = ibuf;
|
||||
thumb.cropx_min = 0;
|
||||
thumb.cropx_max = ibuf->x - 1;
|
||||
if (clipped) {
|
||||
thumb.cropx_min = clamp_f(cropx_min, 0, ibuf->x - 1);
|
||||
thumb.cropx_max = clamp_f(cropx_max - 1 * 0, 0, ibuf->x - 1);
|
||||
}
|
||||
thumb.left_handle = strip.left_handle;
|
||||
thumb.right_handle = strip.right_handle;
|
||||
thumb.is_muted = is_muted;
|
||||
thumb.bottom = strip.bottom;
|
||||
thumb.top = strip.top;
|
||||
thumb.x1 = timeline_frame + cut_off;
|
||||
thumb.x2 = thumb_x_end;
|
||||
thumb.y1 = strip.bottom;
|
||||
thumb.y2 = strip.strip_content_top;
|
||||
r_thumbs.append(thumb);
|
||||
|
||||
timeline_frame = thumb_calc_next_timeline_frame(
|
||||
strip.seq, strip.left_handle, timeline_frame, thumb_width);
|
||||
}
|
||||
}
|
||||
|
||||
struct ThumbsDrawBatch {
|
||||
ed::seq::StripsDrawBatch &strips_batch_;
|
||||
Array<SeqStripThumbData> thumbs_;
|
||||
GPUUniformBuf *ubo_thumbs_ = nullptr;
|
||||
GPUShader *shader_ = nullptr;
|
||||
gpu::Batch *batch_ = nullptr;
|
||||
GPUTexture *atlas_ = nullptr;
|
||||
int binding_context_ = 0;
|
||||
int binding_thumbs_ = 0;
|
||||
int binding_image_ = 0;
|
||||
int thumbs_count_ = 0;
|
||||
|
||||
ThumbsDrawBatch(ed::seq::StripsDrawBatch &strips_batch, GPUTexture *atlas)
|
||||
: strips_batch_(strips_batch), thumbs_(GPU_SEQ_STRIP_DRAW_DATA_LEN), atlas_(atlas)
|
||||
{
|
||||
shader_ = GPU_shader_get_builtin_shader(GPU_SHADER_SEQUENCER_THUMBS);
|
||||
binding_thumbs_ = GPU_shader_get_ubo_binding(shader_, "thumb_data");
|
||||
binding_context_ = GPU_shader_get_ubo_binding(shader_, "context_data");
|
||||
binding_image_ = GPU_shader_get_sampler_binding(shader_, "image");
|
||||
|
||||
ubo_thumbs_ = GPU_uniformbuf_create(sizeof(SeqStripThumbData) * GPU_SEQ_STRIP_DRAW_DATA_LEN);
|
||||
|
||||
batch_ = GPU_batch_preset_quad();
|
||||
}
|
||||
|
||||
~ThumbsDrawBatch()
|
||||
{
|
||||
flush_batch();
|
||||
GPU_uniformbuf_unbind(ubo_thumbs_);
|
||||
GPU_uniformbuf_free(ubo_thumbs_);
|
||||
}
|
||||
|
||||
void add_thumb(
|
||||
const SeqThumbInfo &info, float width, const rcti &rect, int tex_width, int tex_height)
|
||||
{
|
||||
if (thumbs_count_ == GPU_SEQ_STRIP_DRAW_DATA_LEN) {
|
||||
flush_batch();
|
||||
}
|
||||
|
||||
/* If thumbnail start or end falls within strip corner rounding area,
|
||||
* we need to manually set thumbnail pixels that are outside of rounded
|
||||
* rectangle to be transparent. Ideally this would be done on the GPU
|
||||
* while drawing, but since rendering is done through OCIO shaders that
|
||||
* is hard to do. */
|
||||
const float xpos = timeline_frame + cut_off;
|
||||
SeqStripThumbData &res = thumbs_[thumbs_count_];
|
||||
thumbs_count_++;
|
||||
|
||||
const float zoom_x = (thumb_x_end - xpos) / ibuf->x;
|
||||
res.left = strips_batch_.pos_to_pixel_space_x(info.left_handle);
|
||||
res.right = strips_batch_.pos_to_pixel_space_x(info.right_handle);
|
||||
res.bottom = strips_batch_.pos_to_pixel_space_y(info.bottom);
|
||||
res.top = strips_batch_.pos_to_pixel_space_y(info.top);
|
||||
res.tint_color = float4(1.0f, 1.0f, 1.0f, info.is_muted ? 0.47f : 1.0f);
|
||||
res.x1 = strips_batch_.pos_to_pixel_space_x(info.x1);
|
||||
res.x2 = strips_batch_.pos_to_pixel_space_x(info.x2);
|
||||
res.y1 = strips_batch_.pos_to_pixel_space_y(info.y1);
|
||||
res.y2 = strips_batch_.pos_to_pixel_space_y(info.y2);
|
||||
res.u1 = float(rect.xmin) / float(tex_width);
|
||||
res.u2 = float(rect.xmin + width) / float(tex_width);
|
||||
res.v1 = float(rect.ymin) / float(tex_height);
|
||||
res.v2 = float(rect.ymax) / float(tex_height);
|
||||
}
|
||||
|
||||
const float radius = ibuf->y * round_radius * pixely / (y2 - y1);
|
||||
if (radius > 0.9f) {
|
||||
if (xpos < seq_left_handle + round_radius * pixelx ||
|
||||
thumb_x_end > seq_right_handle - round_radius * pixelx)
|
||||
{
|
||||
/* Work on a copy of the thumbnail image, so that corner rounding
|
||||
* is not stored into thumbnail cache. */
|
||||
ImBuf *copy = IMB_dupImBuf(ibuf);
|
||||
IMB_freeImBuf(ibuf);
|
||||
ibuf = copy;
|
||||
void flush_batch()
|
||||
{
|
||||
if (thumbs_count_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
float round_y_top = ibuf->y * (y_top - y1) / (y2 - y1);
|
||||
make_ibuf_round_corners(ibuf,
|
||||
radius,
|
||||
float2((seq_left_handle - xpos) / zoom_x, 0),
|
||||
float2((seq_right_handle - xpos) / zoom_x, round_y_top));
|
||||
GPU_uniformbuf_update(ubo_thumbs_, thumbs_.data());
|
||||
|
||||
GPU_shader_bind(shader_);
|
||||
GPU_uniformbuf_bind(ubo_thumbs_, binding_thumbs_);
|
||||
GPU_uniformbuf_bind(strips_batch_.get_ubo_context(), binding_context_);
|
||||
GPU_texture_bind(atlas_, binding_image_);
|
||||
|
||||
GPU_batch_set_shader(batch_, shader_);
|
||||
GPU_batch_draw_instance_range(batch_, 0, thumbs_count_);
|
||||
thumbs_count_ = 0;
|
||||
}
|
||||
};
|
||||
|
||||
void draw_strip_thumbnails(TimelineDrawContext *ctx,
|
||||
ed::seq::StripsDrawBatch &strips_batch,
|
||||
const Vector<StripDrawContext> &strips)
|
||||
{
|
||||
/* Nothing to do if we're not showing thumbnails overall. */
|
||||
if ((ctx->sseq->flag & SEQ_SHOW_OVERLAY) == 0 ||
|
||||
(ctx->sseq->timeline_overlay.flag & SEQ_TIMELINE_SHOW_THUMBNAILS) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Gather information for all thumbnails. */
|
||||
ListBase *channels = ctx->channels;
|
||||
Vector<SeqThumbInfo> thumbs;
|
||||
for (const StripDrawContext &strip : strips) {
|
||||
bool is_muted = channels ? SEQ_render_is_muted(channels, strip.seq) : false;
|
||||
get_seq_strip_thumbnails(
|
||||
ctx->v2d, ctx->C, ctx->scene, strip, ctx->pixelx, ctx->pixely, is_muted, thumbs);
|
||||
}
|
||||
if (thumbs.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ColorManagedViewSettings *view_settings;
|
||||
ColorManagedDisplaySettings *display_settings;
|
||||
IMB_colormanagement_display_settings_from_ctx(ctx->C, &view_settings, &display_settings);
|
||||
|
||||
/* Arrange thumbnail images into a texture atlas, using a simple
|
||||
* "add to current row until end, then start a new row". Thumbnail
|
||||
* images are most often same height (but varying width due to horizontal
|
||||
* cropping), so this simple algorithm works well enough. */
|
||||
constexpr int ATLAS_WIDTH = 4096;
|
||||
constexpr int ATLAS_MAX_HEIGHT = 4096;
|
||||
int cur_row_x = 0;
|
||||
int cur_row_y = 0;
|
||||
int cur_row_height = 0;
|
||||
Vector<rcti> rects;
|
||||
rects.reserve(thumbs.size());
|
||||
for (const SeqThumbInfo &info : thumbs) {
|
||||
int cropx_min = int(info.cropx_min);
|
||||
int cropx_max = int(math::ceil(info.cropx_max));
|
||||
int width = cropx_max - cropx_min + 1;
|
||||
int height = info.ibuf->y;
|
||||
cur_row_height = math::max(cur_row_height, height);
|
||||
|
||||
/* If this thumb would not fit onto current row, start a new row. */
|
||||
if (cur_row_x + width > ATLAS_WIDTH) {
|
||||
cur_row_y += cur_row_height + 1; /* +1 empty pixel for bilinear filter. */
|
||||
cur_row_height = height;
|
||||
cur_row_x = 0;
|
||||
if (cur_row_y > ATLAS_MAX_HEIGHT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ED_draw_imbuf_ctx_clipping(
|
||||
C, ibuf, xpos, y1, true, xpos, y1, thumb_x_end, thumb_y_end, zoom_x, zoom_y);
|
||||
IMB_freeImBuf(ibuf);
|
||||
timeline_frame = thumb_calc_next_timeline_frame(scene, seq, timeline_frame, thumb_width);
|
||||
/* Record our rect. */
|
||||
rcti rect{cur_row_x, cur_row_x + width, cur_row_y, cur_row_y + height};
|
||||
rects.append(rect);
|
||||
|
||||
/* Advance to next item inside row. */
|
||||
cur_row_x += width + 1; /* +1 empty pixel for bilinear filter. */
|
||||
}
|
||||
|
||||
/* Create the atlas GPU texture. */
|
||||
const int tex_width = ATLAS_WIDTH;
|
||||
const int tex_height = cur_row_y + cur_row_height;
|
||||
Array<uchar> tex_data(tex_width * tex_height * 4, 0);
|
||||
for (int64_t i = 0; i < rects.size(); i++) {
|
||||
/* Copy one thumbnail into atlas. */
|
||||
const rcti &rect = rects[i];
|
||||
SeqThumbInfo &info = thumbs[i];
|
||||
|
||||
void *cache_handle = nullptr;
|
||||
uchar *display_buffer = IMB_display_buffer_acquire(
|
||||
info.ibuf, view_settings, display_settings, &cache_handle);
|
||||
if (display_buffer != nullptr && info.ibuf != nullptr) {
|
||||
int cropx_min = int(info.cropx_min);
|
||||
int cropx_max = int(math::ceil(info.cropx_max));
|
||||
int width = cropx_max - cropx_min + 1;
|
||||
int height = info.ibuf->y;
|
||||
const uchar *src = display_buffer + cropx_min * 4;
|
||||
uchar *dst = &tex_data[(rect.ymin * ATLAS_WIDTH + rect.xmin) * 4];
|
||||
for (int y = 0; y < height; y++) {
|
||||
memcpy(dst, src, width * 4);
|
||||
src += info.ibuf->x * 4;
|
||||
dst += ATLAS_WIDTH * 4;
|
||||
}
|
||||
}
|
||||
IMB_display_buffer_release(cache_handle);
|
||||
|
||||
/* Release thumb image reference. */
|
||||
IMB_freeImBuf(info.ibuf);
|
||||
info.ibuf = nullptr;
|
||||
}
|
||||
GPUTexture *atlas = GPU_texture_create_2d(
|
||||
"thumb_atlas", tex_width, tex_height, 1, GPU_RGBA8, GPU_TEXTURE_USAGE_SHADER_READ, nullptr);
|
||||
GPU_texture_update(atlas, GPU_DATA_UBYTE, tex_data.data());
|
||||
GPU_texture_filter_mode(atlas, true);
|
||||
GPU_texture_extend_mode(atlas, GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER);
|
||||
|
||||
/* Draw all thumbnails. */
|
||||
GPU_matrix_push_projection();
|
||||
wmOrtho2_region_pixelspace(ctx->region);
|
||||
|
||||
ThumbsDrawBatch batch(strips_batch, atlas);
|
||||
for (int64_t i = 0; i < rects.size(); i++) {
|
||||
const rcti &rect = rects[i];
|
||||
const SeqThumbInfo &info = thumbs[i];
|
||||
batch.add_thumb(info, info.cropx_max - info.cropx_min + 1, rect, tex_width, tex_height);
|
||||
}
|
||||
batch.flush_batch();
|
||||
|
||||
GPU_matrix_pop_projection();
|
||||
|
||||
GPU_texture_unbind(atlas);
|
||||
GPU_texture_free(atlas);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
|
||||
#include "GPU_matrix.hh"
|
||||
|
||||
#include "IMB_imbuf.hh"
|
||||
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "SEQ_channels.hh"
|
||||
@@ -1519,9 +1521,9 @@ static void draw_seq_strips(TimelineDrawContext *timeline_ctx,
|
||||
UI_view2d_view_ortho(timeline_ctx->v2d);
|
||||
|
||||
/* Draw parts of strips below thumbnails. */
|
||||
GPU_blend(GPU_BLEND_ALPHA);
|
||||
draw_strips_background(timeline_ctx, strips_batch, strips);
|
||||
|
||||
GPU_blend(GPU_BLEND_ALPHA);
|
||||
const float round_radius = calc_strip_round_radius(timeline_ctx->pixely);
|
||||
for (const StripDrawContext &strip_ctx : strips) {
|
||||
draw_strip_offsets(timeline_ctx, &strip_ctx);
|
||||
@@ -1529,20 +1531,8 @@ static void draw_seq_strips(TimelineDrawContext *timeline_ctx,
|
||||
}
|
||||
timeline_ctx->quads->draw();
|
||||
|
||||
/* Draw all thumbnails and retiming continuity. */
|
||||
GPU_blend(GPU_BLEND_ALPHA);
|
||||
for (const StripDrawContext &strip_ctx : strips) {
|
||||
draw_seq_strip_thumbnail(timeline_ctx->v2d,
|
||||
timeline_ctx->C,
|
||||
timeline_ctx->scene,
|
||||
strip_ctx.seq,
|
||||
strip_ctx.bottom,
|
||||
strip_ctx.strip_content_top,
|
||||
strip_ctx.top,
|
||||
timeline_ctx->pixelx,
|
||||
timeline_ctx->pixely,
|
||||
round_radius);
|
||||
}
|
||||
/* Draw thumbnails. */
|
||||
draw_strip_thumbnails(timeline_ctx, strips_batch, strips);
|
||||
/* Draw retiming continuity ranges. */
|
||||
draw_retiming_continuity_ranges(timeline_ctx, strips);
|
||||
|
||||
|
||||
@@ -511,6 +511,8 @@ set(GLSL_SRC
|
||||
|
||||
shaders/gpu_shader_sequencer_strips_vert.glsl
|
||||
shaders/gpu_shader_sequencer_strips_frag.glsl
|
||||
shaders/gpu_shader_sequencer_thumbs_vert.glsl
|
||||
shaders/gpu_shader_sequencer_thumbs_frag.glsl
|
||||
|
||||
shaders/gpu_shader_codegen_lib.glsl
|
||||
|
||||
@@ -531,6 +533,7 @@ set(GLSL_SRC
|
||||
shaders/common/gpu_shader_math_rotation_lib.glsl
|
||||
shaders/common/gpu_shader_math_vector_lib.glsl
|
||||
shaders/common/gpu_shader_print_lib.glsl
|
||||
shaders/common/gpu_shader_sequencer_lib.glsl
|
||||
shaders/common/gpu_shader_shared_exponent_lib.glsl
|
||||
shaders/common/gpu_shader_smaa_lib.glsl
|
||||
shaders/common/gpu_shader_test_lib.glsl
|
||||
|
||||
@@ -77,6 +77,8 @@ enum eGPUBuiltinShader {
|
||||
|
||||
/** Draw strip widgets in sequencer timeline. */
|
||||
GPU_SHADER_SEQUENCER_STRIPS,
|
||||
/** Draw strip thumbnails in sequencer timeline. */
|
||||
GPU_SHADER_SEQUENCER_THUMBS,
|
||||
|
||||
/** Compute shaders to generate 2d index buffers (mainly for curve drawing). */
|
||||
GPU_SHADER_INDEXBUF_POINTS,
|
||||
|
||||
@@ -138,6 +138,17 @@ BLI_STATIC_ASSERT_ALIGN(SeqStripDrawData, 16)
|
||||
BLI_STATIC_ASSERT(sizeof(SeqStripDrawData) * GPU_SEQ_STRIP_DRAW_DATA_LEN <= 16384,
|
||||
"SeqStripDrawData UBO must not exceed minspec UBO size (16384)")
|
||||
|
||||
/* VSE per-thumbnail data for timeline rendering. */
|
||||
struct SeqStripThumbData {
|
||||
float left, right, bottom, top; /* Strip rectangle positions. */
|
||||
float x1, y1, x2, y2; /* Thumbnail rectangle positions. */
|
||||
float u1, v1, u2, v2; /* Thumbnail UVs. */
|
||||
float4 tint_color;
|
||||
};
|
||||
BLI_STATIC_ASSERT_ALIGN(SeqStripThumbData, 16)
|
||||
BLI_STATIC_ASSERT(sizeof(SeqStripThumbData) * GPU_SEQ_STRIP_DRAW_DATA_LEN <= 16384,
|
||||
"SeqStripThumbData UBO must not exceed minspec UBO size (16384)")
|
||||
|
||||
/* VSE global data for timeline rendering. */
|
||||
struct SeqContextDrawData {
|
||||
float round_radius;
|
||||
|
||||
@@ -89,6 +89,8 @@ static const char *builtin_shader_create_info_name(eGPUBuiltinShader shader)
|
||||
return "gpu_shader_gpencil_stroke";
|
||||
case GPU_SHADER_SEQUENCER_STRIPS:
|
||||
return "gpu_shader_sequencer_strips";
|
||||
case GPU_SHADER_SEQUENCER_THUMBS:
|
||||
return "gpu_shader_sequencer_thumbs";
|
||||
case GPU_SHADER_INDEXBUF_POINTS:
|
||||
return "gpu_shader_index_2d_array_points";
|
||||
case GPU_SHADER_INDEXBUF_LINES:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/* Signed distance to rounded box, centered at origin.
|
||||
* Reference: https://iquilezles.org/articles/distfunctions2d/ */
|
||||
float sdf_rounded_box(vec2 pos, vec2 size, float radius)
|
||||
{
|
||||
vec2 q = abs(pos) - size + radius;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
||||
}
|
||||
|
||||
void strip_box(float left,
|
||||
float right,
|
||||
float bottom,
|
||||
float top,
|
||||
vec2 pos,
|
||||
out vec2 r_pos1,
|
||||
out vec2 r_pos2,
|
||||
out vec2 r_size,
|
||||
out vec2 r_center,
|
||||
out vec2 r_pos,
|
||||
out float r_radius)
|
||||
{
|
||||
/* Snap to pixel grid coordinates, so that outline/border is non-fractional
|
||||
* pixel sizes. */
|
||||
r_pos1 = round(vec2(left, bottom));
|
||||
r_pos2 = round(vec2(right, top));
|
||||
/* Make sure strip is at least 1px wide. */
|
||||
r_pos2.x = max(r_pos2.x, r_pos1.x + 1.0);
|
||||
r_size = (r_pos2 - r_pos1) * 0.5;
|
||||
r_center = (r_pos1 + r_pos2) * 0.5;
|
||||
r_pos = round(pos);
|
||||
|
||||
r_radius = context_data.round_radius;
|
||||
if (r_radius > r_size.x) {
|
||||
r_radius = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/* Signed distance to rounded box, centered at origin.
|
||||
* Reference: https://iquilezles.org/articles/distfunctions2d/ */
|
||||
float sdf_rounded_box(vec2 pos, vec2 size, float radius)
|
||||
{
|
||||
vec2 q = abs(pos) - size + radius;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
||||
}
|
||||
#pragma BLENDER_REQUIRE(gpu_shader_sequencer_lib.glsl)
|
||||
|
||||
vec3 color_shade(vec3 rgb, float shade)
|
||||
{
|
||||
@@ -44,20 +38,19 @@ void main()
|
||||
|
||||
SeqStripDrawData strip = strip_data[strip_id];
|
||||
|
||||
/* Snap to pixel grid coordinates, so that outline/border is non-fractional
|
||||
* pixel sizes. */
|
||||
vec2 pos1 = round(vec2(strip.left_handle, strip.bottom));
|
||||
vec2 pos2 = round(vec2(strip.right_handle, strip.top));
|
||||
/* Make sure strip is at least 1px wide. */
|
||||
pos2.x = max(pos2.x, pos1.x + 1.0);
|
||||
vec2 size = (pos2 - pos1) * 0.5;
|
||||
vec2 center = (pos1 + pos2) * 0.5;
|
||||
vec2 pos = round(co);
|
||||
|
||||
float radius = context_data.round_radius;
|
||||
if (radius > size.x) {
|
||||
radius = 0.0;
|
||||
}
|
||||
vec2 pos1, pos2, size, center, pos;
|
||||
float radius;
|
||||
strip_box(strip.left_handle,
|
||||
strip.right_handle,
|
||||
strip.bottom,
|
||||
strip.top,
|
||||
co,
|
||||
pos1,
|
||||
pos2,
|
||||
size,
|
||||
center,
|
||||
pos,
|
||||
radius);
|
||||
|
||||
bool border = (strip.flags & GPU_SEQ_FLAG_BORDER) != 0;
|
||||
bool selected = (strip.flags & GPU_SEQ_FLAG_SELECTED) != 0;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma BLENDER_REQUIRE(gpu_shader_sequencer_lib.glsl)
|
||||
|
||||
void main()
|
||||
{
|
||||
SeqStripThumbData thumb = thumb_data[thumb_id];
|
||||
vec2 pos1, pos2, size, center, pos;
|
||||
float radius;
|
||||
strip_box(thumb.left,
|
||||
thumb.right,
|
||||
thumb.bottom,
|
||||
thumb.top,
|
||||
pos_interp,
|
||||
pos1,
|
||||
pos2,
|
||||
size,
|
||||
center,
|
||||
pos,
|
||||
radius);
|
||||
|
||||
/* Sample thumbnail texture, modulate with color. */
|
||||
vec4 col = texture(image, texCoord_interp) * thumb.tint_color;
|
||||
|
||||
/* Outside of strip rounded rectangle? */
|
||||
float sdf = sdf_rounded_box(pos - center, size, radius);
|
||||
if (sdf > 0.0) {
|
||||
col = vec4(0.0);
|
||||
}
|
||||
|
||||
fragColor = col;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
void main()
|
||||
{
|
||||
int id = gl_InstanceID;
|
||||
thumb_id = id;
|
||||
int vid = gl_VertexID;
|
||||
SeqStripThumbData thumb = thumb_data[id];
|
||||
vec4 coords = vec4(thumb.x1, thumb.y1, thumb.x2, thumb.y2);
|
||||
vec4 uvs = vec4(thumb.u1, thumb.v1, thumb.u2, thumb.v2);
|
||||
|
||||
vec2 co;
|
||||
vec2 uv;
|
||||
if (vid == 0) {
|
||||
co = coords.xw;
|
||||
uv = uvs.xw;
|
||||
}
|
||||
else if (vid == 1) {
|
||||
co = coords.xy;
|
||||
uv = uvs.xy;
|
||||
}
|
||||
else if (vid == 2) {
|
||||
co = coords.zw;
|
||||
uv = uvs.zw;
|
||||
}
|
||||
else {
|
||||
co = coords.zy;
|
||||
uv = uvs.zy;
|
||||
}
|
||||
|
||||
pos_interp = co;
|
||||
texCoord_interp = uv;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(co, 0.0f, 1.0f);
|
||||
}
|
||||
@@ -23,3 +23,20 @@ GPU_SHADER_CREATE_INFO(gpu_shader_sequencer_strips)
|
||||
.vertex_source("gpu_shader_sequencer_strips_vert.glsl")
|
||||
.fragment_source("gpu_shader_sequencer_strips_frag.glsl")
|
||||
.do_static_compilation(true);
|
||||
|
||||
GPU_SHADER_INTERFACE_INFO(gpu_seq_thumb_iface, "")
|
||||
.no_perspective(Type::VEC2, "pos_interp")
|
||||
.no_perspective(Type::VEC2, "texCoord_interp")
|
||||
.flat(Type::UINT, "thumb_id");
|
||||
|
||||
GPU_SHADER_CREATE_INFO(gpu_shader_sequencer_thumbs)
|
||||
.vertex_out(gpu_seq_thumb_iface)
|
||||
.fragment_out(0, Type::VEC4, "fragColor")
|
||||
.push_constant(Type::MAT4, "ModelViewProjectionMatrix")
|
||||
.uniform_buf(0, "SeqStripThumbData", "thumb_data[GPU_SEQ_STRIP_DRAW_DATA_LEN]")
|
||||
.uniform_buf(1, "SeqContextDrawData", "context_data")
|
||||
.sampler(0, ImageType::FLOAT_2D, "image")
|
||||
.typedef_source("GPU_shader_shared.hh")
|
||||
.vertex_source("gpu_shader_sequencer_thumbs_vert.glsl")
|
||||
.fragment_source("gpu_shader_sequencer_thumbs_frag.glsl")
|
||||
.do_static_compilation(true);
|
||||
|
||||
@@ -55,4 +55,6 @@ void thumbnail_cache_discard_requests_outside(Scene *scene, const rctf &rect);
|
||||
void thumbnail_cache_clear(Scene *scene);
|
||||
void thumbnail_cache_destroy(Scene *scene);
|
||||
|
||||
bool strip_can_have_thumbnail(const Scene *scene, const Sequence *seq);
|
||||
|
||||
} // namespace blender::seq
|
||||
|
||||
@@ -156,7 +156,7 @@ static ThumbnailCache *query_thumbnail_cache(Scene *scene)
|
||||
return scene->ed->runtime.thumbnail_cache;
|
||||
}
|
||||
|
||||
static bool can_have_thumbnail(Scene *scene, const Sequence *seq)
|
||||
bool strip_can_have_thumbnail(const Scene *scene, const Sequence *seq)
|
||||
{
|
||||
if (scene == nullptr || scene->ed == nullptr || seq == nullptr) {
|
||||
return false;
|
||||
@@ -489,7 +489,7 @@ ImBuf *thumbnail_cache_get(const bContext *C,
|
||||
const Sequence *seq,
|
||||
float timeline_frame)
|
||||
{
|
||||
if (!can_have_thumbnail(scene, seq)) {
|
||||
if (!strip_can_have_thumbnail(scene, seq)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -516,7 +516,7 @@ ImBuf *thumbnail_cache_get(const bContext *C,
|
||||
|
||||
void thumbnail_cache_invalidate_strip(Scene *scene, const Sequence *seq)
|
||||
{
|
||||
if (!can_have_thumbnail(scene, seq)) {
|
||||
if (!strip_can_have_thumbnail(scene, seq)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user