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, Poco::UInt16& 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<Poco::UInt16
>(std::stoi(authority.substr(colon + 1)));
63 if (
host.empty())
host =
"127.0.0.1";
73void RenderVision::ensureEnvLocked() {
74 if (envLoaded_)
return;
76 if (
const char*
v = std::getenv(
"EVE_VISION_BASE_URL")) baseUrl_ =
v;
77 if (
const char*
v = std::getenv(
"EVE_VISION_API_KEY")) apiKey_ =
v;
78 if (
const char*
v = std::getenv(
"EVE_VISION_MODEL")) model_ =
v;
79 if (
const char*
v = std::getenv(
"EVE_VISION_PATH")) path_ =
v;
80 if (
const char*
v = std::getenv(
"EVE_VISION_TIMEOUT_MS")) {
82 timeoutMs_ = std::stoi(
v);
89 std::lock_guard<std::mutex> lock(mu_);
91 baseUrl_ = std::move(url);
94 std::lock_guard<std::mutex> lock(mu_);
96 apiKey_ = std::move(
key);
99 std::lock_guard<std::mutex> lock(mu_);
101 model_ = std::move(
model);
104 std::lock_guard<std::mutex> lock(mu_);
106 if (
path.empty())
path =
"/chat/completions";
107 if (
path.front() !=
'/')
path.insert(
path.begin(),
'/');
108 path_ = std::move(
path);
111 std::lock_guard<std::mutex> lock(mu_);
113 if (ms > 0) timeoutMs_ = ms;
117 std::lock_guard<std::mutex> lock(mu_);
119 return !baseUrl_.empty() && !model_.empty();
123 std::lock_guard<std::mutex> lock(mu_);
125 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
126 o->set(
"baseUrl", baseUrl_);
127 o->set(
"model", model_);
128 o->set(
"path", path_);
129 o->set(
"timeoutMs", timeoutMs_);
130 o->set(
"apiKeySet", !apiKey_.empty());
131 std::ostringstream oss;
132 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(o), oss, 0, 0);
137 std::lock_guard<std::mutex> lock(mu_);
142 std::lock_guard<std::mutex> lock(mu_);
147 std::lock_guard<std::mutex> lock(mu_);
148 pendingReason_ = reason.empty() ?
"breakpoint" : reason;
149 pendingLoc_ =
source +
":" + std::to_string(
line);
150 pending_.store(
true);
156 std::lock_guard<std::mutex> lock(mu_);
157 return pendingReason_;
161 const std::string& reason) {
163 std::lock_guard<std::mutex> lock(mu_);
165 if (!fresh && !latest_.empty())
return latest_;
167 if (!cap)
return "error: Graphics module not available for vision describe";
168 return doDescribe(cap, renderDataJson, reason);
172 if (!pending_.load() || !cap)
return;
175 std::lock_guard<std::mutex> lock(mu_);
176 reason = pendingReason_;
179 pending_.store(
false);
180 doDescribe(cap, renderDataJson, reason);
183std::string RenderVision::doDescribe(
eve::IRenderCapture* cap,
const std::string& renderDataJson,
184 const std::string& reason) {
185 std::string baseUrl, apiKey,
model,
path;
186 int timeoutMs = timeoutMs_;
188 std::lock_guard<std::mutex> lock(mu_);
194 timeoutMs = timeoutMs_;
196 if (baseUrl.empty() ||
model.empty()) {
197 const std::string err =
"error: RenderVision not configured (set EVE_VISION_BASE_URL/MODEL or call eve_render_vision_config)";
198 std::lock_guard<std::mutex> lock(mu_);
205 if (dataUrl.empty()) {
206 const std::string err =
"error: frame readback returned no image";
207 std::lock_guard<std::mutex> lock(mu_);
213 std::string
context = reason.empty() ?
"Engine render parameters:" :
"Engine render parameters (dump reason: " + reason +
"):";
214 if (!renderDataJson.empty())
context +=
"\n" + renderDataJson;
216 Poco::JSON::Object::Ptr
root = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
218 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
220 Poco::JSON::Object::Ptr sys = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
221 sys->set(
"role",
"system");
222 sys->set(
"content", kSystemPrompt);
225 Poco::JSON::Object::Ptr user = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
226 user->set(
"role",
"user");
227 Poco::JSON::Array::Ptr uc = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
229 Poco::JSON::Object::Ptr textPart = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
230 textPart->set(
"type",
"text");
231 textPart->set(
"text",
context);
234 Poco::JSON::Object::Ptr imgPart = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
235 imgPart->set(
"type",
"image_url");
236 Poco::JSON::Object::Ptr iu = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
237 iu->set(
"url", dataUrl);
238 imgPart->set(
"image_url", iu);
241 user->set(
"content", uc);
243 root->set(
"messages", messages);
244 root->set(
"max_tokens", 1024);
246 std::ostringstream bodyStream;
247 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(
root), bodyStream, 0, 0);
248 const std::string
body = bodyStream.str();
252 Poco::UInt16 port = 80;
253 std::string basePath;
254 splitBaseUrl(baseUrl,
host, port, basePath);
255 std::string endpoint = basePath +
path;
256 if (endpoint.empty()) endpoint =
"/chat/completions";
258 std::string responseBody;
262 Poco::Timespan(
static_cast<long long>(timeoutMs) * 1000));
264 Poco::Net::HTTPRequest req(Poco::Net::HTTPRequest::HTTP_POST, endpoint,
265 Poco::Net::HTTPMessage::HTTP_1_1);
266 req.setContentType(
"application/json");
267 req.setChunkedTransferEncoding(
false);
268 req.setContentLength(
static_cast<int>(
body.size()));
269 if (!apiKey.empty()) req.set(
"Authorization",
"Bearer " + apiKey);
270 std::ostream& os =
session.sendRequest(req);
273 Poco::Net::HTTPResponse res;
274 std::istream& is =
session.receiveResponse(res);
275 responseBody.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
276 if (res.getStatus() != Poco::Net::HTTPResponse::HTTP_OK) {
277 std::string snippet = responseBody.size() > 300 ? responseBody.substr(0, 300) : responseBody;
278 const std::string err =
"error: vision HTTP " + std::to_string(res.getStatus()) +
": " + snippet;
279 std::lock_guard<std::mutex> lock(mu_);
283 }
catch (
const Poco::Exception& e) {
284 const std::string err = std::string(
"error: vision request failed: ") + e.displayText();
285 std::lock_guard<std::mutex> lock(mu_);
288 }
catch (
const std::exception& e) {
289 const std::string err = std::string(
"error: vision request failed: ") + e.what();
290 std::lock_guard<std::mutex> lock(mu_);
297 Poco::JSON::Parser parser;
298 auto var = parser.parse(responseBody);
299 auto obj = var.extract<Poco::JSON::Object::Ptr>();
300 auto choices = obj ? obj->getArray(
"choices") :
nullptr;
302 const std::string err =
"error: vision response had no choices";
303 std::lock_guard<std::mutex> lock(mu_);
307 auto c0 =
choices->getObject(0);
308 auto msg = c0 ? c0->getObject(
"message") :
nullptr;
309 std::string content = msg ? msg->optValue<std::string>(
"content",
"") :
"";
310 if (content.empty()) {
311 const std::string err =
"error: vision response had empty content";
312 std::lock_guard<std::mutex> lock(mu_);
316 std::lock_guard<std::mutex> lock(mu_);
320 }
catch (
const Poco::Exception& e) {
321 const std::string err = std::string(
"error: vision response parse failed: ") + e.displayText();
322 std::lock_guard<std::mutex> lock(mu_);
325 }
catch (
const std::exception& e) {
326 const std::string err = std::string(
"error: vision response parse failed: ") + e.what();
327 std::lock_guard<std::mutex> lock(mu_);
std::set< std::string > choices
std::optional< InteractionSession > session
const UnitySourceAsset & source
const VegetationPresetContext & context
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.