From 8d29c09512d2dae73d8998923312fd443913ac57 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Fri, 2 Oct 2026 01:27:18 +0200 Subject: [PATCH] research(intervals): OBJ export switch and a D3D11 fly-through viewer for the spike Viewer generates the world at startup with the spike's code (timing shown), free-fly camera, wireframe, near/far toggles, seed cycling. Benchmark hashes unchanged (e5ad62303de5f641). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- Saved/Research/intervals/interval_spike.cpp | 101 +++++++++++++++++- .../intervals/viewer/interval_viewer.cpp | 91 ++++++++++++++++ 2 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 Saved/Research/intervals/viewer/interval_viewer.cpp diff --git a/Saved/Research/intervals/interval_spike.cpp b/Saved/Research/intervals/interval_spike.cpp index 1d3ca80..f1cb70c 100644 --- a/Saved/Research/intervals/interval_spike.cpp +++ b/Saved/Research/intervals/interval_spike.cpp @@ -10,6 +10,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -1323,11 +1326,104 @@ void print_axis_stats(const char* name,const AxisStats& s) { s.runsP50,s.runsP95,s.runsMax); } +bool write_obj(const std::filesystem::path& path, const std::vector& vertices, + const std::vector& indices, const std::vector* pages = nullptr) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file) return false; + file << "# VoxelForge interval spike; Unreal centimetres, Z up, relative to spike world origin\n"; + std::vector remap; + std::vector used; + if (pages) { + remap.assign(vertices.size(), 0); + used.assign(vertices.size(), 0); + for (const PageRange& page : *pages) { + if (static_cast(page.indexStart) + page.indexCount > indices.size()) return false; + for (uint32_t i = 0; i < page.indexCount; ++i) { + const uint32_t index = indices[page.indexStart + i]; + if (index >= vertices.size()) return false; + used[index] = 1; + } + } + uint32_t next = 1; + for (size_t i = 0; i < vertices.size(); ++i) if (used[i]) remap[i] = next++; + } + file << std::fixed << std::setprecision(6); + for (size_t i = 0; i < vertices.size(); ++i) { + if (pages && !used[i]) continue; + file << "v " << vertices[i].x * 100.0f << ' ' << vertices[i].y * 100.0f << ' ' + << vertices[i].z * 100.0f << '\n'; + } + for (size_t i = 0; i < vertices.size(); ++i) { + if (pages && !used[i]) continue; + file << "vn " << vertices[i].nx << ' ' << vertices[i].ny << ' ' << vertices[i].nz << '\n'; + } + auto emit_face = [&](uint32_t a, uint32_t b, uint32_t c) { + if (pages) { a = remap[a]; b = remap[b]; c = remap[c]; } + file << "f " << a << "//" << a << ' ' << b << "//" << b << ' ' << c << "//" << c << '\n'; + }; + if (pages) { + for (const PageRange& page : *pages) + for (uint32_t i = 0; i < page.indexCount; i += 3) + emit_face(indices[page.indexStart + i], indices[page.indexStart + i + 1], indices[page.indexStart + i + 2]); + } else { + if (indices.size() % 3 != 0) return false; + for (size_t i = 0; i < indices.size(); i += 3) { + if (indices[i] >= vertices.size() || indices[i + 1] >= vertices.size() || indices[i + 2] >= vertices.size()) return false; + emit_face(indices[i] + 1, indices[i + 1] + 1, indices[i + 2] + 1); + } + } + file.flush(); + return file.good(); +} + +bool export_obj_files(const std::filesystem::path& directory, const MeshOutput& nearMesh, const FarOutput& farMesh) { + std::error_code ec; + std::filesystem::create_directories(directory, ec); + if (ec) { std::fprintf(stderr, "OBJ export: cannot create %s: %s\n", directory.string().c_str(), ec.message().c_str()); return false; } + std::vector nearVertices; + nearVertices.reserve(nearMesh.vertices.size()); + for (const TempVertex& v : nearMesh.vertices) { + Vertex local = v.v; + local.x -= kWorldOriginCm[0] * 0.01f; + local.y -= kWorldOriginCm[1] * 0.01f; + local.z -= kWorldOriginCm[2] * 0.01f; + nearVertices.push_back(local); + } + if (!write_obj(directory / "near.obj", nearVertices, nearMesh.indices)) { + std::fprintf(stderr, "OBJ export: failed writing %s\n", (directory / "near.obj").string().c_str()); return false; + } + std::vector farVertices = farMesh.vertices; + for (Vertex& v : farVertices) { + v.x -= kWorldOriginCm[0] * 0.01f; + v.y -= kWorldOriginCm[1] * 0.01f; + v.z -= kWorldOriginCm[2] * 0.01f; + } + if (!write_obj(directory / "far.obj", farVertices, farMesh.indices)) { + std::fprintf(stderr, "OBJ export: failed writing %s\n", (directory / "far.obj").string().c_str()); return false; + } + for (uint32_t pageId = 0; pageId < static_cast(kPagesPerAxis*kPagesPerAxis*kPagesPerAxis); ++pageId) { + std::vector ranges; + for (const PageRange& range : nearMesh.pages) if (range.page == pageId) ranges.push_back(range); + if (std::none_of(ranges.begin(), ranges.end(), [](const PageRange& range) { return range.indexCount != 0; })) continue; + char filename[32]; std::snprintf(filename, sizeof(filename), "near_page_%02u.obj", pageId); + if (!write_obj(directory / filename, nearVertices, nearMesh.indices, &ranges)) { + std::fprintf(stderr, "OBJ export: failed writing %s\n", (directory / filename).string().c_str()); return false; + } + } + return true; +} + } // namespace int main(int argc,char** argv) { - const uint64_t seed=argc>1?std::strtoull(argv[1],nullptr,10):1; - const int workers=argc>2?std::clamp(std::atoi(argv[2]),1,8):1; + uint64_t seed=1; int workers=1; std::filesystem::path exportDirectory; bool exportObj=false; + int positional=0; + for (int i=1;i(totalBytes),static_cast(combinedHash), static_cast(process_working_set()),validationPass?"true":"false"); + if (exportObj && !export_obj_files(exportDirectory, mesh, farOutput)) return 3; return validationPass?0:2; } diff --git a/Saved/Research/intervals/viewer/interval_viewer.cpp b/Saved/Research/intervals/viewer/interval_viewer.cpp new file mode 100644 index 0000000..a8e76f2 --- /dev/null +++ b/Saved/Research/intervals/viewer/interval_viewer.cpp @@ -0,0 +1,91 @@ +#define main interval_spike_benchmark_main +#include "../interval_spike.cpp" +#undef main + +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "d3d11.lib") +#pragma comment(lib, "dxgi.lib") +#pragma comment(lib, "d3dcompiler.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "user32.lib") + +namespace { +struct V3 { float x,y,z; }; +V3 add(V3 a,V3 b){return {a.x+b.x,a.y+b.y,a.z+b.z};} +V3 sub(V3 a,V3 b){return {a.x-b.x,a.y-b.y,a.z-b.z};} +V3 mul(V3 a,float s){return {a.x*s,a.y*s,a.z*s};} +float dot(V3 a,V3 b){return a.x*b.x+a.y*b.y+a.z*b.z;} +V3 cross(V3 a,V3 b){return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};} +V3 norm(V3 a){float n=std::sqrt(std::max(1e-20f,dot(a,a)));return mul(a,1.0f/n);} +struct M4 { float m[4][4]{}; }; +M4 identity(){M4 a{};for(int i=0;i<4;++i)a.m[i][i]=1;return a;} +M4 perspective(float fov,float aspect,float zn,float zf){M4 a{};float y=1/std::tan(fov*.5f);a.m[0][0]=y/aspect;a.m[1][1]=y;a.m[2][2]=zf/(zf-zn);a.m[2][3]=1;a.m[3][2]=-zn*zf/(zf-zn);return a;} +M4 view_matrix(V3 p,V3 f,V3 r,V3 u){M4 a=identity();a.m[0][0]=r.x;a.m[1][0]=r.y;a.m[2][0]=r.z;a.m[3][0]=-dot(r,p);a.m[0][1]=u.x;a.m[1][1]=u.y;a.m[2][1]=u.z;a.m[3][1]=-dot(u,p);a.m[0][2]=f.x;a.m[1][2]=f.y;a.m[2][2]=f.z;a.m[3][2]=-dot(f,p);return a;} +M4 transpose(M4 a){M4 b{};for(int i=0;i<4;++i)for(int j=0;j<4;++j)b.m[i][j]=a.m[j][i];return b;} +struct GPUVertex { float p[3],n[3]; }; +struct CB { M4 viewProj; float camera[3],pad; float light[3],fogStart; float fogEnd,ambient,pad2[2]; }; +struct App { + HWND hwnd{}; ID3D11Device* dev{}; ID3D11DeviceContext* ctx{}; IDXGISwapChain* swap{}; ID3D11RenderTargetView* rtv{}; ID3D11DepthStencilView* dsv{}; + ID3D11VertexShader* vs{}; ID3D11PixelShader* ps{}; ID3D11InputLayout* layout{}; ID3D11Buffer* cb{}; ID3D11RasterizerState* solid{}; ID3D11RasterizerState* wire{}; + ID3D11Buffer* nearVB{}; ID3D11Buffer* nearIB{}; ID3D11Buffer* farVB{}; ID3D11Buffer* farIB{}; UINT nearIndices{},farIndices{}; + uint64_t seed=1;int workers=6;double generationMs{};bool showNear=true,showFar=true,wireframe=false,captured=false;float yaw=0,pitch=0,speed=8;V3 pos{};POINT last{};int width=1280,height=800;Clock::time_point prev=Clock::now();double frameMs=0;bool keys[256]{}; + void release_mesh(){if(nearVB)nearVB->Release();if(nearIB)nearIB->Release();if(farVB)farVB->Release();if(farIB)farIB->Release();nearVB=nearIB=farVB=farIB=nullptr;nearIndices=farIndices=0;} + ~App(){release_mesh();if(solid)solid->Release();if(wire)wire->Release();if(cb)cb->Release();if(layout)layout->Release();if(vs)vs->Release();if(ps)ps->Release();if(rtv)rtv->Release();if(dsv)dsv->Release();if(swap)swap->Release();if(ctx)ctx->Release();if(dev)dev->Release();} + bool make_buffer(ID3D11Buffer** out,const void* data,size_t bytes,UINT bind){D3D11_BUFFER_DESC d{};d.ByteWidth=static_cast(bytes);d.Usage=D3D11_USAGE_DEFAULT;d.BindFlags=bind;D3D11_SUBRESOURCE_DATA s{};s.pSysMem=data;return SUCCEEDED(dev->CreateBuffer(&d,&s,out));} + bool generate(){ + auto begin=Clock::now(); Corpus corpus;build_corpus(corpus,seed);AxisData axes[3];AxisStats stats[3];bool overflow=false; + for(int a=0;a<3;++a)axes[a]=solve_axis(corpus,a,workers,stats[a],overflow); + std::vector active;active_cell_masks(axes[2],active);MeshOutput nearMesh=emit_near_mesh(corpus,axes,axes[2],active,workers);FarOutput farMesh=build_far_world(seed); + generationMs=ms(begin,Clock::now());release_mesh(); + std::vector nv;nv.reserve(nearMesh.vertices.size());for(const auto& v:nearMesh.vertices)nv.push_back({{v.v.x,v.v.y,v.v.z},{v.v.nx,v.v.ny,v.v.nz}}); + std::vector fv;fv.reserve(farMesh.vertices.size());for(const auto& v:farMesh.vertices)fv.push_back({{v.x,v.y,v.z},{v.nx,v.ny,v.nz}}); + if(!nv.empty()&&(!make_buffer(&nearVB,nv.data(),nv.size()*sizeof(GPUVertex),D3D11_BIND_VERTEX_BUFFER)||!make_buffer(&nearIB,nearMesh.indices.data(),nearMesh.indices.size()*sizeof(uint32_t),D3D11_BIND_INDEX_BUFFER)))return false; + if(!fv.empty()&&(!make_buffer(&farVB,fv.data(),fv.size()*sizeof(GPUVertex),D3D11_BIND_VERTEX_BUFFER)||!make_buffer(&farIB,farMesh.indices.data(),farMesh.indices.size()*sizeof(uint32_t),D3D11_BIND_INDEX_BUFFER)))return false; + nearIndices=static_cast(nearMesh.indices.size());farIndices=static_cast(farMesh.indices.size()); + // The first generated room is centred near local (-13, -13, -8.5) metres. + // These offsets match its deterministic seed-1 center and keep the camera in its air volume. + Rng rng(seed ^ 0x0A57A20261002ull);int lx=-1300+rng.range(-170,170),ly=-1300+rng.range(-170,170),lz=-850+rng.range(-120,120); + pos={kWorldOriginCm[0]*.01f+lx*.01f,kWorldOriginCm[1]*.01f+ly*.01f,kWorldOriginCm[2]*.01f+lz*.01f};yaw=0;pitch=0; + return true; + } + bool init_device(){ + DXGI_SWAP_CHAIN_DESC sd{};sd.BufferCount=1;sd.BufferDesc.Format=DXGI_FORMAT_R8G8B8A8_UNORM;sd.BufferUsage=DXGI_USAGE_RENDER_TARGET_OUTPUT;sd.OutputWindow=hwnd;sd.SampleDesc.Count=1;sd.Windowed=TRUE;sd.SwapEffect=DXGI_SWAP_EFFECT_DISCARD; + UINT flags=0; +#if defined(_DEBUG) + flags|=D3D11_CREATE_DEVICE_DEBUG; +#endif + D3D_FEATURE_LEVEL fl{};if(FAILED(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,flags,nullptr,0,D3D11_SDK_VERSION,&sd,&swap,&dev,&fl,&ctx)))return false; + const char* shader="cbuffer C:register(b0){float4x4 VP;float3 Camera;float Pad;float3 LightDir;float FogStart;float FogEnd;float Ambient;float2 Pad2;} struct I{float3 p:POSITION;float3 n:NORMAL;};struct O{float4 p:SV_POSITION;float3 world:TEXCOORD0;float3 n:NORMAL;};O VS(I i){O o;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=i.n;return o;}float4 PS(O i):SV_TARGET{float3 N=normalize(i.n);float3 L=normalize(-LightDir);float diffuse=saturate(dot(N,L))*0.72;float3 toCam=Camera-i.world;float dist=length(toCam);float head=saturate(dot(N,normalize(toCam)))*0.85/(1+dist*0.018);float light=Ambient+diffuse+head;float3 rock=float3(0.55,0.57,0.60)*light;float fog=saturate((dist-FogStart)/(FogEnd-FogStart));return float4(lerp(rock,float3(0.075,0.095,0.12),fog),1);} "; + ID3DBlob *vb=nullptr,*pb=nullptr,*err=nullptr;if(FAILED(D3DCompile(shader,strlen(shader),nullptr,nullptr,nullptr,"VS","vs_4_0",0,0,&vb,&err)))return false;if(FAILED(D3DCompile(shader,strlen(shader),nullptr,nullptr,nullptr,"PS","ps_4_0",0,0,&pb,&err))){if(vb)vb->Release();return false;} + HRESULT hr=dev->CreateVertexShader(vb->GetBufferPointer(),vb->GetBufferSize(),nullptr,&vs);if(SUCCEEDED(hr))hr=dev->CreatePixelShader(pb->GetBufferPointer(),pb->GetBufferSize(),nullptr,&ps); + D3D11_INPUT_ELEMENT_DESC il[]={{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},{"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0}};if(SUCCEEDED(hr))hr=dev->CreateInputLayout(il,2,vb->GetBufferPointer(),vb->GetBufferSize(),&layout);vb->Release();pb->Release();if(FAILED(hr))return false; + D3D11_BUFFER_DESC cbd{};cbd.ByteWidth=sizeof(CB);cbd.Usage=D3D11_USAGE_DEFAULT;cbd.BindFlags=D3D11_BIND_CONSTANT_BUFFER;if(FAILED(dev->CreateBuffer(&cbd,nullptr,&cb)))return false; + D3D11_RASTERIZER_DESC rd{};rd.FillMode=D3D11_FILL_SOLID;rd.CullMode=D3D11_CULL_NONE;rd.DepthClipEnable=TRUE;if(FAILED(dev->CreateRasterizerState(&rd,&solid)))return false;rd.FillMode=D3D11_FILL_WIREFRAME;if(FAILED(dev->CreateRasterizerState(&rd,&wire)))return false; + return resize(width,height); + } + bool resize(int w,int h){if(!dev||w<1||h<1)return true;width=w;height=h;if(rtv){rtv->Release();rtv=nullptr;}if(dsv){dsv->Release();dsv=nullptr;}if(FAILED(swap->ResizeBuffers(0,w,h,DXGI_FORMAT_UNKNOWN,0)))return false;ID3D11Texture2D* back{};if(FAILED(swap->GetBuffer(0,__uuidof(ID3D11Texture2D),reinterpret_cast(&back))))return false;HRESULT hr=dev->CreateRenderTargetView(back,nullptr,&rtv);back->Release();if(FAILED(hr))return false;D3D11_TEXTURE2D_DESC dd{};dd.Width=w;dd.Height=h;dd.MipLevels=1;dd.ArraySize=1;dd.Format=DXGI_FORMAT_D24_UNORM_S8_UINT;dd.SampleDesc.Count=1;dd.BindFlags=D3D11_BIND_DEPTH_STENCIL;ID3D11Texture2D* depth{};if(FAILED(dev->CreateTexture2D(&dd,nullptr,&depth)))return false;hr=dev->CreateDepthStencilView(depth,nullptr,&dsv);depth->Release();return SUCCEEDED(hr);} + V3 forward(){return norm({std::sin(yaw)*std::cos(pitch),std::cos(yaw)*std::cos(pitch),std::sin(pitch)});} + void title(){char b[256];snprintf(b,sizeof(b),"Interval spike | %.2f ms/frame | %u tris | (%.1f, %.1f, %.1f) m | gen %.1f ms | seed %llu",frameMs,((showNear?nearIndices:0)+(showFar?farIndices:0))/3,pos.x,pos.y,pos.z,generationMs,(unsigned long long)seed);SetWindowTextA(hwnd,b);} + void draw(){auto now=Clock::now();float dt=std::clamp(std::chrono::duration(now-prev).count(),0.0f,.1f);prev=now;frameMs=dt*1000; + V3 f=forward(),r=norm(cross(f,{0,0,1})),u=norm(cross(r,f));float step=speed*dt*(keys[VK_SHIFT]?4.0f:1.0f);if(keys['W'])pos=add(pos,mul(f,step));if(keys['S'])pos=sub(pos,mul(f,step));if(keys['A'])pos=sub(pos,mul(r,step));if(keys['D'])pos=add(pos,mul(r,step));if(keys[VK_SPACE]||keys['E'])pos.z+=step;if(keys[VK_CONTROL]||keys['Q'])pos.z-=step; + float clear[]={.075f,.095f,.12f,1};ctx->OMSetRenderTargets(1,&rtv,dsv);ctx->ClearRenderTargetView(rtv,clear);ctx->ClearDepthStencilView(dsv,D3D11_CLEAR_DEPTH,1,0);D3D11_VIEWPORT vp{0,0,(float)width,(float)height,0,1};ctx->RSSetViewports(1,&vp);ctx->RSSetState(wireframe?wire:solid);ctx->IASetInputLayout(layout);ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + CB c{};M4 v=view_matrix(pos,f,r,u);M4 p=perspective(1.0472f,(float)width/height,.05f,20000); // compose view and projection for row-vector shader + M4 out{};for(int i=0;i<4;++i)for(int j=0;j<4;++j)for(int k=0;k<4;++k)out.m[i][j]+=v.m[i][k]*p.m[k][j];c.viewProj=transpose(out);c.camera[0]=pos.x;c.camera[1]=pos.y;c.camera[2]=pos.z;c.light[0]=-.45f;c.light[1]=-.8f;c.light[2]=-.35f;c.fogStart=80;c.fogEnd=1200;c.ambient=.28f;ctx->UpdateSubresource(cb,0,nullptr,&c,0,0);ctx->VSSetConstantBuffers(0,1,&cb);ctx->PSSetConstantBuffers(0,1,&cb);ctx->VSSetShader(vs,nullptr,0);ctx->PSSetShader(ps,nullptr,0); + auto mesh=[&](ID3D11Buffer* vb,ID3D11Buffer* ib,UINT count){if(!vb||!ib||!count)return;UINT stride=sizeof(GPUVertex),offset=0;ctx->IASetVertexBuffers(0,1,&vb,&stride,&offset);ctx->IASetIndexBuffer(ib,DXGI_FORMAT_R32_UINT,0);ctx->DrawIndexed(count,0,0);};if(showNear)mesh(nearVB,nearIB,nearIndices);if(showFar)mesh(farVB,farIB,farIndices);swap->Present(1,0);title();} + void capture(bool on){captured=on;if(on){SetCapture(hwnd);ShowCursor(FALSE);GetCursorPos(&last);POINT c{width/2,height/2};ClientToScreen(hwnd,&c);SetCursorPos(c.x,c.y);}else{ReleaseCapture();ShowCursor(TRUE);}} +}; +App* g{}; +LRESULT CALLBACK wndproc(HWND h,UINT m,WPARAM w,LPARAM l){if(!g)return DefWindowProc(h,m,w,l);switch(m){case WM_SIZE:if(w!=SIZE_MINIMIZED)g->resize(LOWORD(l),HIWORD(l));return 0;case WM_KEYDOWN:if(w<256)g->keys[w]=true;if(w==VK_ESCAPE){if(g->captured)g->capture(false);else DestroyWindow(h);}if(w=='F')g->wireframe=!g->wireframe;if(w=='N'||w=='P'){g->seed+=(w=='N'?1:static_cast(-1));if(!g->generate())MessageBoxA(h,"Could not create mesh buffers.","Viewer error",MB_ICONERROR);}if(w=='M')g->showNear=!g->showNear;if(w=='B')g->showFar=!g->showFar;return 0;case WM_KEYUP:if(w<256)g->keys[w]=false;return 0;case WM_RBUTTONDOWN:g->capture(true);return 0;case WM_RBUTTONUP:g->capture(false);return 0;case WM_MOUSEMOVE:if(g->captured){POINT c{g->width/2,g->height/2};ClientToScreen(h,&c);POINT p{GET_X_LPARAM(l),GET_Y_LPARAM(l)};POINT sc=p;ClientToScreen(h,&sc);int dx=sc.x-c.x,dy=sc.y-c.y;if(dx||dy){g->yaw+=dx*.0035f;g->pitch=std::clamp(g->pitch-dy*.0035f,-1.55f,1.55f);SetCursorPos(c.x,c.y);}}return 0;case WM_MOUSEWHEEL:g->speed=std::clamp(g->speed*(GET_WHEEL_DELTA_WPARAM(w)>0?1.25f:.8f),.25f,1000.0f);return 0;case WM_DESTROY:PostQuitMessage(0);return 0;}return DefWindowProc(h,m,w,l);} +} + +int WINAPI WinMain(HINSTANCE instance,HINSTANCE,LPSTR,int){ + int argc=0;LPWSTR* argv=CommandLineToArgvW(GetCommandLineW(),&argc);uint64_t seed=1;int workers=6,positional=0;if(argv){for(int i=1;i