#include "glcache.h" #include "gles.h" #include "quad.h" #include "rend/tileclip.h" #include "rend/osd.h" #include "naomi2.h" #include "rend/transform_matrix.h" #ifdef LIBRETRO #include "postprocess.h" #include "vmu_xhair.h" #endif #include /* Drawing and related state management Takes vertex, textures and renders to the currently set up target */ const static u32 CullModes[] = { GL_NONE, //0 No culling No culling GL_NONE, //1 Cull if Small Cull if ( |det| < fpu_cull_val ) GL_FRONT, //2 Cull if Negative Cull if ( |det| < 0 ) or ( |det| < fpu_cull_val ) GL_BACK, //3 Cull if Positive Cull if ( |det| > 0 ) or ( |det| < fpu_cull_val ) }; const u32 Zfunction[] = { GL_NEVER, //0 Never GL_LESS, //1 Less GL_EQUAL, //2 Equal GL_LEQUAL, //3 Less Or Equal GL_GREATER, //4 Greater GL_NOTEQUAL, //5 Not Equal GL_GEQUAL, //6 Greater Or Equal GL_ALWAYS, //7 Always }; /* 0 Zero (0, 0, 0, 0) 1 One (1, 1, 1, 1) 2 Other Color (OR, OG, OB, OA) 3 Inverse Other Color (1-OR, 1-OG, 1-OB, 1-OA) 4 SRC Alpha (SA, SA, SA, SA) 5 Inverse SRC Alpha (1-SA, 1-SA, 1-SA, 1-SA) 6 DST Alpha (DA, DA, DA, DA) 7 Inverse DST Alpha (1-DA, 1-DA, 1-DA, 1-DA) */ const u32 DstBlendGL[] = { GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA }; const u32 SrcBlendGL[] = { GL_ZERO, GL_ONE, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA }; PipelineShader* CurrentShader; void SetCull(u32 CullMode) { if (CullModes[CullMode] == GL_NONE) glcache.Disable(GL_CULL_FACE); else { glcache.Enable(GL_CULL_FACE); glcache.CullFace(CullModes[CullMode]); //GL_FRONT/GL_BACK, ... } } static void SetTextureRepeatMode(GLuint dir, u32 clamp, u32 mirror) { if (clamp) glcache.TexParameteri(GL_TEXTURE_2D, dir, GL_CLAMP_TO_EDGE); else glcache.TexParameteri(GL_TEXTURE_2D, dir, mirror ? GL_MIRRORED_REPEAT : GL_REPEAT); } static void SetBaseClipping() { if (ShaderUniforms.base_clipping.enabled) { glcache.Enable(GL_SCISSOR_TEST); glcache.Scissor(ShaderUniforms.base_clipping.x, ShaderUniforms.base_clipping.y, ShaderUniforms.base_clipping.width, ShaderUniforms.base_clipping.height); } else glcache.Disable(GL_SCISSOR_TEST); } static TileClipping setTileClip(u32 tileclip, int clip_rect[4]) { TileClipping clipmode = GetTileClip(tileclip, ViewportMatrix, clip_rect); if (clipmode == TileClipping::Outside) { glcache.Enable(GL_SCISSOR_TEST); glcache.Scissor(clip_rect[0], clip_rect[1], clip_rect[2], clip_rect[3]); } else { SetBaseClipping(); } return clipmode; } template void SetGPState(const PolyParam* gp,u32 cflip=0) { float trilinear_alpha; if (gp->pcw.Texture && gp->tsp.FilterMode > 1 && Type != ListType_Punch_Through && gp->tcw.MipMapped == 1) { trilinear_alpha = 0.25f * (gp->tsp.MipMapD & 0x3); if (gp->tsp.FilterMode == 2) // Trilinear pass A trilinear_alpha = 1.f - trilinear_alpha; } else trilinear_alpha = 1.f; bool color_clamp = gp->tsp.ColorClamp && (pvrrc.fog_clamp_min.full != 0 || pvrrc.fog_clamp_max.full != 0xffffffff); int fog_ctrl = config::Fog ? gp->tsp.FogCtrl : 2; int clip_rect[4] = {}; TileClipping clipmode = setTileClip(gp->tileclip, clip_rect); TextureCacheData *texture = (TextureCacheData *)gp->texture; int gpuPalette = texture == nullptr || !texture->gpuPalette ? 0 : gp->tsp.FilterMode + 1; if (gpuPalette != 0) { if (config::TextureFiltering == 1) gpuPalette = 1; // force nearest else if (config::TextureFiltering == 2) gpuPalette = 2; // force linear } CurrentShader = GetProgram(Type == ListType_Punch_Through ? true : false, clipmode == TileClipping::Inside, gp->pcw.Texture, gp->tsp.UseAlpha, gp->tsp.IgnoreTexA, gp->tsp.ShadInstr, gp->pcw.Offset, fog_ctrl, gp->pcw.Gouraud, gp->tcw.PixelFmt == PixelBumpMap, color_clamp, trilinear_alpha != 1.f, gpuPalette, gp->isNaomi2(), ShaderUniforms.dithering); glcache.UseProgram(CurrentShader->program); if (CurrentShader->trilinear_alpha != -1) glUniform1f(CurrentShader->trilinear_alpha, trilinear_alpha); if (gpuPalette != 0) { int paletteIndex; if (gp->tcw.PixelFmt == PixelPal4) paletteIndex = gp->tcw.PalSelect << 4; else paletteIndex = (gp->tcw.PalSelect >> 4) << 8; glUniform1i(CurrentShader->palette_index, paletteIndex); if (gpuPalette == 2 && CurrentShader->texSize != -1) { float texSize[] { (float)texture->width, (float)texture->height }; glUniform2fv(CurrentShader->texSize, 1, texSize); } } if (clipmode == TileClipping::Inside) glUniform4f(CurrentShader->pp_ClipTest, (float)clip_rect[0], (float)clip_rect[1], (float)(clip_rect[0] + clip_rect[2]), (float)(clip_rect[1] + clip_rect[3])); if (config::ModifierVolumes) { //This bit control which pixels are affected //by modvols const u32 stencil = gp->pcw.Shadow != 0 ? 0x80 : 0; glcache.StencilFunc(GL_ALWAYS, stencil, stencil); } if (texture != nullptr) { glcache.BindTexture(GL_TEXTURE_2D, texture->texID); SetTextureRepeatMode(GL_TEXTURE_WRAP_S, gp->tsp.ClampU, gp->tsp.FlipU); SetTextureRepeatMode(GL_TEXTURE_WRAP_T, gp->tsp.ClampV, gp->tsp.FlipV); bool nearest_filter; if (gpuPalette != 0) nearest_filter = true; else if (config::TextureFiltering == 0) nearest_filter = gp->tsp.FilterMode == 0; else if (config::TextureFiltering == 1) nearest_filter = true; else nearest_filter = false; bool mipmapped = texture->IsMipmapped(); //set texture filter mode if (nearest_filter) { //nearest-neighbor filtering glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); } else { //bilinear filtering //PowerVR supports also trilinear via two passes, but we ignore that for now glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmapped && Type != ListType_Punch_Through ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } #ifdef GL_TEXTURE_LOD_BIAS if (!gl.is_gles && gl.gl_major >= 3 && mipmapped) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, D_Adjust_LoD_Bias[gp->tsp.MipMapD]); #endif if (gl.max_anisotropy > 1.f) { if (config::AnisotropicFiltering > 1 && !nearest_filter) { glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, std::min(config::AnisotropicFiltering, gl.max_anisotropy)); // Set the recommended minification filter for best results if (mipmapped && Type != ListType_Punch_Through) glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); } else glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 1.f); } } // Apparently punch-through polys support blending, or at least some combinations // Opaque polygons support blending in list continuations (wild guess) glcache.Enable(GL_BLEND); glcache.BlendFunc(SrcBlendGL[gp->tsp.SrcInstr],DstBlendGL[gp->tsp.DstInstr]); //set cull mode ! //cflip is required when exploding triangles for triangle sorting SetCull(gp->isp.CullMode ^ cflip ^ 1); //set Z mode, only if required if (Type == ListType_Punch_Through || (Type == ListType_Translucent && SortingEnabled)) { glcache.DepthFunc(Zfunction[6]); // >= } else { glcache.DepthFunc(Zfunction[gp->isp.DepthMode]); } if (SortingEnabled /* && !config::PerStripSorting */) // Looks glitchy too but less missing graphics (but wrong depth order...) glcache.DepthMask(GL_FALSE); else { // Z Write Disable seems to be ignored for punch-through. // Fixes Worms World Party, Bust-a-Move 4 and Re-Volt if (Type == ListType_Punch_Through) glcache.DepthMask(GL_TRUE); else glcache.DepthMask(!gp->isp.ZWriteDis); } if (CurrentShader->naomi2) setN2Uniforms(gp, CurrentShader, pvrrc); } template void DrawList(const std::vector& gply, int first, int count) { if (count == 0) return; const PolyParam* params = &gply[first]; glcache.Enable(GL_STENCIL_TEST); glcache.StencilFunc(GL_ALWAYS,0,0); glcache.StencilOp(GL_KEEP,GL_KEEP,GL_REPLACE); for (; count > 0; count--, params++) { if (params->count < 3) continue; if ((Type == ListType_Opaque || (Type == ListType_Translucent && !SortingEnabled)) && params->isp.DepthMode == 0) // depthFunc = never continue; SetGPState(params); glDrawElements(GL_TRIANGLE_STRIP, params->count, gl.index_type, (GLvoid*)(gl.get_index_size() * params->first)); glCheck(); } } static void drawSorted(int first, int count, bool multipass) { if (count == 0) return; glcache.Enable(GL_STENCIL_TEST); glcache.StencilFunc(GL_ALWAYS,0,0); glcache.StencilOp(GL_KEEP,GL_KEEP,GL_REPLACE); int end = first + count; for (int p = first; p < end; p++) { const PolyParam* params = &pvrrc.global_param_tr[pvrrc.sortedTriangles[p].polyIndex]; SetGPState(params); glDrawElements(GL_TRIANGLES, pvrrc.sortedTriangles[p].count, gl.index_type, (GLvoid*)(gl.get_index_size() * pvrrc.sortedTriangles[p].first)); } if (multipass && config::TranslucentPolygonDepthMask) { // Write to the depth buffer now. The next render pass might need it. (Cosmic Smash) glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glcache.Disable(GL_BLEND); glcache.StencilMask(0); // We use the modifier volumes shader because it's fast. We don't need textures, etc. glcache.UseProgram(gl.modvol_shader.program); glUniform1f(gl.modvol_shader.sp_ShaderColor, 1.f); glcache.DepthFunc(GL_GEQUAL); glcache.DepthMask(GL_TRUE); for (int p = first; p < end; p++) { const PolyParam* params = &pvrrc.global_param_tr[pvrrc.sortedTriangles[p].polyIndex]; if (!params->isp.ZWriteDis) { // FIXME no clipping in modvol shader //SetTileClip(gp->tileclip,true); SetCull(params->isp.CullMode ^ 1); glDrawElements(GL_TRIANGLES, pvrrc.sortedTriangles[p].count, gl.index_type, (GLvoid*)(gl.get_index_size() * pvrrc.sortedTriangles[p].first)); } } glcache.StencilMask(0xFF); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); } } //All pixels are in area 0 by default. //If inside an 'in' volume, they are in area 1 //if inside an 'out' volume, they are in area 0 /* Stencil bits: bit 7: mv affected (must be preserved) bit 1: current volume state but 0: summary result (starts off as 0) Lower 2 bits: IN volume (logical OR): 00 -> 00 01 -> 01 10 -> 01 11 -> 01 Out volume (logical AND): 00 -> 00 01 -> 00 10 -> 00 11 -> 01 */ void SetMVS_Mode(ModifierVolumeMode mv_mode, ISP_Modvol ispc) { if (mv_mode == Xor) { // set states glcache.Enable(GL_DEPTH_TEST); // write only bit 1 glcache.StencilMask(2); // no stencil testing glcache.StencilFunc(GL_ALWAYS, 0, 2); // count the number of pixels in front of the Z buffer (xor zpass) glcache.StencilOp(GL_KEEP, GL_KEEP, GL_INVERT); // Cull mode needs to be set SetCull(ispc.CullMode); } else if (mv_mode == Or) { // set states glcache.Enable(GL_DEPTH_TEST); // write only bit 1 glcache.StencilMask(2); // no stencil testing glcache.StencilFunc(GL_ALWAYS, 2, 2); // Or'ing of all triangles glcache.StencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // Cull mode needs to be set SetCull(ispc.CullMode); } else { // Inclusion or Exclusion volume // no depth test glcache.Disable(GL_DEPTH_TEST); // write bits 1:0 glcache.StencilMask(3); if (mv_mode == Inclusion) { // Inclusion volume //res : old : final //0 : 0 : 00 //0 : 1 : 01 //1 : 0 : 01 //1 : 1 : 01 // if (1<=st) st=1; else st=0; glcache.StencilFunc(GL_LEQUAL,1,3); glcache.StencilOp(GL_ZERO, GL_ZERO, GL_REPLACE); } else { // Exclusion volume /* I've only seen a single game use it, so i guess it doesn't matter ? (Zombie revenge) (actually, i think there was also another, racing game) */ // The initial value for exclusion volumes is 1 so we need to invert the result before and'ing. //res : old : final //0 : 0 : 00 //0 : 1 : 01 //1 : 0 : 00 //1 : 1 : 00 // if (1 == st) st = 1; else st = 0; glcache.StencilFunc(GL_EQUAL, 1, 3); glcache.StencilOp(GL_ZERO, GL_ZERO, GL_KEEP); } } } void MainVertexArray::defineVtxAttribs() { //setup vertex buffers attrib pointers glEnableVertexAttribArray(VERTEX_POS_ARRAY); glVertexAttribPointer(VERTEX_POS_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex,x)); glEnableVertexAttribArray(VERTEX_COL_BASE_ARRAY); glVertexAttribPointer(VERTEX_COL_BASE_ARRAY, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex,col)); glEnableVertexAttribArray(VERTEX_COL_OFFS_ARRAY); glVertexAttribPointer(VERTEX_COL_OFFS_ARRAY, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex,spc)); glEnableVertexAttribArray(VERTEX_UV_ARRAY); glVertexAttribPointer(VERTEX_UV_ARRAY, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex,u)); glEnableVertexAttribArray(VERTEX_NORM_ARRAY); glVertexAttribPointer(VERTEX_NORM_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, nx)); } void SetupMainVBO() { gl.vbo.mainVAO.bind(gl.vbo.geometry.get(), gl.vbo.idxs.get()); glCheck(); } void ModvolVertexArray::defineVtxAttribs() { glEnableVertexAttribArray(VERTEX_POS_ARRAY); glVertexAttribPointer(VERTEX_POS_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, (void*)0); glDisableVertexAttribArray(VERTEX_UV_ARRAY); glDisableVertexAttribArray(VERTEX_COL_OFFS_ARRAY); glDisableVertexAttribArray(VERTEX_COL_BASE_ARRAY); } static void SetupModvolVBO() { gl.vbo.modvolVAO.bind(gl.vbo.modvols.get()); } void DrawModVols(int first, int count) { if (count == 0 || pvrrc.modtrig.empty()) return; SetupModvolVBO(); glcache.Disable(GL_BLEND); SetBaseClipping(); glcache.Enable(GL_DEPTH_TEST); glcache.DepthMask(GL_FALSE); glcache.DepthFunc(GL_GREATER); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); ModifierVolumeParam* params = &pvrrc.global_param_mvo[first]; int mod_base = -1; int curMVMat = -1; int curProjMat = -1; for (int cmv = 0; cmv < count; cmv++) { ModifierVolumeParam& param = params[cmv]; if (param.count == 0) continue; if (param.isNaomi2()) { glcache.UseProgram(gl.n2ModVolShader.program); if (param.mvMatrix != curMVMat) { curMVMat = param.mvMatrix; glUniformMatrix4fv(gl.n2ModVolShader.mvMat, 1, GL_FALSE, pvrrc.matrices[curMVMat].mat); } if (param.projMatrix != curProjMat) { curProjMat = param.projMatrix; glUniformMatrix4fv(gl.n2ModVolShader.projMat, 1, GL_FALSE, pvrrc.matrices[curProjMat].mat); } } else { glcache.UseProgram(gl.modvol_shader.program); } int clip_rect[4]; setTileClip(param.tileclip, clip_rect); // TODO inside clipping u32 mv_mode = param.isp.DepthMode; if (mod_base == -1) mod_base = param.first; if (!param.isp.VolumeLast && mv_mode > 0) SetMVS_Mode(Or, param.isp); // OR'ing (open volume or quad) else SetMVS_Mode(Xor, param.isp); // XOR'ing (closed volume) glDrawArrays(GL_TRIANGLES, param.first * 3, param.count * 3); if (mv_mode == 1 || mv_mode == 2) { // Sum the area SetMVS_Mode(mv_mode == 1 ? Inclusion : Exclusion, param.isp); glDrawArrays(GL_TRIANGLES, mod_base * 3, (param.first + param.count - mod_base) * 3); mod_base = -1; } } //disable culling SetCull(0); //enable color writes glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); SetBaseClipping(); //black out any stencil with '1' glcache.Enable(GL_BLEND); glcache.BlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glcache.Enable(GL_STENCIL_TEST); glcache.StencilFunc(GL_EQUAL,0x81,0x81); //only pixels that are Modvol enabled, and in area 1 //clear the stencil result bit glcache.StencilMask(0x3); //write to lsb glcache.StencilOp(GL_ZERO,GL_ZERO,GL_ZERO); //don't do depth testing glcache.Disable(GL_DEPTH_TEST); SetupMainVBO(); glDrawArrays(GL_TRIANGLE_STRIP,0,4); //restore states glcache.Enable(GL_DEPTH_TEST); } void DrawStrips() { SetupMainVBO(); //Draw the strips ! //We use sampler 0 glActiveTexture(GL_TEXTURE0); RenderPass previous_pass = {}; for (int render_pass = 0; render_pass < (int)pvrrc.render_passes.size(); render_pass++) { const RenderPass& current_pass = pvrrc.render_passes[render_pass]; DEBUG_LOG(RENDERER, "Render pass %d OP %d PT %d TR %d MV %d", render_pass + 1, current_pass.op_count - previous_pass.op_count, current_pass.pt_count - previous_pass.pt_count, current_pass.tr_count - previous_pass.tr_count, current_pass.mvo_count - previous_pass.mvo_count); //initial state glcache.Enable(GL_DEPTH_TEST); glcache.DepthMask(GL_TRUE); //Opaque DrawList(pvrrc.global_param_op, previous_pass.op_count, current_pass.op_count - previous_pass.op_count); //Alpha tested DrawList(pvrrc.global_param_pt, previous_pass.pt_count, current_pass.pt_count - previous_pass.pt_count); // Modifier volumes if (config::ModifierVolumes) DrawModVols(previous_pass.mvo_count, current_pass.mvo_count - previous_pass.mvo_count); //Alpha blended { if (current_pass.autosort) { if (!config::PerStripSorting) drawSorted(previous_pass.sorted_tr_count, current_pass.sorted_tr_count - previous_pass.sorted_tr_count, render_pass < (int)pvrrc.render_passes.size() - 1); else DrawList(pvrrc.global_param_tr, previous_pass.tr_count, current_pass.tr_count - previous_pass.tr_count); } else DrawList(pvrrc.global_param_tr, previous_pass.tr_count, current_pass.tr_count - previous_pass.tr_count); } previous_pass = current_pass; } } void OpenGLRenderer::RenderFramebuffer(const FramebufferInfo& info) { glReadFramebuffer(info); saveCurrentFramebuffer(); initVideoRoutingFrameBuffer(); getVideoShift(gl.ofbo.shiftX, gl.ofbo.shiftY); #ifdef LIBRETRO glBindFramebuffer(GL_FRAMEBUFFER, postProcessor.getFramebuffer(gl.dcfb.width, gl.dcfb.height)); glcache.BindTexture(GL_TEXTURE_2D, gl.dcfb.tex); #else if (gl.ofbo2.framebuffer != nullptr && (gl.dcfb.width != gl.ofbo2.framebuffer->getWidth() || gl.dcfb.height != gl.ofbo2.framebuffer->getHeight())) gl.ofbo2.framebuffer.reset(); if (gl.ofbo2.framebuffer == nullptr) gl.ofbo2.framebuffer = std::make_unique(gl.dcfb.width, gl.dcfb.height, false, true); else gl.ofbo2.framebuffer->bind(); glCheck(); gl.ofbo2.ready = true; #endif this->width = gl.dcfb.width; this->height = gl.dcfb.height; gl.ofbo.aspectRatio = getDCFramebufferAspectRatio(); glViewport(0, 0, gl.dcfb.width, gl.dcfb.height); glcache.Disable(GL_SCISSOR_TEST); if (info.fb_r_ctrl.fb_enable == 0 || info.vo_control.blank_video == 1) { // Video output disabled glcache.ClearColor(info.vo_border_col.red(), info.vo_border_col.green(), info.vo_border_col.blue(), 1.f); glClear(GL_COLOR_BUFFER_BIT); } else { glcache.Disable(GL_BLEND); gl.quad->draw(gl.dcfb.tex, false, false); } #ifdef LIBRETRO postProcessor.render(glsm_get_current_framebuffer()); #else renderLastFrame(); #endif drawOSD(); frameRendered = true; clearLastFrame = false; renderVideoRouting(); restoreCurrentFramebuffer(); } void writeFramebufferToVRAM() { u32 width = (pvrrc.ta_GLOB_TILE_CLIP.tile_x_num + 1) * 32; u32 height = (pvrrc.ta_GLOB_TILE_CLIP.tile_y_num + 1) * 32; float xscale = pvrrc.scaler_ctl.hscale == 1 ? 0.5f : 1.f; float yscale = 1024.f / pvrrc.scaler_ctl.vscalefactor; if (std::abs(yscale - 1.f) < 0.01) yscale = 1.f; FB_X_CLIP_type xClip = pvrrc.fb_X_CLIP; FB_Y_CLIP_type yClip = pvrrc.fb_Y_CLIP; if (xscale != 1.f || yscale != 1.f) { u32 scaledW = width * xscale; u32 scaledH = height * yscale; if (gl.fbscaling.framebuffer != nullptr && (gl.fbscaling.framebuffer->getWidth() != (int)scaledW || gl.fbscaling.framebuffer->getHeight() != (int)scaledH)) gl.fbscaling.framebuffer.reset(); if (gl.fbscaling.framebuffer == nullptr) gl.fbscaling.framebuffer = std::make_unique(scaledW, scaledH); if (gl.bogusBlitFramebuffer) { gl.fbscaling.framebuffer->bind(); glViewport(0, 0, scaledW, scaledH); glcache.Disable(GL_SCISSOR_TEST); glcache.ClearColor(0.f, 0.f, 0.f, 0.f); glClear(GL_COLOR_BUFFER_BIT); glcache.BindTexture(GL_TEXTURE_2D, gl.ofbo.framebuffer->getTexture()); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glcache.Disable(GL_BLEND); gl.quad->draw(gl.ofbo.framebuffer->getTexture(), false); } else { #ifndef GLES2 gl.ofbo.framebuffer->bind(GL_READ_FRAMEBUFFER); gl.fbscaling.framebuffer->bind(GL_DRAW_FRAMEBUFFER); glcache.Disable(GL_SCISSOR_TEST); glBlitFramebuffer(0, 0, width, height, 0, 0, scaledW, scaledH, GL_COLOR_BUFFER_BIT, GL_LINEAR); gl.fbscaling.framebuffer->bind(); #endif } width = scaledW; height = scaledH; // FB_Y_CLIP is applied before vscalefactor if > 1, so it must be scaled here if (yscale > 1) { yClip.min = std::round(yClip.min * yscale); yClip.max = std::round(yClip.max * yscale); } } u32 tex_addr = pvrrc.fb_W_SOF1 & VRAM_MASK; // TODO SCALER_CTL.interlace, SCALER_CTL.fieldselect glPixelStorei(GL_PACK_ALIGNMENT, 1); u32 linestride = pvrrc.fb_W_LINESTRIDE * 8; PixelBuffer tmp_buf; tmp_buf.init(width, height); u8 *p = (u8 *)tmp_buf.data(); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, p); xClip.min = std::min(xClip.min, width - 1); xClip.max = std::min(xClip.max, width - 1); yClip.min = std::min(yClip.min, height - 1); yClip.max = std::min(yClip.max, height - 1); WriteFramebuffer(width, height, p, tex_addr, pvrrc.fb_W_CTRL, linestride, xClip, yClip); glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); glCheck(); } bool OpenGLRenderer::renderLastFrame() { GlFramebuffer *framebuffer = gl.ofbo2.ready ? gl.ofbo2.framebuffer.get() : gl.ofbo.framebuffer.get(); if (framebuffer == nullptr) return false; int dx = 0; int dy = 0; glcache.Disable(GL_SCISSOR_TEST); getWindowboxDimensions(settings.display.width, settings.display.height, gl.ofbo.aspectRatio, dx, dy, config::Rotate90); if (gl.bogusBlitFramebuffer || config::Rotate90) { glViewport(dx, dy, settings.display.width - dx * 2, settings.display.height - dy * 2); glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); glcache.ClearColor(VO_BORDER_COL.red(), VO_BORDER_COL.green(), VO_BORDER_COL.blue(), 1.f); glClear(GL_COLOR_BUFFER_BIT); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, config::LinearInterpolation ? GL_LINEAR : GL_NEAREST); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, config::LinearInterpolation ? GL_LINEAR : GL_NEAREST); float *vertices = nullptr; if (gl.ofbo.shiftX != 0 || gl.ofbo.shiftY != 0) { static float sverts[20] = { -1.f, -1.f, 1.f, 0.f, 1.f, -1.f, 1.f, 1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, }; sverts[0] = sverts[5] = -1.f + gl.ofbo.shiftX * 2.f / framebuffer->getWidth(); sverts[10] = sverts[15] = sverts[0] + 2; sverts[1] = sverts[11] = -1.f - gl.ofbo.shiftY * 2.f / framebuffer->getHeight(); sverts[6] = sverts[16] = sverts[1] + 2; vertices = sverts; } glcache.Disable(GL_BLEND); gl.quad->draw(framebuffer->getTexture(), config::Rotate90, true, vertices); } else { #ifndef GLES2 framebuffer->bind(GL_READ_FRAMEBUFFER); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gl.ofbo.origFbo); glcache.ClearColor(VO_BORDER_COL.red(), VO_BORDER_COL.green(), VO_BORDER_COL.blue(), 1.f); glClear(GL_COLOR_BUFFER_BIT); glBlitFramebuffer(-gl.ofbo.shiftX, -gl.ofbo.shiftY, framebuffer->getWidth() - gl.ofbo.shiftX, framebuffer->getHeight() - gl.ofbo.shiftY, dx, settings.display.height - dy, settings.display.width - dx, dy, GL_COLOR_BUFFER_BIT, config::LinearInterpolation ? GL_LINEAR : GL_NEAREST); glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); #endif } return true; } bool OpenGLRenderer::GetLastFrame(std::vector& data, int& width, int& height) { GlFramebuffer *framebuffer = gl.ofbo2.ready ? gl.ofbo2.framebuffer.get() : gl.ofbo.framebuffer.get(); if (framebuffer == nullptr) return false; if (width != 0) { height = width / gl.ofbo.aspectRatio; } else if (height != 0) { width = gl.ofbo.aspectRatio * height; } else { width = framebuffer->getWidth(); height = framebuffer->getHeight(); if (config::Rotate90) std::swap(width, height); // We need square pixels for PNG int w = gl.ofbo.aspectRatio * height; if (width > w) height = width / gl.ofbo.aspectRatio; else width = w; } GlFramebuffer dstFramebuffer(width, height, false, false); glViewport(0, 0, width, height); glcache.Disable(GL_BLEND); verify(framebuffer->getTexture() != 0); const float *vertices = nullptr; if (config::Rotate90) { static float rvertices[4][5] = { { -1.f, 1.f, 1.f, 1.f, 0.f }, { -1.f, -1.f, 1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f, 0.f, 0.f }, { 1.f, -1.f, 1.f, 0.f, 1.f }, }; vertices = &rvertices[0][0]; } gl.quad->draw(framebuffer->getTexture(), config::Rotate90, false, vertices); data.resize(width * height * 3); glPixelStorei(GL_PACK_ALIGNMENT, 1); if (gl.is_gles) { // GL_RGB not supported std::vector tmp(width * height * 4); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, tmp.data()); u8 *dst = data.data(); const u8 *src = tmp.data(); while (src <= &tmp.back()) { *dst++ = *src++; *dst++ = *src++; *dst++ = *src++; src++; } } else { glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, data.data()); } restoreCurrentFramebuffer(); glCheck(); return true; } static GLuint vmuTextureId[8] {}; static GLuint lightgunTextureId {}; static u64 vmuLastUpdated[8] {}; static void updateVmuTexture(int vmuIndex) { if (vmuTextureId[vmuIndex] == 0) { vmuTextureId[vmuIndex] = glcache.GenTexture(); glcache.BindTexture(GL_TEXTURE_2D, vmuTextureId[vmuIndex]); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); } else glcache.BindTexture(GL_TEXTURE_2D, vmuTextureId[vmuIndex]); const u32 *data = vmu_lcd_data[vmuIndex]; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 48, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); vmuLastUpdated[vmuIndex] = vmuLastChanged[vmuIndex]; } static void drawVmuTexture(u8 vmuIndex, int width, int height) { const float *color = nullptr; #ifndef LIBRETRO const float vmu_padding = 8.f * settings.display.uiScale; const float w = 96.f * settings.display.uiScale; const float h = 64.f * settings.display.uiScale; float x; float y; if (vmuIndex & 2) x = width - vmu_padding - w; else x = vmu_padding; if (vmuIndex & 4) { y = height - vmu_padding - h; if (vmuIndex & 1) y -= vmu_padding + h; } else { y = vmu_padding; if (vmuIndex & 1) y += vmu_padding + h; } const float blend_factor[4] = { 0.75f, 0.75f, 0.75f, 0.75f }; color = blend_factor; #else if (vmuIndex & 1) return; const float vmu_padding_x = 8.f * width / 640.f * 4.f / 3.f / gl.ofbo.aspectRatio; const float vmu_padding_y = 8.f * height / 480.f; const float w = (float)VMU_SCREEN_WIDTH * width / 640.f * 4.f / 3.f / gl.ofbo.aspectRatio * vmu_screen_params[vmuIndex / 2].vmu_screen_size_mult; const float h = (float)VMU_SCREEN_HEIGHT * height / 480.f * vmu_screen_params[vmuIndex / 2].vmu_screen_size_mult; float x; float y; switch (vmu_screen_params[vmuIndex / 2].vmu_screen_position) { case UPPER_LEFT: default: x = vmu_padding_x; y = vmu_padding_y; break; case UPPER_RIGHT: x = width - vmu_padding_x - w; y = vmu_padding_y; break; case LOWER_LEFT: x = vmu_padding_x; y = height - vmu_padding_y - h; break; case LOWER_RIGHT: x = width - vmu_padding_x - w; y = height - vmu_padding_y - h; break; } #endif if (vmuLastChanged[vmuIndex] != vmuLastUpdated[vmuIndex] || vmuTextureId[vmuIndex] == 0) updateVmuTexture(vmuIndex); float x1 = (x + w) * 2 / width - 1; float y1 = -(y + h) * 2 / height + 1; x = x * 2 / width - 1; y = -y * 2 / height + 1; float vertices[20] = { x, y1, 1.f, 0.f, 0.f, x, y, 1.f, 0.f, 1.f, x1, y1, 1.f, 1.f, 0.f, x1, y, 1.f, 1.f, 1.f, }; glcache.Enable(GL_BLEND); glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); gl.quad->draw(vmuTextureId[vmuIndex], false, false, vertices, color); } static void updateLightGunTexture() { if (lightgunTextureId == 0) { lightgunTextureId = glcache.GenTexture(); glcache.BindTexture(GL_TEXTURE_2D, lightgunTextureId); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, getCrosshairTextureData()); } } static void drawGunCrosshair(u8 port, int width, int height) { auto [x, y] = getCrosshairPosition(port); #ifdef LIBRETRO float halfWidth = lightgun_crosshair_size / 2.f / config::ScreenStretching * 100.f * config::RenderResolution / 480.f; float halfHeight = lightgun_crosshair_size / 2.f * config::RenderResolution / 480.f; x /= config::ScreenStretching / 100.f; #else float halfWidth = config::CrosshairSize * settings.display.uiScale / 2.f; float halfHeight = halfWidth; #endif updateLightGunTexture(); float x1 = (x + halfWidth) * 2 / width - 1; float y1 = -(y + halfHeight) * 2 / height + 1; x = (x - halfWidth) * 2 / width - 1; y = -(y - halfHeight) * 2 / height + 1; float vertices[20] = { x, y1, 1.f, 0.f, 0.f, x, y, 1.f, 0.f, 1.f, x1, y1, 1.f, 1.f, 0.f, x1, y, 1.f, 1.f, 1.f, }; const float color[4] = { (config::CrosshairColor[port] & 0xff) / 255.f, ((config::CrosshairColor[port] >> 8) & 0xff) / 255.f, ((config::CrosshairColor[port] >> 16) & 0xff) / 255.f, ((config::CrosshairColor[port] >> 24) & 0xff) / 255.f }; glcache.Enable(GL_BLEND); glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); gl.quad->draw(lightgunTextureId, false, false, vertices, color); } void drawVmusAndCrosshairs(int width, int height) { #ifndef LIBRETRO width = settings.display.width; height = settings.display.height; glViewport(0, 0, width, height); glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); const bool showVmus = config::FloatVMUs; #else const bool showVmus = true; #endif if (settings.platform.isConsole() && showVmus) { for (int i = 0; i < 8 ; i++) if (vmu_lcd_status[i]) drawVmuTexture(i, width, height); } for (int i = 0 ; i < 4 ; i++) if (crosshairNeeded(i)) drawGunCrosshair(i, width, height); glCheck(); } void termVmuLightgun() { glcache.DeleteTextures(std::size(vmuTextureId), vmuTextureId); memset(vmuTextureId, 0, sizeof(vmuTextureId)); glcache.DeleteTextures(1, &lightgunTextureId); lightgunTextureId = 0; }