#include #define GLFW_INCLUDE_NONE #include #include "contracts.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { class GlfwSession { public: GlfwSession() { glfwSetErrorCallback([](int code,const char* message){ std::fprintf(stderr,"GLFW %d: %s\n",code,message?message:"unknown error"); }); if(!glfwInit()) throw std::runtime_error("GLFW initialization failed."); } ~GlfwSession(){glfwTerminate();} GlfwSession(const GlfwSession&)=delete; GlfwSession& operator=(const GlfwSession&)=delete; }; struct WindowDelete { void operator()(GLFWwindow* window)const noexcept {glfwDestroyWindow(window);} }; using Window=std::unique_ptr; struct BufferDelete {void operator()(GLuint name)const noexcept {glDeleteBuffers(1,&name);}}; struct VertexArrayDelete {void operator()(GLuint name)const noexcept {glDeleteVertexArrays(1,&name);}}; struct ShaderDelete {void operator()(GLuint name)const noexcept {glDeleteShader(name);}}; struct ProgramDelete {void operator()(GLuint name)const noexcept {glDeleteProgram(name);}}; // This move-only owner borrows a context. It neither moves the context between // threads nor makes queued GPU work complete. Its containing scope owns that proof. template class GlObject { public: GlObject(GLuint name,GLFWwindow* context):name_{name},context_{context}{ if(!name||!context||glfwGetCurrentContext()!=context) throw std::runtime_error("GL object creation requires its current context."); } ~GlObject(){reset();} GlObject(const GlObject&)=delete; GlObject& operator=(const GlObject&)=delete; GlObject(GlObject&& other)noexcept :name_{std::exchange(other.name_,0)},context_{other.context_}{} GlObject& operator=(GlObject&& other)noexcept { if(this!=&other){reset();name_=std::exchange(other.name_,0);context_=other.context_;} return *this; } [[nodiscard]] GLuint get()const noexcept{return name_;} private: void reset()noexcept { if(name_){ // A lifetime contract, asserted in Debug. Keep owners inside the // window scope and never switch away before their destruction. assert(glfwGetCurrentContext()==context_); Delete{}(name_);name_=0; } } GLuint name_{}; GLFWwindow* context_{}; }; using Shader=GlObject; using Program=GlObject; using Buffer=GlObject; using VertexArray=GlObject; static_assert(!std::is_copy_constructible_v); static_assert(std::is_nothrow_move_constructible_v); std::string read_shader(const std::filesystem::path& path){ std::ifstream input{path,std::ios::binary}; if(!input)throw std::runtime_error("Could not open shader: "+path.string()); std::array storage{}; input.read(storage.data(),static_cast(storage.size())); if(input.bad())throw std::runtime_error("Shader read failed."); const auto count=static_cast(input.gcount()); if(count==0||count>65536)throw std::runtime_error("Shader must contain 1–65536 bytes."); return {storage.data(),count}; } std::string shader_log(GLuint object,bool program){ std::array log{}; GLsizei count{}; if(program)glGetProgramInfoLog(object,static_cast(log.size()),&count,log.data()); else glGetShaderInfoLog(object,static_cast(log.size()),&count,log.data()); if(count<0||count>=static_cast(log.size()))return "No bounded compiler log available."; return {log.data(),static_cast(count)}; } Shader compile(GLenum kind,std::string_view source,GLFWwindow* context){ if(source.empty()||source.size()>65536)throw std::runtime_error("Unsupported shader source length."); Shader shader{glCreateShader(kind),context}; // Ownership precedes fallible compilation. const char* bytes=source.data(); const GLint length=static_cast(source.size()); glShaderSource(shader.get(),1,&bytes,&length); glCompileShader(shader.get()); GLint success{};glGetShaderiv(shader.get(),GL_COMPILE_STATUS,&success); if(success!=GL_TRUE)throw std::runtime_error(std::string{kind==GL_VERTEX_SHADER?"Vertex":"Fragment"}+ " shader compilation failed:\n"+shader_log(shader.get(),false)); return shader; } Program link(const Shader& vertex,const Shader& fragment,GLFWwindow* context){ Program program{glCreateProgram(),context}; glAttachShader(program.get(),vertex.get());glAttachShader(program.get(),fragment.get()); glLinkProgram(program.get()); GLint success{};glGetProgramiv(program.get(),GL_LINK_STATUS,&success); if(success!=GL_TRUE)throw std::runtime_error("Program linking failed:\n"+shader_log(program.get(),true)); glDetachShader(program.get(),vertex.get());glDetachShader(program.get(),fragment.get()); return program; } void check_gl(const char* operation){ if(const GLenum error=glGetError();error!=GL_NO_ERROR) throw std::runtime_error(std::string{operation}+" failed with GL error "+std::to_string(error)); } void capture(const std::filesystem::path& path,int width,int height){ const auto size=observatory::rgb_bytes(width,height); if(!size)throw std::runtime_error(std::string{size.error()}); std::vector pixels(*size); glPixelStorei(GL_PACK_ALIGNMENT,1); glReadBuffer(GL_BACK); glReadPixels(0,0,width,height,GL_RGB,GL_UNSIGNED_BYTE,pixels.data()); check_gl("Framebuffer capture"); std::ofstream output{path,std::ios::binary|std::ios::trunc}; if(!output)throw std::runtime_error("Could not open capture destination."); output.imbue(std::locale::classic()); output<<"P6\n"<(width)*3; // OpenGL reads bottom row first; PPM writes the top row first. for(int y=height-1;y>=0;--y) output.write(reinterpret_cast(pixels.data()+static_cast(y)*row_bytes), static_cast(row_bytes)); output.close(); if(!output)throw std::runtime_error("Capture write failed; output may be incomplete."); } struct Vertex {std::array position;std::array color;}; static_assert(std::is_standard_layout_v); constexpr std::array vertices{{ {{{-0.72F,-0.55F,0}},{{0.95F,0.24F,0.12F}}}, {{{ 0.72F,-0.55F,0}},{{0.12F,0.42F,0.92F}}}, {{{ 0.00F, 0.72F,0}},{{0.98F,0.72F,0.20F}}} }}; } // namespace int main(int argc,char* argv[]){ try { bool smoke=false,bad_shader=false,camera_mode=false,resize_smoke=false,reverse_draw=false; std::filesystem::path destination; std::optional check_stage; std::filesystem::path check_source; std::optional> check_program; for(int i=1;i(glewGetErrorString(status))); // GLEW can probe unsupported enums during core-context initialization. // Drain loader-only status before any application GL work begins. for(GLenum error=glGetError();error!=GL_NO_ERROR;error=glGetError()){ if(error!=GL_INVALID_ENUM)throw std::runtime_error("Unexpected GL status after loader initialization: "+std::to_string(error)); } if(!GLEW_VERSION_3_3)throw std::runtime_error("The loaded driver does not support OpenGL 3.3."); std::cout<<"GL_VENDOR: "<first),window.get()); const auto fragment=compile(GL_FRAGMENT_SHADER,read_shader(check_program->second),window.get()); const auto program=link(vertex,fragment,window.get()); std::cout<<"Shader program linked; no uniform updates or draw were tested.\n"; return 0; } if(check_stage){ const auto shader=compile(*check_stage,read_shader(check_source),window.get()); std::cout<<"Shader stage compiled; no program link or draw was tested.\n"; return 0; } auto vertex=compile(GL_VERTEX_SHADER,read_shader(std::filesystem::path{OBSERVATORY_SHADER_DIR}/(camera_mode?"camera.vert":"observatory.vert")),window.get()); const auto fragment_source=bad_shader?std::string{"#version 330 core\nnot valid GLSL"}: read_shader(std::filesystem::path{OBSERVATORY_SHADER_DIR}/"observatory.frag"); auto fragment=compile(GL_FRAGMENT_SHADER,fragment_source,window.get()); auto program=link(vertex,fragment,window.get()); GLuint vao_name{},buffer_name{};glGenVertexArrays(1,&vao_name); VertexArray vao{vao_name,window.get()}; glGenBuffers(1,&buffer_name);Buffer buffer{buffer_name,window.get()}; glBindVertexArray(vao.get());glBindBuffer(GL_ARRAY_BUFFER,buffer.get()); glBufferData(GL_ARRAY_BUFFER,static_cast(sizeof(vertices)),vertices.data(),GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,static_cast(sizeof(Vertex)), reinterpret_cast(offsetof(Vertex,position))); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,static_cast(sizeof(Vertex)), reinterpret_cast(offsetof(Vertex,color))); glEnableVertexAttribArray(0);glEnableVertexAttribArray(1); const GLint model_location=glGetUniformLocation(program.get(),"u_model"); if(model_location<0)throw std::runtime_error("Required u_model interface is absent."); const GLint view_location=camera_mode?glGetUniformLocation(program.get(),"u_view"):-1; const GLint projection_location=camera_mode?glGetUniformLocation(program.get(),"u_projection"):-1; if(camera_mode&&(view_location<0||projection_location<0)) throw std::runtime_error("Required camera uniform interface is absent."); if(camera_mode){ GLint depth_bits{}; glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER,GL_DEPTH,GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,&depth_bits); check_gl("Default framebuffer depth attachment query"); if(depth_bits<=0)throw std::runtime_error("Camera mode requires a depth buffer."); glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LESS); } check_gl("Geometry and interface setup"); float yaw=0,pitch=0,distance=4; double previous_time=glfwGetTime(),resize_deadline=0; bool resize_requested=false; int original_width{},original_height{}; if(camera_mode&&!smoke)std::cout<<"Arrow keys orbit; W/S zoom; R resets; Escape closes.\n"; do { glfwPollEvents(); const double now=glfwGetTime(); const float delta=static_cast(std::clamp(now-previous_time,0.0,0.05)); previous_time=now; if(glfwGetKey(window.get(),GLFW_KEY_ESCAPE)==GLFW_PRESS)glfwSetWindowShouldClose(window.get(),GLFW_TRUE); if(glfwWindowShouldClose(window.get()))break; int width{},height{};glfwGetFramebufferSize(window.get(),&width,&height); if(width<=0||height<=0){ if(smoke)throw std::runtime_error("No drawable framebuffer."); glfwWaitEvents();previous_time=glfwGetTime();continue; } if(resize_requested&&width==original_width&&height==original_height){ if(now>resize_deadline)throw std::runtime_error("Framebuffer resize did not arrive within two seconds."); glfwWaitEventsTimeout(0.01);continue; } // Viewport and projection use the same pixel extent, never the logical window size. glViewport(0,0,width,height); glClearColor(0.035F,0.055F,0.10F,1);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glUseProgram(program.get());glBindVertexArray(vao.get()); if(camera_mode){ if(!smoke&&glfwGetWindowAttrib(window.get(),GLFW_FOCUSED)==GLFW_TRUE){ const auto pressed=[&](int key){return glfwGetKey(window.get(),key)==GLFW_PRESS?1.0F:0.0F;}; yaw=std::remainder(yaw+(pressed(GLFW_KEY_RIGHT)-pressed(GLFW_KEY_LEFT))*delta,2*std::numbers::pi_v); pitch=std::clamp(pitch+(pressed(GLFW_KEY_UP)-pressed(GLFW_KEY_DOWN))*delta,-1.2F,1.2F); distance=std::clamp(distance+(pressed(GLFW_KEY_S)-pressed(GLFW_KEY_W))*delta*2,1.5F,10.0F); if(pressed(GLFW_KEY_R)>0){yaw=0;pitch=0;distance=4;} } const observatory::Vec3 eye{distance*std::sin(yaw)*std::cos(pitch),distance*std::sin(pitch), distance*std::cos(yaw)*std::cos(pitch)}; const auto view=observatory::look_at(eye,{0,0,0},{0,1,0}); const auto projection=observatory::perspective(50,static_cast(width)/static_cast(height),0.1F,30); if(!view||!projection)throw std::runtime_error("Camera parameters violated the supported bounds."); glUniformMatrix4fv(view_location,1,GL_FALSE,view->data.data()); glUniformMatrix4fv(projection_location,1,GL_FALSE,projection->data.data()); const std::array models{ observatory::multiply(observatory::translation({-0.45F,0,-0.5F}),observatory::rotate_z(-0.25F)), observatory::multiply(observatory::translation({0.45F,0,0}),observatory::rotate_z(0.2F)), observatory::translation({0,-0.25F,0.5F})}; for(std::size_t draw=0;draw(std::fmod(now*0.15,6.283185307))); glUniformMatrix4fv(model_location,1,GL_FALSE,model.data.data()); glDrawArrays(GL_TRIANGLES,0,3); } check_gl("Frame draw"); if(smoke)std::cout<<"Drawable framebuffer: "< viewport{};glGetIntegerv(GL_VIEWPORT,viewport.data()); if(viewport[2]!=width||viewport[3]!=height|| std::abs(static_cast(width)/static_cast(height)-4.0F/3)>0.01F) throw std::runtime_error("Resized viewport or framebuffer aspect did not match the checkpoint."); std::cout<<"Resize checkpoint passed: viewport and projection use the new extent.\n"; } if(!destination.empty())capture(destination,width,height); glfwSwapBuffers(window.get()); if(smoke)break; } while(!glfwWindowShouldClose(window.get())); } std::cout<<"All GL owners released while their context was current.\n"; return 0; } catch(const std::exception& error){std::cerr<