6#include <Poco/Base64Encoder.h>
7#include <Poco/Exception.h>
8#include <Poco/JSON/Array.h>
9#include <Poco/JSON/Object.h>
10#include <Poco/JSON/Parser.h>
11#include <Poco/JSON/Stringifier.h>
12#include <Poco/Net/HTTPClientSession.h>
13#include <Poco/Net/HTTPRequest.h>
14#include <Poco/Net/HTTPResponse.h>
15#include <Poco/Net/NetException.h>
16#include <Poco/Net/SocketAddress.h>
17#include <Poco/Timespan.h>
27const char* kSystemPrompt =
28 "You are a rendering debug assistant integrated with the EVEngine game engine. "
29 "You receive a screenshot of the current rendered frame plus a block of engine "
30 "render parameters. Respond in concise English prose aimed at another LLM agent. "
31 "Cover: (1) what is visibly rendered (scene content, objects, colors, lighting, "
32 "UI overlays, any artifacts or anomalies); (2) how the provided render parameters "
33 "relate to the visible result (resolution, whether a 3D scene is active, readback "
34 "state, render-pipeline event counts); (3) any anomalies (black screen, missing "
35 "geometry, wrong colors, flicker) and the parameter likely responsible. Be specific "
36 "and reference the parameter names verbatim.";
39void splitBaseUrl(
const std::string& url, std::string& host,
unsigned& port,
40 std::string& basePath) {
44 std::string rest = url;
45 const auto scheme = rest.find(
"://");
46 if (scheme != std::string::npos) rest = rest.substr(scheme + 3);
47 const auto slash = rest.find(
'/');
48 std::string authority = (slash == std::string::npos) ? rest : rest.substr(0, slash);
49 if (slash != std::string::npos) basePath = rest.substr(slash);
51 while (basePath.size() > 1 && basePath.back() ==
'/') basePath.pop_back();
52 const auto colon = authority.rfind(
':');
53 if (colon != std::string::npos) {
54 host = authority.substr(0, colon);
56 port =
static_cast<unsigned>(std::stoi(authority.substr(colon + 1)));
63 if (host.empty()) host =
"127.0.0.1";
66std::string base64Encode(
const void* data,
size_t size) {
67 std::ostringstream oss;
69 Poco::Base64Encoder enc(oss);
70 enc.write(
static_cast<const char*
>(data),
static_cast<std::streamsize
>(size));
83void RenderVision::ensureEnvLocked() {
84 if (envLoaded_)
return;
86 if (
const char*
v = std::getenv(
"EVE_VISION_BASE_URL")) baseUrl_ =
v;
87 if (
const char*
v = std::getenv(
"EVE_VISION_API_KEY")) apiKey_ =
v;
88 if (
const char*
v = std::getenv(
"EVE_VISION_MODEL")) model_ =
v;
89 if (
const char*
v = std::getenv(
"EVE_VISION_PATH")) path_ =
v;
90 if (
const char*
v = std::getenv(
"EVE_VISION_TIMEOUT_MS")) {
92 timeoutMs_ = std::stoi(
v);
99 std::lock_guard<std::mutex> lock(mu_);
101 baseUrl_ = std::move(url);
104 std::lock_guard<std::mutex> lock(mu_);
106 apiKey_ = std::move(key);
109 std::lock_guard<std::mutex> lock(mu_);
111 model_ = std::move(
model);
114 std::lock_guard<std::mutex> lock(mu_);
116 if (path.empty()) path =
"/chat/completions";
117 if (path.front() !=
'/') path.insert(path.begin(),
'/');
118 path_ = std::move(path);
121 std::lock_guard<std::mutex> lock(mu_);
123 if (ms > 0) timeoutMs_ = ms;
127 std::lock_guard<std::mutex> lock(mu_);
129 return !baseUrl_.empty() && !model_.empty();
133 std::lock_guard<std::mutex> lock(mu_);
135 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
136 o->set(
"baseUrl", baseUrl_);
137 o->set(
"model", model_);
138 o->set(
"path", path_);
139 o->set(
"timeoutMs", timeoutMs_);
140 o->set(
"apiKeySet", !apiKey_.empty());
141 std::ostringstream oss;
142 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(o), oss, 0, 0);
147 std::lock_guard<std::mutex> lock(mu_);
152 std::lock_guard<std::mutex> lock(mu_);
157 std::lock_guard<std::mutex> lock(mu_);
158 pendingReason_ = reason.empty() ?
"breakpoint" : reason;
159 pendingLoc_ = source +
":" + std::to_string(
line);
160 pending_.store(
true);
166 std::lock_guard<std::mutex> lock(mu_);
167 return pendingReason_;
171 const std::string& reason) {
173 std::lock_guard<std::mutex> lock(mu_);
175 if (!fresh && !latest_.empty())
return latest_;
177 if (!cap)
return "error: Graphics module not available for vision describe";
178 return doDescribe(cap, renderDataJson, reason);
182 if (!pending_.load() || !cap)
return;
185 std::lock_guard<std::mutex> lock(mu_);
186 reason = pendingReason_;
189 pending_.store(
false);
190 doDescribe(cap, renderDataJson, reason);
193std::string RenderVision::doDescribe(
eve::IRenderCapture* cap,
const std::string& renderDataJson,
194 const std::string& reason) {
195 std::string baseUrl, apiKey,
model, path;
196 int timeoutMs = timeoutMs_;
198 std::lock_guard<std::mutex> lock(mu_);
204 timeoutMs = timeoutMs_;
206 if (baseUrl.empty() ||
model.empty()) {
207 const std::string err =
"error: RenderVision not configured (set EVE_VISION_BASE_URL/MODEL or call eve_render_vision_config)";
208 std::lock_guard<std::mutex> lock(mu_);
215 if (dataUrl.empty()) {
216 const std::string err =
"error: frame readback returned no image";
217 std::lock_guard<std::mutex> lock(mu_);
223 std::string context = reason.empty() ?
"Engine render parameters:" :
"Engine render parameters (dump reason: " + reason +
"):";
224 if (!renderDataJson.empty()) context +=
"\n" + renderDataJson;
226 Poco::JSON::Object::Ptr root = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
227 root->set(
"model",
model);
228 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
230 Poco::JSON::Object::Ptr sys = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
231 sys->set(
"role",
"system");
232 sys->set(
"content", kSystemPrompt);
235 Poco::JSON::Object::Ptr user = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
236 user->set(
"role",
"user");
237 Poco::JSON::Array::Ptr uc = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
239 Poco::JSON::Object::Ptr textPart = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
240 textPart->set(
"type",
"text");
241 textPart->set(
"text", context);
244 Poco::JSON::Object::Ptr imgPart = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
245 imgPart->set(
"type",
"image_url");
246 Poco::JSON::Object::Ptr iu = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
247 iu->set(
"url", dataUrl);
248 imgPart->set(
"image_url", iu);
251 user->set(
"content", uc);
253 root->set(
"messages", messages);
254 root->set(
"max_tokens", 1024);
256 std::ostringstream bodyStream;
257 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(root), bodyStream, 0, 0);
258 const std::string
body = bodyStream.str();
263 std::string basePath;
264 splitBaseUrl(baseUrl, host, port, basePath);
265 std::string endpoint = basePath + path;
266 if (endpoint.empty()) endpoint =
"/chat/completions";
268 std::string responseBody;
270 Poco::Net::HTTPClientSession session(host, port);
271 session.setTimeout(Poco::Timespan(timeoutMs / 1000, (timeoutMs % 1000) * 1000));
272 session.setKeepAlive(
false);
273 Poco::Net::HTTPRequest req(Poco::Net::HTTPRequest::HTTP_POST, endpoint,
274 Poco::Net::HTTPMessage::HTTP_1_1);
275 req.setContentType(
"application/json");
276 req.setChunkedTransferEncoding(
false);
277 req.setContentLength(
static_cast<int>(
body.size()));
278 if (!apiKey.empty()) req.set(
"Authorization",
"Bearer " + apiKey);
279 std::ostream&
os = session.sendRequest(req);
282 Poco::Net::HTTPResponse res;
283 std::istream& is = session.receiveResponse(res);
284 responseBody.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
285 if (res.getStatus() != Poco::Net::HTTPResponse::HTTP_OK) {
286 std::string snippet = responseBody.size() > 300 ? responseBody.substr(0, 300) : responseBody;
287 const std::string err =
"error: vision HTTP " + std::to_string(res.getStatus()) +
": " + snippet;
288 std::lock_guard<std::mutex> lock(mu_);
292 }
catch (
const Poco::Exception& e) {
293 const std::string err = std::string(
"error: vision request failed: ") + e.displayText();
294 std::lock_guard<std::mutex> lock(mu_);
297 }
catch (
const std::exception& e) {
298 const std::string err = std::string(
"error: vision request failed: ") + e.what();
299 std::lock_guard<std::mutex> lock(mu_);
306 Poco::JSON::Parser parser;
307 auto var = parser.parse(responseBody);
308 auto obj = var.extract<Poco::JSON::Object::Ptr>();
309 auto choices = obj ? obj->getArray(
"choices") :
nullptr;
310 if (!choices || choices->size() == 0) {
311 const std::string err =
"error: vision response had no choices";
312 std::lock_guard<std::mutex> lock(mu_);
316 auto c0 = choices->getObject(0);
317 auto msg = c0 ? c0->getObject(
"message") :
nullptr;
318 std::string content = msg ? msg->optValue<std::string>(
"content",
"") :
"";
319 if (content.empty()) {
320 const std::string err =
"error: vision response had empty content";
321 std::lock_guard<std::mutex> lock(mu_);
325 std::lock_guard<std::mutex> lock(mu_);
329 }
catch (
const Poco::Exception& e) {
330 const std::string err = std::string(
"error: vision response parse failed: ") + e.displayText();
331 std::lock_guard<std::mutex> lock(mu_);
334 }
catch (
const std::exception& e) {
335 const std::string err = std::string(
"error: vision response parse failed: ") + e.what();
336 std::lock_guard<std::mutex> lock(mu_);
Frame capture + camera + visible-entity inspection (graphics).
virtual std::string capturePngDataUrl()=0
Capture the last presented frame as a base64 PNG data URL.
Vision-model bridge for rendered-frame review (desktop / devtools only).
void notifyPending(const std::string &reason, const std::string &source, int line)
Record that a breakpoint / critical site wants a vision dump.
std::string configJson()
JSON description of current config (API key masked).
void setApiKey(std::string key)
std::string latest() const
void setModel(std::string model)
static RenderVision & instance()
std::string lastError() const
void pollPending(eve::IRenderCapture *cap, const std::string &renderDataJson)
McpServer::poll hook: performs one pending dump when safe, clears flag.
void setTimeoutMs(int ms)
void setPath(std::string path)
std::string pendingReason() const
std::string describe(eve::IRenderCapture *cap, const std::string &renderDataJson, bool fresh, const std::string &reason={})
Main-thread capture + vision describe. Returns the description text, or a string starting with "error...
void setBaseUrl(std::string url)
static T & get()
Returns the process-lifetime instance.