Skip to content

Commit 84f7d16

Browse files
committed
Add a workaround for poor NPOT mipmap implementations
We use the deepest mipmap level as the average of texture data in order to compute brightness of the scene in AtmosphereShowMySky. Some OpenGL implementations, however, appear to handle non-power-of-two textures poorly when computing mipmap levels. The result is that, when a texture dimension is close to a power of two but not exactly it, the whole mip map displays ugly aliasing, and the deepest 1×1 level is very far from the average of the texels of level 0. The effect can be seen when resizing the window on e.g. Mesa-based drivers on Linux (and, IIRC, Intel on Windows). As you resize the window, the daytime atmosphere will change its brightness erratically, which shouldn't really happen. To work around such implementations, we resize the base level texture to the closest smaller power-of-two texture, and compute the mip map from it instead of the initial texture. The resize introduces a small inaccuracy, but the result is still much higher quality than if we used glGenerateMipmap() on the initial NPOT texture. On good implementations such as NVIDIA GPU drivers, the workaround is disabled. The functionality of such averaging is implemented in a class TextureAverageComputer that's only made available on desktop OpenGL. On OpenGL ES 2 it's #ifdefed out.
1 parent b321deb commit 84f7d16

6 files changed

Lines changed: 313 additions & 28 deletions

File tree

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ SET(stellarium_lib_SRCS
126126
core/StelOpenGLArray.cpp
127127
core/StelHips.hpp
128128
core/StelHips.cpp
129+
core/TextureAverageComputer.hpp
130+
core/TextureAverageComputer.cpp
129131

130132
${spout_SRCS}
131133

src/StelMainView.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "StelOpenGL.hpp"
3434
#include "StelOpenGLArray.hpp"
3535
#include "StelProjector.hpp"
36+
#include "TextureAverageComputer.hpp"
3637

3738
#include <QDebug>
3839
#include <QDir>
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Stellarium
3+
* Copyright (C) 2023 Ruslan Kabatsayev
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU General Public License
7+
* as published by the Free Software Foundation; either version 2
8+
* of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18+
*/
19+
20+
#include "TextureAverageComputer.hpp"
21+
22+
#if !QT_CONFIG(opengles2) // This class uses glGetTexImage(), which is not supported in GLES2
23+
24+
#include <QOpenGLFunctions_3_3_Core>
25+
26+
namespace
27+
{
28+
29+
int roundDownToClosestPowerOfTwo(const int x)
30+
{
31+
if(x==0) return 1;
32+
int shift=0;
33+
for(auto v=x;v;v>>=1)
34+
++shift;
35+
return 1<<(shift-1);
36+
}
37+
38+
}
39+
40+
Vec4f TextureAverageComputer::getTextureAverageSimple(const GLuint texture, const int width, const int height)
41+
{
42+
// Get average value of the pixels as the value of the deepest mipmap level
43+
gl.glActiveTexture(GL_TEXTURE0);
44+
gl.glBindTexture(GL_TEXTURE_2D, texture);
45+
gl.glGenerateMipmap(GL_TEXTURE_2D);
46+
47+
using namespace std;
48+
// Formula from the glspec, "Mipmapping" subsection in section 3.8.11 Texture Minification
49+
const auto totalMipmapLevels = 1+floor(log2(max(width,height)));
50+
const auto deepestLevel=totalMipmapLevels-1;
51+
52+
#ifndef NDEBUG
53+
// Sanity check
54+
int deepestMipmapLevelWidth=-1, deepestMipmapLevelHeight=-1;
55+
gl.glGetTexLevelParameteriv(GL_TEXTURE_2D, deepestLevel, GL_TEXTURE_WIDTH, &deepestMipmapLevelWidth);
56+
gl.glGetTexLevelParameteriv(GL_TEXTURE_2D, deepestLevel, GL_TEXTURE_HEIGHT, &deepestMipmapLevelHeight);
57+
assert(deepestMipmapLevelWidth==1);
58+
assert(deepestMipmapLevelHeight==1);
59+
#endif
60+
61+
Vec4f pixel;
62+
gl.glGetTexImage(GL_TEXTURE_2D, deepestLevel, GL_RGBA, GL_FLOAT, &pixel[0]);
63+
return pixel;
64+
}
65+
66+
// Clobbers:
67+
// GL_VERTEX_ARRAY_BINDING, GL_ACTIVE_TEXTURE, GL_TEXTURE_BINDING_2D, GL_CURRENT_PROGRAM,
68+
// input texture's minification filter
69+
Vec4f TextureAverageComputer::getTextureAverageWithWorkaround(const GLuint texture)
70+
{
71+
// Play it safe: we don't want to make the GPU struggle with very large textures
72+
// if we happen to make them ~4 times larger. Instead round the dimensions down.
73+
const auto potWidth = roundDownToClosestPowerOfTwo(npotWidth);
74+
const auto potHeight = roundDownToClosestPowerOfTwo(npotHeight);
75+
76+
gl.glActiveTexture(GL_TEXTURE0);
77+
gl.glBindTexture(GL_TEXTURE_2D, texture);
78+
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
79+
80+
blitTexProgram->bind();
81+
82+
GLint oldViewport[4];
83+
gl.glGetIntegerv(GL_VIEWPORT, oldViewport);
84+
GLint oldFBO=-1;
85+
gl.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &oldFBO);
86+
87+
gl.glBindFramebuffer(GL_FRAMEBUFFER, potFBO);
88+
gl.glViewport(0,0,potWidth,potHeight);
89+
90+
gl.glBindVertexArray(vao);
91+
gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
92+
gl.glBindVertexArray(0);
93+
94+
gl.glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
95+
gl.glBindFramebuffer(GL_FRAMEBUFFER, oldFBO);
96+
97+
blitTexProgram->release();
98+
99+
return getTextureAverageSimple(potTex, potWidth, potHeight);
100+
}
101+
102+
Vec4f TextureAverageComputer::getTextureAverage(const GLuint texture)
103+
{
104+
if(workaroundNeeded)
105+
return getTextureAverageWithWorkaround(texture);
106+
return getTextureAverageSimple(texture, npotWidth, npotHeight);
107+
}
108+
109+
void TextureAverageComputer::init()
110+
{
111+
GLuint texture = -1;
112+
gl.glGenTextures(1, &texture);
113+
assert(texture>0);
114+
gl.glActiveTexture(GL_TEXTURE0);
115+
gl.glBindTexture(GL_TEXTURE_2D, texture);
116+
117+
std::vector<Vec4f> data;
118+
for(int n=0; n<10; ++n)
119+
data.emplace_back(1,1,1,1);
120+
for(int n=0; n<10; ++n)
121+
data.emplace_back(1,1,1,0);
122+
for(int n=0; n<10; ++n)
123+
data.emplace_back(1,1,0,0);
124+
for(int n=0; n<10; ++n)
125+
data.emplace_back(1,0,0,0);
126+
127+
constexpr int width = 63;
128+
for(int n=data.size(); n<width; ++n)
129+
data.emplace_back(0,0,0,0);
130+
131+
gl.glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,data.size(),1,0,GL_RGBA,GL_FLOAT,&data[0][0]);
132+
const auto mipmapAverage = getTextureAverageSimple(texture, width, 1);
133+
134+
const auto sum = std::accumulate(data.begin(), data.end(), Vec4f(0,0,0,0));
135+
const auto trueAverage = sum / float(data.size());
136+
qDebug().nospace() << "Test texture true average: "
137+
<< trueAverage[0] << ", "
138+
<< trueAverage[1] << ", "
139+
<< trueAverage[2] << ", "
140+
<< trueAverage[3];
141+
qDebug().nospace() << "Test texture mipmap average: "
142+
<< mipmapAverage[0] << ", "
143+
<< mipmapAverage[1] << ", "
144+
<< mipmapAverage[2] << ", "
145+
<< mipmapAverage[3];
146+
147+
const auto diff = mipmapAverage - trueAverage;
148+
using std::abs;
149+
const auto maxDiff = std::max({abs(diff[0]),abs(diff[1]),abs(diff[2]),abs(diff[3])});
150+
workaroundNeeded = maxDiff >= 2./255.;
151+
152+
if(workaroundNeeded)
153+
{
154+
qDebug() << "Mipmap average is unusable, will resize textures to "
155+
"power-of-two size when average value is required.";
156+
}
157+
else
158+
{
159+
qDebug() << "Mipmap average works correctly";
160+
}
161+
162+
gl.glBindTexture(GL_TEXTURE_2D, 0);
163+
gl.glDeleteTextures(1, &texture);
164+
165+
inited = true;
166+
}
167+
168+
// Clobbers: GL_TEXTURE_BINDING_2D, GL_VERTEX_ARRAY_BINDING, GL_ARRAY_BUFFER_BINDING
169+
TextureAverageComputer::TextureAverageComputer(QOpenGLFunctions_3_3_Core& gl, const int texWidth, const int texHeight, const GLenum internalFormat)
170+
: gl(gl)
171+
, npotWidth(texWidth)
172+
, npotHeight(texHeight)
173+
{
174+
if(!inited) init();
175+
if(!workaroundNeeded) return;
176+
177+
GLint oldFBO=-1;
178+
gl.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &oldFBO);
179+
180+
gl.glGenFramebuffers(1, &potFBO);
181+
gl.glGenTextures(1, &potTex);
182+
gl.glBindTexture(GL_TEXTURE_2D, potTex);
183+
const auto potWidth = roundDownToClosestPowerOfTwo(npotWidth);
184+
const auto potHeight = roundDownToClosestPowerOfTwo(npotHeight);
185+
gl.glTexImage2D(GL_TEXTURE_2D,0,internalFormat,potWidth,potHeight,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr);
186+
gl.glBindTexture(GL_TEXTURE_2D,0);
187+
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER,potFBO);
188+
gl.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,potTex,0);
189+
[[maybe_unused]] const auto status=gl.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
190+
assert(status==GL_FRAMEBUFFER_COMPLETE);
191+
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);
192+
193+
gl.glGenVertexArrays(1, &vao);
194+
gl.glBindVertexArray(vao);
195+
gl.glGenBuffers(1, &vbo);
196+
gl.glBindBuffer(GL_ARRAY_BUFFER, vbo);
197+
const GLfloat vertices[]=
198+
{
199+
-1, -1,
200+
1, -1,
201+
-1, 1,
202+
1, 1,
203+
};
204+
gl.glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
205+
constexpr GLuint attribIndex=0;
206+
constexpr int coordsPerVertex=2;
207+
gl.glVertexAttribPointer(attribIndex, coordsPerVertex, GL_FLOAT, false, 0, 0);
208+
gl.glEnableVertexAttribArray(attribIndex);
209+
gl.glBindVertexArray(0);
210+
211+
blitTexProgram.reset(new QOpenGLShaderProgram);
212+
blitTexProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, 1+R"(
213+
#version 330
214+
layout(location=0) in vec4 vertex;
215+
out vec2 texcoord;
216+
void main()
217+
{
218+
gl_Position = vertex;
219+
texcoord = vertex.st*0.5+vec2(0.5);
220+
}
221+
)");
222+
blitTexProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, 1+R"(
223+
#version 330
224+
in vec2 texcoord;
225+
out vec4 color;
226+
uniform sampler2D tex;
227+
void main()
228+
{
229+
color = texture(tex, texcoord);
230+
}
231+
)");
232+
blitTexProgram->link();
233+
blitTexProgram->bind();
234+
blitTexProgram->setUniformValue("tex", 0);
235+
blitTexProgram->release();
236+
237+
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, oldFBO);
238+
}
239+
240+
TextureAverageComputer::~TextureAverageComputer()
241+
{
242+
gl.glDeleteTextures(1, &potTex);
243+
gl.glDeleteFramebuffers(1, &potFBO);
244+
gl.glDeleteVertexArrays(1, &vao);
245+
gl.glDeleteBuffers(1, &vbo);
246+
}
247+
248+
#endif
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Stellarium
3+
* Copyright (C) 2023 Ruslan Kabatsayev
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU General Public License
7+
* as published by the Free Software Foundation; either version 2
8+
* of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18+
*/
19+
20+
#ifndef INCLUDE_ONCE_D638566A_098E_4855_B0B5_C8BB5940DA10
21+
#define INCLUDE_ONCE_D638566A_098E_4855_B0B5_C8BB5940DA10
22+
23+
#include <string>
24+
#include <memory>
25+
#include "VecMath.hpp"
26+
#include <QOpenGLContext>
27+
#include <QOpenGLShaderProgram>
28+
29+
#if !QT_CONFIG(opengles2) // This class uses glGetTexImage(), which is not supported in GLES2
30+
31+
class QOpenGLFunctions_3_3_Core;
32+
class TextureAverageComputer
33+
{
34+
QOpenGLFunctions_3_3_Core& gl;
35+
std::unique_ptr<QOpenGLShaderProgram> blitTexProgram;
36+
GLuint potFBO = 0;
37+
GLuint potTex = 0;
38+
GLuint vbo = 0, vao = 0;
39+
GLint npotWidth, npotHeight;
40+
static inline bool inited = false;
41+
static inline bool workaroundNeeded = false;
42+
43+
void init();
44+
Vec4f getTextureAverageSimple(GLuint texture, int width, int height);
45+
Vec4f getTextureAverageWithWorkaround(GLuint texture);
46+
public:
47+
Vec4f getTextureAverage(GLuint texture);
48+
TextureAverageComputer(QOpenGLFunctions_3_3_Core&, int texW, int texH, GLenum internalFormat);
49+
~TextureAverageComputer();
50+
};
51+
52+
#endif
53+
54+
#endif

src/core/modules/AtmosphereShowMySky.cpp

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "StelPainter.hpp"
3131
#include "Dithering.hpp"
3232
#include "StelTranslator.hpp"
33+
#include "TextureAverageComputer.hpp"
3334

3435
#include <cassert>
3536
#include <cstring>
@@ -316,7 +317,10 @@ vec3 calcViewDir()
316317

317318
void AtmosphereShowMySky::resizeRenderTarget(int width, int height)
318319
{
319-
renderer_->resizeEvent(width/atmoRes, height/atmoRes);
320+
const int physWidth = width/atmoRes;
321+
const int physHeight = height/atmoRes;
322+
renderer_->resizeEvent(physWidth, physHeight);
323+
textureAverager_.reset(new TextureAverageComputer(*glfuncs(), physWidth, physHeight, GL_RGBA32F));
320324

321325
prevWidth_=width;
322326
prevHeight_=height;
@@ -697,33 +701,7 @@ void AtmosphereShowMySky::drawAtmosphere(Mat4f const& projectionMatrix, const fl
697701
Vec4f AtmosphereShowMySky::getMeanPixelValue()
698702
{
699703
StelOpenGL::checkGLErrors(__FILE__,__LINE__);
700-
auto& gl = *glfuncs();
701-
702-
GL(gl.glActiveTexture(GL_TEXTURE0));
703-
GL(gl.glBindTexture(GL_TEXTURE_2D, renderer_->getLuminanceTexture()));
704-
GL(gl.glGenerateMipmap(GL_TEXTURE_2D));
705-
706-
int texW=-1, texH=-1;
707-
GL(gl.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &texW));
708-
GL(gl.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &texH));
709-
710-
using namespace std;
711-
// Formula from the glspec, "Mipmapping" subsection in section 3.8.11 Texture Minification
712-
const auto totalMipmapLevels = 1+floor(log2(max(texW,texH)));
713-
const auto deepestLevel=totalMipmapLevels-1;
714-
715-
#ifndef NDEBUG
716-
// Sanity check
717-
int deepestMipmapLevelWidth=-1, deepestMipmapLevelHeight=-1;
718-
GL(gl.glGetTexLevelParameteriv(GL_TEXTURE_2D, deepestLevel, GL_TEXTURE_WIDTH, &deepestMipmapLevelWidth));
719-
GL(gl.glGetTexLevelParameteriv(GL_TEXTURE_2D, deepestLevel, GL_TEXTURE_HEIGHT, &deepestMipmapLevelHeight));
720-
assert(deepestMipmapLevelWidth==1);
721-
assert(deepestMipmapLevelHeight==1);
722-
#endif
723-
724-
Vec4f pixel;
725-
GL(gl.glGetTexImage(GL_TEXTURE_2D, deepestLevel, GL_RGBA, GL_FLOAT, &pixel[0]));
726-
return pixel;
704+
return textureAverager_->getTextureAverage(renderer_->getLuminanceTexture());
727705
}
728706

729707
bool AtmosphereShowMySky::dynamicResolution(StelProjectorP prj, Vec3d &currPos, int width, int height)

src/core/modules/AtmosphereShowMySky.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class StelProjector;
4141
class StelToneReproducer;
4242
class StelCore;
4343
class QOpenGLFunctions;
44+
class TextureAverageComputer;
4445

4546
class AtmosphereShowMySky : public Atmosphere, public QObject
4647
{
@@ -115,6 +116,7 @@ class AtmosphereShowMySky : public Atmosphere, public QObject
115116
} shaderAttribLocations;
116117

117118
StelTextureSP ditherPatternTex_;
119+
std::unique_ptr<TextureAverageComputer> textureAverager_;
118120

119121
float prevFad=0, prevFov=0;
120122
Vec3d prevPos=Vec3d(0,0,0), prevSun=Vec3d(0,0,0);

0 commit comments

Comments
 (0)