Skip to content

Commit 232a703

Browse files
committed
Improve screen capture scaling and backend casts
Validate and support scaled screen captures across backends and unify backend pointer handling. - Vulkan: check inputs, retrieve VulkanState from RendererState passed as user_data, support capturing a larger framebuffer region and resampling it to the requested size (nearest sampling). Added includes for renderer and vector. - OpenGL: compute capture region using viewport -> framebuffer scaling, clamp/crop to framebuffer bounds, read pixels from the computed region, and resample when capture size differs from requested size. Preserve row flip semantics. Improve input validation. - Metal/OpenGL common: replace static_cast pointer conversions with reinterpret_cast for backend pointers, fix anonymous namespace placement/visibility, and ensure renderer texture/backend pointers are cast consistently. - Python tool: treat missing debug fields safely and accept items whose debug contains "image_canvas" as image entries. These changes fix crashes and incorrect captures when UI scaling or viewport/framebuffer size mismatch occurs and make backend pointer usage more consistent.
1 parent 5abff6b commit 232a703

4 files changed

Lines changed: 166 additions & 37 deletions

File tree

src/imiv/imiv_capture.cpp

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
// https://github.com/AcademySoftwareFoundation/OpenImageIO
44

55
#include "imiv_types.h"
6+
#include "imiv_renderer.h"
67

78
#include <cmath>
89
#include <cstdio>
910
#include <cstring>
1011
#include <string>
12+
#include <vector>
1113

1214
namespace Imiv {
1315

@@ -414,10 +416,16 @@ bool
414416
imiv_vulkan_screen_capture(ImGuiID viewport_id, int x, int y, int w, int h,
415417
unsigned int* pixels, void* user_data)
416418
{
417-
VulkanState* vk_state = reinterpret_cast<VulkanState*>(user_data);
418-
if (vk_state == nullptr)
419+
if (pixels == nullptr || w <= 0 || h <= 0)
419420
return false;
420421

422+
// renderer_screen_capture() forwards the owning RendererState as user data.
423+
RendererState* renderer_state = reinterpret_cast<RendererState*>(user_data);
424+
if (renderer_state == nullptr || renderer_state->backend == nullptr)
425+
return false;
426+
VulkanState* vk_state = reinterpret_cast<VulkanState*>(
427+
renderer_state->backend);
428+
421429
int capture_x = x;
422430
int capture_y = y;
423431
int capture_w = w;
@@ -457,8 +465,55 @@ imiv_vulkan_screen_capture(ImGuiID viewport_id, int x, int y, int w, int h,
457465
vk_state->window_data.Height - capture_y);
458466
}
459467

460-
return capture_swapchain_region_rgba8(*vk_state, capture_x, capture_y,
461-
capture_w, capture_h, pixels);
468+
if (capture_w <= 0 || capture_h <= 0)
469+
return false;
470+
471+
if (capture_w == w && capture_h == h)
472+
return capture_swapchain_region_rgba8(*vk_state, capture_x, capture_y,
473+
capture_w, capture_h, pixels);
474+
475+
std::vector<unsigned int> captured_pixels(static_cast<size_t>(capture_w)
476+
* static_cast<size_t>(capture_h));
477+
if (!capture_swapchain_region_rgba8(*vk_state, capture_x, capture_y,
478+
capture_w, capture_h,
479+
captured_pixels.data())) {
480+
return false;
481+
}
482+
483+
const unsigned char* src_bytes = reinterpret_cast<const unsigned char*>(
484+
captured_pixels.data());
485+
unsigned char* dst_bytes = reinterpret_cast<unsigned char*>(pixels);
486+
const double sample_scale_x = static_cast<double>(capture_w)
487+
/ static_cast<double>(w);
488+
const double sample_scale_y = static_cast<double>(capture_h)
489+
/ static_cast<double>(h);
490+
for (int row = 0; row < h; ++row) {
491+
unsigned char* dst_row = dst_bytes
492+
+ static_cast<size_t>(row)
493+
* static_cast<size_t>(w) * 4;
494+
const int sample_row = std::clamp(
495+
static_cast<int>(std::floor((static_cast<double>(row) + 0.5)
496+
* sample_scale_y)),
497+
0, capture_h - 1);
498+
const unsigned char* src_row
499+
= src_bytes + static_cast<size_t>(sample_row)
500+
* static_cast<size_t>(capture_w) * 4;
501+
for (int col = 0; col < w; ++col) {
502+
const int sample_col = std::clamp(
503+
static_cast<int>(std::floor((static_cast<double>(col) + 0.5)
504+
* sample_scale_x)),
505+
0, capture_w - 1);
506+
const unsigned char* src
507+
= src_row + static_cast<size_t>(sample_col) * 4;
508+
unsigned char* dst = dst_row + static_cast<size_t>(col) * 4;
509+
dst[0] = src[0];
510+
dst[1] = src[1];
511+
dst[2] = src[2];
512+
dst[3] = src[3];
513+
}
514+
}
515+
516+
return true;
462517
}
463518

464519
#endif

src/imiv/imiv_renderer_metal.mm

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
namespace Imiv {
3535

36+
namespace {
37+
3638
struct RendererTextureBackendState {
3739
__strong id<MTLTexture> source_texture = nil;
3840
__strong id<MTLTexture> preview_linear_texture = nil;
@@ -111,32 +113,32 @@
111113
uint32_t data_type = 0;
112114
};
113115

114-
namespace {
115-
116116
RendererBackendState*
117117
backend_state(RendererState& renderer_state)
118118
{
119-
return static_cast<RendererBackendState*>(renderer_state.backend);
119+
return reinterpret_cast<RendererBackendState*>(renderer_state.backend);
120120
}
121121

122122
const RendererTextureBackendState*
123123
texture_backend_state(const RendererTexture& texture)
124124
{
125-
return static_cast<const RendererTextureBackendState*>(texture.backend);
125+
return reinterpret_cast<const RendererTextureBackendState*>(
126+
texture.backend);
126127
}
127128

128129
RendererTextureBackendState*
129130
texture_backend_state(RendererTexture& texture)
130131
{
131-
return static_cast<RendererTextureBackendState*>(texture.backend);
132+
return reinterpret_cast<RendererTextureBackendState*>(texture.backend);
132133
}
133134

134135
bool
135136
ensure_backend_state(RendererState& renderer_state)
136137
{
137138
if (renderer_state.backend != nullptr)
138139
return true;
139-
renderer_state.backend = new RendererBackendState();
140+
renderer_state.backend = reinterpret_cast<::Imiv::RendererBackendState*>(
141+
new RendererBackendState());
140142
return renderer_state.backend != nullptr;
141143
}
142144

@@ -1631,8 +1633,6 @@ inline float2 display_to_source_uv(float2 uv, int orientation)
16311633
return true;
16321634
}
16331635

1634-
} // namespace
1635-
16361636
bool
16371637
metal_get_viewer_texture_refs(const ViewerState& viewer,
16381638
const PlaceholderUiState& ui_state,
@@ -1732,7 +1732,8 @@ inline float2 display_to_source_uv(float2 uv, int orientation)
17321732
texture_state->input_channels = image.nchannels;
17331733
texture_state->preview_dirty = true;
17341734

1735-
texture.backend = texture_state;
1735+
texture.backend = reinterpret_cast<
1736+
::Imiv::RendererTextureBackendState*>(texture_state);
17361737
texture.preview_initialized = false;
17371738
error_message.clear();
17381739
return true;
@@ -2214,6 +2215,7 @@ inline float2 display_to_source_uv(float2 uv, int orientation)
22142215
return true;
22152216
}
22162217

2218+
} // namespace
22172219
} // namespace Imiv
22182220

22192221
namespace Imiv {

src/imiv/imiv_renderer_opengl.cpp

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@
132132

133133
namespace Imiv {
134134

135+
namespace {
136+
135137
struct RendererTextureBackendState {
136138
GLuint source_texture = 0;
137139
GLuint preview_linear_texture = 0;
@@ -251,11 +253,9 @@ struct RendererBackendState {
251253
OcioPreviewProgram ocio_preview;
252254
};
253255

254-
namespace {
255-
256256
RendererBackendState* backend_state(RendererState& renderer_state)
257257
{
258-
return static_cast<RendererBackendState*>(renderer_state.backend);
258+
return reinterpret_cast<RendererBackendState*>(renderer_state.backend);
259259
}
260260

261261
bool ensure_basic_preview_program(RendererBackendState& state,
@@ -266,19 +266,22 @@ namespace {
266266
const RendererTextureBackendState*
267267
texture_backend_state(const RendererTexture& texture)
268268
{
269-
return static_cast<const RendererTextureBackendState*>(texture.backend);
269+
return reinterpret_cast<const RendererTextureBackendState*>(
270+
texture.backend);
270271
}
271272

272273
RendererTextureBackendState* texture_backend_state(RendererTexture& texture)
273274
{
274-
return static_cast<RendererTextureBackendState*>(texture.backend);
275+
return reinterpret_cast<RendererTextureBackendState*>(texture.backend);
275276
}
276277

277278
bool ensure_backend_state(RendererState& renderer_state)
278279
{
279280
if (renderer_state.backend != nullptr)
280281
return true;
281-
renderer_state.backend = new RendererBackendState();
282+
renderer_state.backend
283+
= reinterpret_cast<::Imiv::RendererBackendState*>(
284+
new RendererBackendState());
282285
return renderer_state.backend != nullptr;
283286
}
284287

@@ -1461,8 +1464,6 @@ void main()
14611464
return true;
14621465
}
14631466

1464-
} // namespace
1465-
14661467
bool
14671468
opengl_get_viewer_texture_refs(const ViewerState& viewer,
14681469
const PlaceholderUiState& ui_state,
@@ -1552,7 +1553,8 @@ opengl_create_texture(RendererState& renderer_state, const LoadedImage& image,
15521553
return false;
15531554
}
15541555

1555-
texture.backend = texture_state;
1556+
texture.backend = reinterpret_cast<
1557+
::Imiv::RendererTextureBackendState*>(texture_state);
15561558
texture.preview_initialized = false;
15571559
error_message.clear();
15581560
return true;
@@ -1866,7 +1868,6 @@ bool
18661868
opengl_screen_capture(ImGuiID viewport_id, int x, int y, int w, int h,
18671869
unsigned int* pixels, void* user_data)
18681870
{
1869-
(void)viewport_id;
18701871
RendererState* renderer_state = reinterpret_cast<RendererState*>(user_data);
18711872
if (renderer_state == nullptr || pixels == nullptr || w <= 0 || h <= 0)
18721873
return false;
@@ -1882,30 +1883,99 @@ opengl_screen_capture(ImGuiID viewport_id, int x, int y, int w, int h,
18821883
framebuffer_height);
18831884
if (framebuffer_width <= 0 || framebuffer_height <= 0)
18841885
return false;
1885-
if (x < 0 || y < 0 || x + w > framebuffer_width
1886-
|| y + h > framebuffer_height) {
1887-
return false;
1886+
1887+
int capture_x = x;
1888+
int capture_y = y;
1889+
int capture_w = w;
1890+
int capture_h = h;
1891+
ImGuiViewport* viewport = ImGui::FindViewportByID(viewport_id);
1892+
if (viewport != nullptr && viewport->Size.x > 0.0f
1893+
&& viewport->Size.y > 0.0f) {
1894+
const double scale_x = static_cast<double>(framebuffer_width)
1895+
/ static_cast<double>(viewport->Size.x);
1896+
const double scale_y = static_cast<double>(framebuffer_height)
1897+
/ static_cast<double>(viewport->Size.y);
1898+
capture_x = static_cast<int>(std::lround(
1899+
(static_cast<double>(x) - static_cast<double>(viewport->Pos.x))
1900+
* scale_x));
1901+
capture_y = static_cast<int>(std::lround(
1902+
(static_cast<double>(y) - static_cast<double>(viewport->Pos.y))
1903+
* scale_y));
1904+
capture_w = std::max(1, static_cast<int>(std::lround(
1905+
static_cast<double>(w) * scale_x)));
1906+
capture_h = std::max(1, static_cast<int>(std::lround(
1907+
static_cast<double>(h) * scale_y)));
18881908
}
18891909

1890-
const int read_y = framebuffer_height - (y + h);
1910+
if (capture_x < 0) {
1911+
capture_w += capture_x;
1912+
capture_x = 0;
1913+
}
1914+
if (capture_y < 0) {
1915+
capture_h += capture_y;
1916+
capture_y = 0;
1917+
}
1918+
if (capture_x < framebuffer_width && capture_y < framebuffer_height) {
1919+
capture_w = std::min(capture_w, framebuffer_width - capture_x);
1920+
capture_h = std::min(capture_h, framebuffer_height - capture_y);
1921+
}
1922+
if (capture_w <= 0 || capture_h <= 0)
1923+
return false;
1924+
1925+
const int read_y = framebuffer_height - (capture_y + capture_h);
18911926
if (read_y < 0)
18921927
return false;
18931928

1894-
std::vector<unsigned char> readback(static_cast<size_t>(w)
1895-
* static_cast<size_t>(h) * 4);
1929+
std::vector<unsigned char> readback(static_cast<size_t>(capture_w)
1930+
* static_cast<size_t>(capture_h) * 4);
18961931
glPixelStorei(GL_PACK_ALIGNMENT, 1);
1897-
glReadPixels(x, read_y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, readback.data());
1932+
glReadPixels(capture_x, read_y, capture_w, capture_h, GL_RGBA,
1933+
GL_UNSIGNED_BYTE, readback.data());
18981934
if (glGetError() != GL_NO_ERROR)
18991935
return false;
19001936

19011937
unsigned char* dst_bytes = reinterpret_cast<unsigned char*>(pixels);
1938+
if (capture_w == w && capture_h == h) {
1939+
for (int row = 0; row < h; ++row) {
1940+
const size_t src_offset = static_cast<size_t>(h - 1 - row)
1941+
* static_cast<size_t>(w) * 4;
1942+
const size_t dst_offset = static_cast<size_t>(row)
1943+
* static_cast<size_t>(w) * 4;
1944+
std::memcpy(dst_bytes + dst_offset, readback.data() + src_offset,
1945+
static_cast<size_t>(w) * 4);
1946+
}
1947+
return true;
1948+
}
1949+
1950+
const double sample_scale_x = static_cast<double>(capture_w)
1951+
/ static_cast<double>(w);
1952+
const double sample_scale_y = static_cast<double>(capture_h)
1953+
/ static_cast<double>(h);
19021954
for (int row = 0; row < h; ++row) {
1903-
const size_t src_offset = static_cast<size_t>(h - 1 - row)
1904-
* static_cast<size_t>(w) * 4;
1905-
const size_t dst_offset = static_cast<size_t>(row)
1906-
* static_cast<size_t>(w) * 4;
1907-
std::memcpy(dst_bytes + dst_offset, readback.data() + src_offset,
1908-
static_cast<size_t>(w) * 4);
1955+
unsigned char* dst_row = dst_bytes
1956+
+ static_cast<size_t>(row)
1957+
* static_cast<size_t>(w) * 4;
1958+
const int sample_row = std::clamp(
1959+
static_cast<int>(std::floor((static_cast<double>(row) + 0.5)
1960+
* sample_scale_y)),
1961+
0, capture_h - 1);
1962+
const unsigned char* src_row
1963+
= readback.data()
1964+
+ static_cast<size_t>(capture_h - 1 - sample_row)
1965+
* static_cast<size_t>(capture_w) * 4;
1966+
for (int col = 0; col < w; ++col) {
1967+
const int sample_col = std::clamp(
1968+
static_cast<int>(std::floor((static_cast<double>(col) + 0.5)
1969+
* sample_scale_x)),
1970+
0, capture_w - 1);
1971+
const unsigned char* src
1972+
= src_row + static_cast<size_t>(sample_col) * 4;
1973+
unsigned char* dst = dst_row + static_cast<size_t>(col) * 4;
1974+
dst[0] = src[0];
1975+
dst[1] = src[1];
1976+
dst[2] = src[2];
1977+
dst[3] = src[3];
1978+
}
19091979
}
19101980
return true;
19111981
}
@@ -1939,6 +2009,7 @@ const RendererBackendVTable k_opengl_vtable = {
19392009
opengl_screen_capture,
19402010
};
19412011

2012+
} // namespace
19422013
} // namespace Imiv
19432014

19442015
namespace Imiv {

src/imiv/tools/imiv_sampling_regression.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ def _image_crop_rect(layout_path: Path) -> tuple[int, int, int, int]:
181181

182182
chosen_rect = None
183183
for item in image_window.get("items", []):
184-
if item.get("debug") == "image: Image":
184+
debug = item.get("debug") or ""
185+
if debug == "image: Image" or "image_canvas" in debug:
185186
chosen_rect = item.get("rect_clipped") or item.get("rect_full")
186187
break
187188

0 commit comments

Comments
 (0)