diff --git a/xbox/native-bios/App.cpp b/xbox/native-bios/App.cpp index af9ba87745..91496a0413 100644 --- a/xbox/native-bios/App.cpp +++ b/xbox/native-bios/App.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#include #include "QuickJsEngine.hpp" #include "PhotoDiscService.hpp" #include "OskiewarLivePublisher.hpp" @@ -425,7 +426,15 @@ public: m_oskiewarAccount = std::make_shared(*m_api); m_api->system.render_width = m_frameWidth; m_api->system.render_height = m_frameHeight; - m_api->system.version = "1.0.0.46"; + { + // What capabilities().version reports is the installed package, not a + // literal that drifts from Package.appxmanifest. + const auto id = Windows::ApplicationModel::Package::Current->Id->Version; + char version[32]; + std::snprintf(version, sizeof version, "%u.%u.%u.%u", unsigned(id.Major), + unsigned(id.Minor), unsigned(id.Build), unsigned(id.Revision)); + m_api->system.version = version; + } m_api->telemetry = [this](std::string_view line) { std::string safe(line); for (auto& character : safe) if (character == '\n' || character == '\r') character = ' '; diff --git a/xbox/native-bios/Package.appxmanifest b/xbox/native-bios/Package.appxmanifest index cb3b16c60d..f76fbd06e6 100644 --- a/xbox/native-bios/Package.appxmanifest +++ b/xbox/native-bios/Package.appxmanifest @@ -6,7 +6,7 @@ IgnorableNamespaces="uap mp rescap"> + Version="1.0.0.49" /> diff --git a/xbox/native-bios/QuickJsEngine.cpp b/xbox/native-bios/QuickJsEngine.cpp index 1c572b4270..f76febc3af 100644 --- a/xbox/native-bios/QuickJsEngine.cpp +++ b/xbox/native-bios/QuickJsEngine.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "QuickJsEngine.hpp" #include "../runtime/include/ac/theme_assets.hpp" @@ -15,7 +16,11 @@ extern "C" { namespace ac::xbox { namespace { -struct CallScope { Api* api; }; +struct RetainedMesh; // ScenePrimitives.inc +// Meshes the piece uploads once and draws by handle; owned by the piece, so +// they live exactly as long as its JS context. +struct SceneStore { std::vector> meshes; }; +struct CallScope { Api* api; SceneStore scene; }; #include "ScenePrimitives.inc" bool ValidOskiewarMatchId(std::string_view value) { @@ -837,6 +842,10 @@ JSValue Capabilities(JSContext* context, JSValueConst, int, JSValueConst*) { JS_SetPropertyStr(context, result, "antialiasingMode", JS_NewString(context, scope->api->system.antialiasing_mode.c_str())); JS_SetPropertyStr(context, result, "liveLocalState", JS_NewBool(context, true)); + // The scene API level the piece may assume (xbox/ENGINE.md). Additive only: + // 1 = triangles3d/disc3d/capsule3d/sceneMesh, 2 = + postEffects and retained + // meshes (meshUpload/meshDraw/meshFree). + JS_SetPropertyStr(context, result, "sceneApi", JS_NewInt32(context, 2)); return result; } @@ -969,6 +978,9 @@ class QuickJsPiece final : public JsPiece { JS_SetPropertyStr(context_, global, "disc3d", JS_NewCFunction(context_, SceneDisc, "disc3d", 7)); JS_SetPropertyStr(context_, global, "capsule3d", JS_NewCFunction(context_, SceneCapsule, "capsule3d", 9)); JS_SetPropertyStr(context_, global, "sceneMesh", JS_NewCFunction(context_, SceneMesh, "sceneMesh", 3)); + JS_SetPropertyStr(context_, global, "meshUpload", JS_NewCFunction(context_, MeshUpload, "meshUpload", 3)); + JS_SetPropertyStr(context_, global, "meshDraw", JS_NewCFunction(context_, MeshDraw, "meshDraw", 6)); + JS_SetPropertyStr(context_, global, "meshFree", JS_NewCFunction(context_, MeshFree, "meshFree", 1)); JS_SetPropertyStr(context_, global, "triangle3d", JS_NewCFunction(context_, Triangle3d, "triangle3d", 12)); JS_SetPropertyStr(context_, global, "triangles3d", JS_NewCFunction(context_, Triangles3d, "triangles3d", 2)); JS_SetPropertyStr(context_, global, "sprites3d", JS_NewCFunction(context_, Sprites3d, "sprites3d", 2)); diff --git a/xbox/native-bios/ScenePrimitives.inc b/xbox/native-bios/ScenePrimitives.inc index 4e9dd6351f..89b94e8451 100644 --- a/xbox/native-bios/ScenePrimitives.inc +++ b/xbox/native-bios/ScenePrimitives.inc @@ -35,19 +35,22 @@ JSValue SceneDisc(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ for(int i=1;i<=n;i++){double a=i*6.283185307179586/n;ScenePoint next{v[0]+std::cos(a)*v[3],v[1]+std::sin(a)*v[3],v[2]};SceneTriangle(scope->api->graphics,center,last,next,color);last=next;} return JS_UNDEFINED; } +// The fan both capsule3d and a retained mesh's edge capsules emit. +static void SceneCapsuleFan(Graphics& g,double x1,double y1,double x2,double y2,double z,double width,Color color){ + const double radius=width*.5,dx=x2-x1,dy=y2-y1,length=std::hypot(dx,dy); + if(radius<0||radius>8192)return; + for(double v:{x1,y1,x2,y2})if(std::abs(v)+radius>32200)return; + const double ux=length>1e-6?dx/length:1,uy=length>1e-6?dy/length:0,nx=-uy*radius,ny=ux*radius; + ScenePoint a{x1+nx,y1+ny,z},b{x1-nx,y1-ny,z},c{x2+nx,y2+ny,z},d{x2-nx,y2-ny,z};SceneTriangle(g,a,b,c,color);SceneTriangle(g,b,d,c,color); + const int n=radius<6?3:radius<13?4:radius<26?6:radius<52?8:12; + for(int end:{-1,1}){const double x=end<0?x1:x2,y=end<0?y1:y2;ScenePoint center{x,y,z},last{x+nx,y+ny,z}; + for(int i=1;i<=n;i++){const double angle=i*3.141592653589793/n,side=std::cos(angle),along=std::sin(angle)*end*radius;ScenePoint next{x+nx*side+ux*along,y+ny*side+uy*along,z};SceneTriangle(g,center,last,next,color);last=next;}} +} JSValue SceneCapsule(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ auto* scope=static_cast(JS_GetContextOpaque(ctx));double v[9]{}; if(!scope||!scope->api||argc<9)return JS_ThrowTypeError(ctx,"capsule3d requires endpoints,z,width,r,g,b"); for(int i=0;i<9;i++)if(JS_ToFloat64(ctx,&v[i],args[i])||!std::isfinite(v[i]))return JS_ThrowTypeError(ctx,"non-finite capsule"); - const double radius=v[5]*.5,dx=v[2]-v[0],dy=v[3]-v[1],length=std::hypot(dx,dy); - if(radius<0||radius>8192)return JS_UNDEFINED; - for(int i=0;i<4;i++)if(std::abs(v[i])+radius>32200)return JS_UNDEFINED; - const double ux=length>1e-6?dx/length:1,uy=length>1e-6?dy/length:0,nx=-uy*radius,ny=ux*radius; - const auto color=SceneColor(v[6],v[7],v[8]);auto& g=scope->api->graphics; - ScenePoint a{v[0]+nx,v[1]+ny,v[4]},b{v[0]-nx,v[1]-ny,v[4]},c{v[2]+nx,v[3]+ny,v[4]},d{v[2]-nx,v[3]-ny,v[4]};SceneTriangle(g,a,b,c,color);SceneTriangle(g,b,d,c,color); - const int n=radius<6?3:radius<13?4:radius<26?6:radius<52?8:12; - for(int end:{-1,1}){const double x=end<0?v[0]:v[2],y=end<0?v[1]:v[3];ScenePoint center{x,y,v[4]},last{x+nx,y+ny,v[4]}; - for(int i=1;i<=n;i++){const double angle=i*3.141592653589793/n,side=std::cos(angle),along=std::sin(angle)*end*radius;ScenePoint next{x+nx*side+ux*along,y+ny*side+uy*along,v[4]};SceneTriangle(g,center,last,next,color);last=next;}} + SceneCapsuleFan(scope->api->graphics,v[0],v[1],v[2],v[3],v[4],v[5],SceneColor(v[6],v[7],v[8])); return JS_UNDEFINED; } JSValue SceneMesh(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ @@ -71,3 +74,137 @@ JSValue SceneMesh(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ } return JS_NewInt32(ctx,emitted); } + +// ---- Retained meshes (sceneApi 2). Uploaded once per map, drawn by handle +// with the same 27-float camera sceneMesh takes. Validation, bounds and +// lighting happen at upload; a draw is a bounds test, then a clip walk over +// stack arrays with an all-inside fast path — no per-face allocation. +struct RetainedMesh { + std::vector verts; // world space + std::vector faces; // 10 per face: i0..i3, r,g,b, nx,ny,nz + std::vector capsules; // 11 per capsule: x1,y1,z1, x2,y2,z2, worldWidth, r,g,b, depthBias + double aabb[6]{}; + std::vector lit; double litLight[3]{NAN,NAN,NAN}; + std::vector view; // per-draw scratch +}; +static bool SceneCameraValid(const float* m){ + for(size_t i=0;i<27;i++)if(!std::isfinite(m[i]))return false; + return !(m[17]<=0||m[18]<-32200||m[19]<-32200||m[20]>32200||m[21]>32200||m[18]>=m[20]||m[19]>=m[21]); +} +static ScenePoint SceneToView(const float* m,const ScenePoint& p){ + const double x=p.x-m[0],y=p.y-m[1],z=p.z-m[2]; + return {x*m[3]+y*m[4]+z*m[5],x*m[6]+y*m[7]+z*m[8],x*m[9]+y*m[10]+z*m[11]}; +} +static ScenePoint SceneProject(const float* m,const ScenePoint& p){ + const double k=m[14]+(m[15]/p.z-m[14])*m[16]; + return {m[12]+p.x*k,m[13]-p.y*k,std::clamp(double(m[22])+p.z*m[23],-1.499,1.4)}; +} +template static int SceneClipFixed(const ScenePoint* in,int n,ScenePoint* out,Distance dist){ + int count=0; + for(int i=0;i=0)out[count++]=a;if((da>=0)!=(db>=0))out[count++]=SceneMix(a,b,da/(da-db));} + return count; +} +JSValue MeshUpload(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ + auto* scope=static_cast(JS_GetContextOpaque(ctx));SceneFloats verts(ctx),faces(ctx),capsules(ctx); + if(!scope||!scope->api||argc<2||!verts.read(args[0])||!faces.read(args[1]))return JS_NewInt32(ctx,-1); + const bool hasCapsules=argc>2&&!JS_IsUndefined(args[2])&&!JS_IsNull(args[2]); + if(hasCapsules&&!capsules.read(args[2]))return JS_NewInt32(ctx,-1); + if(verts.count%3||faces.count%10||capsules.count%11||verts.count>300000||faces.count>100000||capsules.count>22000)return JS_NewInt32(ctx,-1); + auto mesh=std::make_shared(); + mesh->verts.reserve(verts.count/3); + for(size_t i=0;iverts.push_back({verts.data[i],verts.data[i+1],verts.data[i+2]});} + for(size_t i=0;i=double(mesh->verts.size())||std::floor(id)!=id)return JS_NewInt32(ctx,-1);}} + for(size_t i=0;ifaces.assign(faces.data,faces.data+faces.count); + if(hasCapsules)mesh->capsules.assign(capsules.data,capsules.data+capsules.count); + double* b=mesh->aabb;b[0]=b[1]=b[2]=1e300;b[3]=b[4]=b[5]=-1e300; + for(const auto& p:mesh->verts){b[0]=std::min(b[0],p.x);b[1]=std::min(b[1],p.y);b[2]=std::min(b[2],p.z);b[3]=std::max(b[3],p.x);b[4]=std::max(b[4],p.y);b[5]=std::max(b[5],p.z);} + for(size_t i=0;iverts.empty()&&capsules.count==0){b[0]=b[1]=b[2]=b[3]=b[4]=b[5]=0;} + auto& store=scope->scene.meshes; + for(size_t i=0;i=4096)return JS_NewInt32(ctx,-1); + store.push_back(mesh);return JS_NewInt32(ctx,int32_t(store.size()-1)); +} +JSValue MeshFree(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ + auto* scope=static_cast(JS_GetContextOpaque(ctx));int32_t handle=-1; + if(!scope||argc<1||JS_ToInt32(ctx,&handle,args[0]))return JS_FALSE; + auto& store=scope->scene.meshes; + if(handle<0||size_t(handle)>=store.size()||!store[handle])return JS_FALSE; + store[handle].reset();return JS_TRUE; +} +// meshDraw(handle, camera, capsuleScale = 1, tintR = 1, tintG = 1, tintB = 1) → triangles emitted. +// capsuleScale is the game's cameraScale(): edge capsules store a world width +// and take their pixel width from it, as drawQuadMesh did in JS. +JSValue MeshDraw(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ + auto* scope=static_cast(JS_GetContextOpaque(ctx));int32_t handle=-1;SceneFloats camera(ctx); + if(!scope||!scope->api||argc<2||JS_ToInt32(ctx,&handle,args[0]))return JS_NewInt32(ctx,0); + auto& store=scope->scene.meshes; + if(handle<0||size_t(handle)>=store.size()||!store[handle])return JS_NewInt32(ctx,0); + if(!camera.read(args[1])||camera.count!=27||!SceneCameraValid(camera.data))return JS_ThrowRangeError(ctx,"meshDraw needs a 27-float camera"); + double extra[4]{1,1,1,1}; + for(int i=0;i<4&&argc>2+i;i++){if(JS_ToFloat64(ctx,&extra[i],args[2+i])||!std::isfinite(extra[i]))return JS_ThrowRangeError(ctx,"meshDraw scale/tint");} + const double capsuleScale=std::clamp(extra[0],0.0,1024.0);const double tint[3]{std::clamp(extra[1],0.0,4.0),std::clamp(extra[2],0.0,4.0),std::clamp(extra[3],0.0,4.0)}; + const bool tinted=tint[0]!=1||tint[1]!=1||tint[2]!=1; + RetainedMesh& mesh=*store[handle];const float* m=camera.data;const double near=m[17]; + auto& g=scope->api->graphics; + // Bounds: the eight corners in view space. All behind the lens, or all past + // one edge of the band once projected, and nothing here can be seen. + {const double* b=mesh.aabb;int behind=0,left=0,right=0,above=0,below=0; + for(int c=0;c<8;c++){ScenePoint p=SceneToView(m,{c&1?b[3]:b[0],c&2?b[4]:b[1],c&4?b[5]:b[2]}); + if(p.zm[20])right++;if(q.ym[21])below++;} + if(behind==8)return JS_NewInt32(ctx,0); + if(behind==0&&(left==8||right==8||above==8||below==8))return JS_NewInt32(ctx,0);} + // Lighting is a function of the light alone; relight only when it moves. + const size_t faceCount=mesh.faces.size()/10; + if(mesh.lit.size()!=faceCount||mesh.litLight[0]!=m[24]||mesh.litLight[1]!=m[25]||mesh.litLight[2]!=m[26]){ + mesh.lit.resize(faceCount);mesh.litLight[0]=m[24];mesh.litLight[1]=m[25];mesh.litLight[2]=m[26]; + for(size_t i=0;im[20])right++;if(a[j].ym[21])below++; + if(a[j].x>=m[18]&&a[j].x<=m[20]&&a[j].y>=m[19]&&a[j].y<=m[21])inside++;} + if(left==4||right==4||above==4||below==4)continue; + if(inside==4){SceneTriangle(g,a[0],a[1],a[2],color);SceneTriangle(g,a[0],a[2],a[3],color);emitted+=2;continue;} + }else{ + n=SceneClipFixed(a,n,b,[&](const ScenePoint& p){return p.z-near;});if(n<3)continue; + for(int j=0;jm[20]+guard&&to.x>m[20]+guard)||(from.ym[21]+guard&&to.y>m[21]+guard))continue; + Color color=SceneColor(c[7],c[8],c[9]); + if(tinted)color=SceneColor(color.r*tint[0],color.g*tint[1],color.b*tint[2]); + const double width=std::max(1.0,double(c[6])*capsuleScale),depth=std::clamp(std::min(from.z,to.z)+double(c[10]),-1.499,1.4); + const double radius=width*.5;const int fan=radius<6?3:radius<13?4:radius<26?6:radius<52?8:12; + SceneCapsuleFan(g,from.x,from.y,to.x,to.y,depth,width,color);emitted+=2+2*fan; + } + return JS_NewInt32(ctx,emitted); +} diff --git a/xbox/native-bios/tests/quickjs_engine_smoke.cpp b/xbox/native-bios/tests/quickjs_engine_smoke.cpp index 97cfb2d026..1dfb64f7ec 100644 --- a/xbox/native-bios/tests/quickjs_engine_smoke.cpp +++ b/xbox/native-bios/tests/quickjs_engine_smoke.cpp @@ -56,7 +56,7 @@ int main() { api.clock.network_synced = true; api.clock.network_offset_ms = 3; api.clock.network_rtt_ms = 21; api.audio.output_latency_ms = 11.5; api.audio.midi_status = "no-input"; api.audio.midi_gate = true; api.audio.midi_pitch_bend = 9000; - auto piece = engine.compile({"smoke", "test", "function boot(){telemetry('BOOT','OK');gameSignal('bullet',1,.5,.25);saveReplay('{\"format\":\"ac.oskiedemo\"}');publishLive('ow-bafegu-dorimi-kunapo','{\"seq\":1}');publishLive('ow-sokku135','{\"seq\":1}');ac();if(!discScan())throw Error('disc scan')} function sim(){if(!gamepad().connected||!gamepad(0).connected||!gamepad(1).connected||gamepad(2).connected||gamepad(1).leftX!==-1)throw Error('indexed gamepads');drum('kick',1,0);const r=runtime();if(!r.clockSynced||r.clockOffsetMs!==3||r.audioLatencyMs!==11.5||r.midiStatus!=='no-input'||!r.midiGate||r.midiPitchBend!==9000||r.clientErrorReportStatus!=='posted to server smoke')throw Error('runtime telemetry');const d=disc();if(d.status!=='ready'||d.volume!=='D:'||d.name!=='PHOTO.JPG'||d.count!==3||d.index!==1||d.width!==1600||d.height!==1200||!d.currentReady)throw Error('disc state');if(!discShow(-1))throw Error('disc show');capabilities();controllers();oscillator(220,.1)} function paint(){wipe(1,2,3);box(1,2,3,4,5,6,7);line(1,2,3,4,2,5,6,7);triangle(1,2,3,4,5,6,7,8,9);const batch=new Float32Array([1,2,.1,3,4,.1,5,6,.1,7,8,9,10,20,.2,30,40,.2,50,60,.2,70,80,90]);if(triangles3d(batch)!==2)throw Error('triangle batch');const textured=new Float32Array([1,2,.1,0,0,3,4,.1,1,0,5,6,.1,0,1,255,255,255]);if(texturedTriangles3d(textured,1)!==1)throw Error('texture batch');const sprites=new Float32Array([100,200,.3,16,255,80,90,1]);if(sprites3d(sprites,1)!==1)throw Error('sprite batch');write('OK',8,9,10,11,12,13);systemWrite('HI',20,30,40);ywftWrite('YWFT',20,70,40);comicWrite('COMIC',20,110,40);systemGlyph('ButtonA',50,60,70);painting(80,90,100,110);stampPainting('#j8t',200,300,1);discPhoto(0,0,1920,1080);blur(4);if(!postEffects(.4,.2,.1,99,-60,3))throw Error('post effects')} function act(b){if(b==='A')synth(440,.01);if(b==='B'){oscillatorStop();if(!discCopy())throw Error('disc copy')}}", "test"}, {}, error); + auto piece = engine.compile({"smoke", "test", "function boot(){telemetry('BOOT','OK');gameSignal('bullet',1,.5,.25);saveReplay('{\"format\":\"ac.oskiedemo\"}');publishLive('ow-bafegu-dorimi-kunapo','{\"seq\":1}');publishLive('ow-sokku135','{\"seq\":1}');ac();if(!discScan())throw Error('disc scan')} function sim(){if(!gamepad().connected||!gamepad(0).connected||!gamepad(1).connected||gamepad(2).connected||gamepad(1).leftX!==-1)throw Error('indexed gamepads');drum('kick',1,0);const r=runtime();if(!r.clockSynced||r.clockOffsetMs!==3||r.audioLatencyMs!==11.5||r.midiStatus!=='no-input'||!r.midiGate||r.midiPitchBend!==9000||r.clientErrorReportStatus!=='posted to server smoke')throw Error('runtime telemetry');const d=disc();if(d.status!=='ready'||d.volume!=='D:'||d.name!=='PHOTO.JPG'||d.count!==3||d.index!==1||d.width!==1600||d.height!==1200||!d.currentReady)throw Error('disc state');if(!discShow(-1))throw Error('disc show');capabilities();controllers();oscillator(220,.1)} function paint(){wipe(1,2,3);box(1,2,3,4,5,6,7);line(1,2,3,4,2,5,6,7);triangle(1,2,3,4,5,6,7,8,9);const batch=new Float32Array([1,2,.1,3,4,.1,5,6,.1,7,8,9,10,20,.2,30,40,.2,50,60,.2,70,80,90]);if(triangles3d(batch)!==2)throw Error('triangle batch');const textured=new Float32Array([1,2,.1,0,0,3,4,.1,1,0,5,6,.1,0,1,255,255,255]);if(texturedTriangles3d(textured,1)!==1)throw Error('texture batch');const sprites=new Float32Array([100,200,.3,16,255,80,90,1]);if(sprites3d(sprites,1)!==1)throw Error('sprite batch');write('OK',8,9,10,11,12,13);systemWrite('HI',20,30,40);ywftWrite('YWFT',20,70,40);comicWrite('COMIC',20,110,40);systemGlyph('ButtonA',50,60,70);painting(80,90,100,110);stampPainting('#j8t',200,300,1);discPhoto(0,0,1920,1080);blur(4);if(!postEffects(.4,.2,.1,99,-60,3))throw Error('post effects');const mh=meshUpload(new Float32Array([0,0,0,100,0,0,100,100,0,0,100,0]),new Float32Array([0,1,2,3,200,100,50,0,0,-1]),new Float32Array([0,0,0,100,0,0,4,255,0,0,-.004]));if(mh!==0)throw Error('mesh upload');const cam=new Float32Array([0,0,-500,1,0,0,0,1,0,0,0,1,960,540,1,1,0,8,0,0,1920,1080,-1.4,.000175,0,0,-1]);if(meshDraw(mh,cam,1)!==10)throw Error('mesh draw '+meshDraw(mh,cam,1));cam[2]=500;if(meshDraw(mh,cam,1)!==0)throw Error('mesh behind lens');cam[2]=-500;if(meshUpload(new Float32Array([0,0,0]),new Float32Array([0,0,0,9,1,1,1,0,0,1]))!==-1)throw Error('mesh index validation');if(!meshFree(mh)||meshDraw(mh,cam,1)!==0||meshFree(mh))throw Error('mesh free');if(capabilities().sceneApi!==2)throw Error('sceneApi')} function act(b){if(b==='A')synth(440,.01);if(b==='B'){oscillatorStop();if(!discCopy())throw Error('disc copy')}}", "test"}, {}, error); auto scene = engine.compile({"scene", "test", R"JS( function boot(){ disc3d(20,20,0,10,255,0,0);capsule3d(10,10,40,40,0,10,0,255,0); @@ -123,7 +123,7 @@ int main() { api.post_effects.tilt_px == 16.f && api.post_effects.motion_x == -48.f && api.post_effects.motion_y == 3.f); assert(graphics.color.r == 1 && graphics.color.g == 2 && graphics.color.b == 3); - assert(graphics.boxes == 1 && graphics.lines == 1 && graphics.triangles == 3 && graphics.textured == 1 && graphics.sprites == 1 && graphics.writes == 1 && + assert(graphics.boxes == 1 && graphics.lines == 1 && graphics.triangles == 13 /* 3 direct + a retained quad (2) and its edge capsule (8) */ && graphics.textured == 1 && graphics.sprites == 1 && graphics.writes == 1 && graphics.systemWrites == 3 && graphics.glyphs == 1 && graphics.images == 3 && graphics.blurs == 1 && graphics.lastImage.source == "disc-photo" && !graphics.lastImage.centered && telemetryCalls == 1 && gameSignalCalls == 1 &&