Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_async.cpp
Go to the documentation of this file.
1#include "mcp_async.h"
2
3#include "mcp_protocol.h"
4
6
7#include <graphics/graphics.h>
9#include <graphics/texture.h>
10#include <logging/logging.h>
11#include <ser20/external/base64.hpp>
12#include <uuid/uuid.h>
13
14#include <bimg/bimg.h>
15#include <bx/file.h>
16
17#include <fstream>
18#include <memory>
19#include <thread>
20
21namespace unravel::mcp
22{
23namespace
24{
25
26struct fbo_capture_state
27{
28 std::vector<uint8_t> pixels;
29 uint32_t ready_frame{0};
30 uint16_t width{0};
31 uint16_t height{0};
32 gfx::texture_handle blit_tex = BGFX_INVALID_HANDLE;
33 std::string error;
34};
35
36auto write_rgba_png(const std::filesystem::path& path,
37 uint16_t width,
38 uint16_t height,
39 const std::vector<uint8_t>& pixels,
40 std::string& error) -> bool
41{
42 bx::FileWriter writer;
43 if(!bx::open(&writer, path.generic_string().c_str()))
44 {
45 error = "Failed to open PNG for write: " + path.generic_string();
46 return false;
47 }
48
49 const uint32_t pitch = static_cast<uint32_t>(width) * 4u;
50 bimg::imageWritePng(&writer,
51 width,
52 height,
53 pitch,
54 pixels.data(),
55 bimg::TextureFormat::RGBA8,
56 false,
57 nullptr);
58 bx::close(&writer);
59 return true;
60}
61
62} // namespace
63
64auto wait_for_file(const std::filesystem::path& path,
65 std::chrono::milliseconds timeout,
66 std::chrono::milliseconds poll_interval) -> bool
67{
68 const auto deadline = std::chrono::steady_clock::now() + timeout;
69 while(std::chrono::steady_clock::now() < deadline)
70 {
71 std::error_code ec;
72 if(std::filesystem::exists(path, ec) && !ec)
73 {
74 const auto size = std::filesystem::file_size(path, ec);
75 if(!ec && size > 0)
76 {
77 // Allow a short settle so the writer finishes closing the file.
78 std::this_thread::sleep_for(std::chrono::milliseconds(16));
79 return true;
80 }
81 }
82 std::this_thread::sleep_for(poll_interval);
83 }
84 return false;
85}
86
87auto read_file_bytes(const std::filesystem::path& path, std::string& error) -> std::vector<uint8_t>
88{
89 std::ifstream file(path, std::ios::binary | std::ios::ate);
90 if(!file)
91 {
92 error = "Failed to open file: " + path.string();
93 return {};
94 }
95
96 const auto size = file.tellg();
97 if(size <= 0)
98 {
99 error = "Empty file: " + path.string();
100 return {};
101 }
102
103 std::vector<uint8_t> bytes(static_cast<size_t>(size));
104 file.seekg(0, std::ios::beg);
105 if(!file.read(reinterpret_cast<char*>(bytes.data()), size))
106 {
107 error = "Failed to read file: " + path.string();
108 return {};
109 }
110 return bytes;
111}
112
113auto encode_base64(const std::vector<uint8_t>& bytes) -> std::string
114{
115 return ser20::base64::encode(bytes.data(), bytes.size());
116}
117
118auto make_temp_screenshot_stem(const std::string& tag) -> std::filesystem::path
119{
120 const auto dir = std::filesystem::temp_directory_path() / "unravel_mcp";
121 std::error_code ec;
122 std::filesystem::create_directories(dir, ec);
123 const auto name = fmt::format("ss_{}_{}", tag, hpp::to_string(generate_uuid()));
124 return dir / name;
125}
126
128 rtti::context& ctx,
129 const std::function<gfx::frame_buffer::ptr(rtti::context&)>& resolve_fbo,
130 const std::string& tag,
131 std::chrono::milliseconds wait_timeout) -> tool_result
132{
133 // bgfx::requestScreenShot only works for *window* framebuffers. Scene/Game
134 // OBUFFER targets are offscreen, so we blit into a READ_BACK texture instead.
135 const auto stem = make_temp_screenshot_stem(tag);
136 const auto png_path = std::filesystem::path(stem.generic_string() + ".png");
137 auto state = std::make_shared<fbo_capture_state>();
138
139 const auto submitted = mcp.invoke_on_main(
140 [&ctx, &resolve_fbo, state]() -> bool
141 {
142 auto fbo = resolve_fbo(ctx);
143 if(!fbo || !fbo->is_valid())
144 {
145 state->error = "Viewport framebuffer is not available yet (panel may not have rendered)";
146 return false;
147 }
148
149 const auto& src_tex = fbo->get_texture(0);
150 if(!src_tex || !bgfx::isValid(src_tex->native_handle()))
151 {
152 state->error = "Viewport color texture is not available";
153 return false;
154 }
155
156 state->width = src_tex->info.width;
157 state->height = src_tex->info.height;
158 if(state->width == 0 || state->height == 0)
159 {
160 state->error = "Viewport framebuffer has zero size";
161 return false;
162 }
163
164 constexpr auto format = gfx::texture_format::RGBA8;
165 const uint64_t flags = BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK | BGFX_SAMPLER_U_CLAMP |
166 BGFX_SAMPLER_V_CLAMP;
167 state->blit_tex = gfx::create_texture_2d(state->width, state->height, false, 1, format, flags);
168 if(!bgfx::isValid(state->blit_tex))
169 {
170 state->error = "Failed to create readback texture";
171 return false;
172 }
173
174 gfx::texture_info info{};
175 gfx::calc_texture_size(info, state->width, state->height, 1, false, false, 1, format);
176 state->pixels.resize(info.storageSize);
177
178 gfx::render_pass pass("mcp_fbo_capture");
179 pass.touch();
180 gfx::blit(pass.id, state->blit_tex, 0, 0, src_tex->native_handle());
181 state->ready_frame = gfx::read_texture(state->blit_tex, state->pixels.data());
182 return true;
183 },
184 std::chrono::milliseconds(10000));
185
186 if(!submitted)
187 {
188 return {.text = "Timed out requesting screenshot on main thread", .is_error = true};
189 }
190 if(!*submitted)
191 {
192 return {.text = state->error.empty() ? "Failed to request screenshot" : state->error, .is_error = true};
193 }
194
195 const auto deadline = std::chrono::steady_clock::now() + wait_timeout;
196 bool ready = false;
197 while(std::chrono::steady_clock::now() < deadline)
198 {
199 auto frame_ready = mcp.invoke_on_main(
200 [state]() -> bool
201 {
202 return gfx::get_render_frame() >= state->ready_frame;
203 },
204 std::chrono::milliseconds(2000));
205 if(frame_ready && *frame_ready)
206 {
207 ready = true;
208 break;
209 }
210 std::this_thread::sleep_for(std::chrono::milliseconds(32));
211 }
212
213 if(!ready)
214 {
215 mcp.invoke_on_main(
216 [state]() -> bool
217 {
218 if(bgfx::isValid(state->blit_tex))
219 {
220 gfx::destroy(state->blit_tex);
221 state->blit_tex = BGFX_INVALID_HANDLE;
222 }
223 return true;
224 });
225 return {.text = fmt::format("Timed out waiting for GPU readback (frame {})", state->ready_frame),
226 .is_error = true};
227 }
228
229 std::string write_error;
230 const auto wrote = mcp.invoke_on_main(
231 [state, &png_path, &write_error]() -> bool
232 {
233 const bool ok = write_rgba_png(png_path, state->width, state->height, state->pixels, write_error);
234 if(bgfx::isValid(state->blit_tex))
235 {
236 gfx::destroy(state->blit_tex);
237 state->blit_tex = BGFX_INVALID_HANDLE;
238 }
239 return ok;
240 },
241 std::chrono::milliseconds(10000));
242
243 if(!wrote)
244 {
245 return {.text = "Timed out writing screenshot PNG on main thread", .is_error = true};
246 }
247 if(!*wrote)
248 {
249 return {.text = write_error.empty() ? "Failed to write screenshot PNG" : write_error, .is_error = true};
250 }
251
252 std::string read_error;
253 auto bytes = read_file_bytes(png_path, read_error);
254 std::error_code ec;
255 std::filesystem::remove(png_path, ec);
256
257 if(bytes.empty())
258 {
259 return {.text = read_error.empty() ? "Failed to read screenshot PNG" : read_error, .is_error = true};
260 }
261
262 tool_result result;
263 result.text = fmt::format(R"({{"source":{},"path":{},"bytes":{},"width":{},"height":{},"mime":"image/png"}})",
265 make_json_string(png_path.generic_string()),
266 bytes.size(),
267 state->width,
268 state->height);
270 result.image_mime = "image/png";
271 result.is_error = false;
272 return result;
273}
274
275} // namespace unravel::mcp
uint32_t width
uint32_t height
gfx::texture_format format
auto invoke_on_main(Fn &&fn, std::chrono::milliseconds timeout=std::chrono::milliseconds(5000)) -> std::optional< std::invoke_result_t< std::decay_t< Fn > > >
Definition mcp_manager.h:71
std::uint64_t bytes
Definition eviction.cpp:823
std::string name
Definition hub.cpp:33
std::string tag
Definition hub.cpp:32
gfx::texture_handle blit_tex
Definition mcp_async.cpp:32
std::string error
Definition mcp_async.cpp:33
uint32_t ready_frame
Definition mcp_async.cpp:29
std::vector< uint8_t > pixels
Definition mcp_async.cpp:28
uint32_t read_texture(texture_handle _handle, void *_data, uint16_t _layer, uint8_t _mip)
Definition graphics.cpp:759
bgfx::TextureHandle texture_handle
Definition graphics.h:51
void blit(view_id _id, texture_handle _dst, uint16_t _dstX, uint16_t _dstY, texture_handle _src, uint16_t _srcX, uint16_t _srcY, uint16_t _width, uint16_t _height)
void calc_texture_size(texture_info &_info, uint16_t _width, uint16_t _height, uint16_t _depth, bool _cubeMap, bool _hasMips, uint16_t _numLayers, texture_format _format)
Definition graphics.cpp:661
texture_handle create_texture_2d(uint16_t _width, uint16_t _height, bool _hasMips, uint16_t _numLayers, texture_format _format, uint64_t _flags, const memory_view *_mem)
Definition graphics.cpp:678
void destroy(index_buffer_handle _handle)
Definition graphics.cpp:505
bgfx::TextureInfo texture_info
Definition graphics.h:24
uint32_t get_render_frame()
auto read_file_bytes(const std::filesystem::path &path, std::string &error) -> std::vector< uint8_t >
Definition mcp_async.cpp:87
auto wait_for_file(const std::filesystem::path &path, std::chrono::milliseconds timeout, std::chrono::milliseconds poll_interval) -> bool
Definition mcp_async.cpp:64
auto encode_base64(const std::vector< uint8_t > &bytes) -> std::string
auto make_temp_screenshot_stem(const std::string &tag) -> std::filesystem::path
auto make_json_string(const std::string &value) -> std::string
auto capture_fbo_screenshot(mcp_manager &mcp, rtti::context &ctx, const std::function< gfx::frame_buffer::ptr(rtti::context &)> &resolve_fbo, const std::string &tag, std::chrono::milliseconds wait_timeout) -> tool_result
auto generate_uuid() -> hpp::uuid
Definition uuid.cpp:25
gfx::view_id id
void touch() const
std::string image_base64
Optional PNG (or other) payload returned as an MCP image content block.