-
Notifications
You must be signed in to change notification settings - Fork 478
feat: add CGEHistogramFilter — luminance histogram overlay via GLES 3.1 compute shaders #567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,210 @@ | ||||||
| /* | ||||||
| * cgeHistogramFilter.cpp | ||||||
| * | ||||||
| * Luminance histogram overlay rendered entirely on the GPU using three | ||||||
| * GLES 3.1 compute-shader passes: | ||||||
| * | ||||||
| * Pass 1 (clear) — zero the 256×1 r32ui atomic-counter texture | ||||||
| * Pass 2 (count) — imageAtomicAdd per pixel into the counter texture | ||||||
| * Pass 3 (draw) — write bar-chart pixels into the 256×256 display texture | ||||||
| * | ||||||
| * The display texture is then blended onto the frame at the configured | ||||||
| * position / size, exactly like CGEWaveformFilter. | ||||||
| * | ||||||
| * Requirements: OpenGL ES 3.1 (compute shaders + image load/store). | ||||||
| */ | ||||||
|
|
||||||
| #include "cgeHistogramFilter.h" | ||||||
|
|
||||||
| #define USING_ALPHA 1 /// 80 % semi-transparent blend, same as WaveformFilter | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // Pass 1 — clear the 256 histogram bins | ||||||
| // Dispatched as glDispatchCompute(256, 1, 1) | ||||||
| // --------------------------------------------------------------------------- | ||||||
| static CGEConstString s_cshHistClear = "#version 310 es\n" CGE_SHADER_STRING( | ||||||
| precision highp float; | ||||||
| precision highp int; | ||||||
| layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; | ||||||
| layout(r32ui, binding = 2) writeonly uniform highp uimage2D histogramImage; | ||||||
|
|
||||||
| void main() { | ||||||
| imageStore(histogramImage, ivec2(int(gl_GlobalInvocationID.x), 0), uvec4(0u, 0u, 0u, 0u)); | ||||||
| }); | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // Pass 2 — accumulate luminance histogram via atomic add | ||||||
| // Dispatched as glDispatchCompute(frameWidth, frameHeight, 1) | ||||||
| // --------------------------------------------------------------------------- | ||||||
| static CGEConstString s_cshHistCount = "#version 310 es\n" CGE_SHADER_STRING( | ||||||
| precision highp float; | ||||||
| precision highp int; | ||||||
| layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; | ||||||
| layout(rgba8ui, binding = 0) uniform readonly highp uimage2D inputImageTexture; | ||||||
| // coherent volatile restrict are required for imageAtomicAdd | ||||||
| layout(r32ui, binding = 2) coherent volatile restrict uniform highp uimage2D histogramImage; | ||||||
|
|
||||||
| void main() { | ||||||
| ivec2 texCoord = ivec2(gl_GlobalInvocationID.xy); | ||||||
| uvec3 color = imageLoad(inputImageTexture, texCoord).rgb; | ||||||
|
|
||||||
| // BT.601 luminance (integer approximation kept in float for clarity) | ||||||
| float lum = dot(vec3(color.rgb), vec3(0.299, 0.587, 0.114)); | ||||||
| uint bin = clamp(uint(lum), 0u, 255u); | ||||||
| imageAtomicAdd(histogramImage, ivec2(bin, 0), 1u); | ||||||
| }); | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // Pass 3 — render bar chart into the 256×256 display texture | ||||||
| // Dispatched as glDispatchCompute(256, 256, 1) | ||||||
| // | ||||||
| // u_scaleInv = 256.0 / (frameWidth * frameHeight) | ||||||
| // → a bin containing 1/256 of all pixels exactly fills the bar height | ||||||
| // --------------------------------------------------------------------------- | ||||||
| static CGEConstString s_cshHistDraw = "#version 310 es\n" CGE_SHADER_STRING( | ||||||
| precision highp float; | ||||||
| precision highp int; | ||||||
| layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; | ||||||
| layout(r32ui, binding = 2) readonly uniform highp uimage2D histogramImage; | ||||||
| layout(rgba8ui, binding = 1) writeonly uniform highp uimage2D displayImage; | ||||||
|
|
||||||
| uniform float u_scaleInv; // 256.0 / (frameWidth * frameHeight) | ||||||
|
|
||||||
| void main() { | ||||||
| ivec2 coord = ivec2(gl_GlobalInvocationID.xy); | ||||||
| uint count = imageLoad(histogramImage, ivec2(coord.x, 0)).r; | ||||||
|
|
||||||
| // Normalise: full height when the bin contains 1/256 of total pixels | ||||||
| uint barHeight = uint(clamp(float(count) * u_scaleInv, 0.0, 1.0) * 256.0); | ||||||
|
|
||||||
| // coord.y == 0 is the bottom of the bar (low luminance side after flip) | ||||||
| if (uint(coord.y) < barHeight) { | ||||||
| imageStore(displayImage, coord, uvec4(255u, 255u, 255u, 255u)); | ||||||
| } else { | ||||||
| imageStore(displayImage, coord, uvec4(0u, 0u, 0u, 255u)); | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| // =========================================================================== | ||||||
|
|
||||||
| namespace CGE | ||||||
| { | ||||||
| CGEHistogramFilter::~CGEHistogramFilter() | ||||||
| { | ||||||
| if (m_histogramTex != 0) | ||||||
| { | ||||||
| glDeleteTextures(1, &m_histogramTex); | ||||||
| m_histogramTex = 0; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| bool CGEHistogramFilter::init() | ||||||
| { | ||||||
| if (!m_clearProgram.initWithComputeShader(s_cshHistClear) || | ||||||
| !m_countProgram.initWithComputeShader(s_cshHistCount) || | ||||||
| !m_drawProgram.initWithComputeShader(s_cshHistDraw)) | ||||||
| { | ||||||
| CGE_LOG_ERROR( | ||||||
| "CGEHistogramFilter::init failed.\n" | ||||||
| "This filter requires GLES 3.1+ (compute shaders + imageAtomicAdd).\n"); | ||||||
| CGE_LOG_ERROR(" clear shader : %s\n", s_cshHistClear); | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| setFormPosition(0.1f, 0.1f); | ||||||
| setFormSize(0.3f, 0.3f); | ||||||
|
|
||||||
| m_drawer.reset(TextureDrawer::create()); | ||||||
| m_drawer->setFlipScale(1.0f, -1.0f); // flip to match GL coordinate system | ||||||
|
|
||||||
| m_displayTexture = std::make_unique<TextureObject>(); | ||||||
| // Allocate the 256×256 display texture (RGBA8, same as waveform uses). | ||||||
| m_displayTexture->resize(256, 256); | ||||||
|
|
||||||
| // Create 256×1 r32ui texture for atomic luminance counters. | ||||||
| glGenTextures(1, &m_histogramTex); | ||||||
| glBindTexture(GL_TEXTURE_2D, m_histogramTex); | ||||||
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, 256, 1); | ||||||
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | ||||||
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | ||||||
| glBindTexture(GL_TEXTURE_2D, 0); | ||||||
|
|
||||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| void CGEHistogramFilter::render2Texture(CGEImageHandlerInterface* handler, | ||||||
| GLuint /*srcTexture*/, | ||||||
| GLuint /*vertexBufferID*/) | ||||||
| { | ||||||
| auto&& sz = handler->getOutputFBOSize(); | ||||||
|
|
||||||
| // scale factor: a uniform histogram bin (1/256 of pixels) → full bar height | ||||||
| const float scaleInv = 256.0f / static_cast<float>(sz.width * sz.height); | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Bind image units | ||||||
| // binding 0 : source frame (rgba8ui, read-only) | ||||||
| // binding 1 : display texture (rgba8ui, write-only) | ||||||
| // binding 2 : histogram bins (r32ui, read-write for atomic) | ||||||
| // ----------------------------------------------------------------------- | ||||||
| glBindImageTexture(0, handler->getTargetTextureID(), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8UI); | ||||||
| glBindImageTexture(1, m_displayTexture->texture(), 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8UI); | ||||||
| glBindImageTexture(2, m_histogramTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI); | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Pass 1 — clear bins | ||||||
| // ----------------------------------------------------------------------- | ||||||
| m_clearProgram.bind(); | ||||||
| glDispatchCompute(256, 1, 1); | ||||||
| glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Pass 2 — count luminance values | ||||||
| // ----------------------------------------------------------------------- | ||||||
| m_countProgram.bind(); | ||||||
| glDispatchCompute(sz.width, sz.height, 1); | ||||||
| glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Pass 3 — render bar chart | ||||||
| // ----------------------------------------------------------------------- | ||||||
| m_drawProgram.bind(); | ||||||
| m_drawProgram.sendUniformf("u_scaleInv", scaleInv); | ||||||
| glDispatchCompute(256, 256, 1); | ||||||
| glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n library/src/main/jni/cge/filters/cgeHistogramFilter.cpp | sed -n '160,210p'Repository: wysaid/android-gpuimage-plus Length of output: 2108 🏁 Script executed: grep -n "m_displayTexture" library/src/main/jni/cge/filters/cgeHistogramFilter.cpp | head -20Repository: wysaid/android-gpuimage-plus Length of output: 342 🏁 Script executed: grep -n "m_drawProgram" library/src/main/jni/cge/filters/cgeHistogramFilter.cpp | head -10Repository: wysaid/android-gpuimage-plus Length of output: 226 🏁 Script executed: grep -n "s_cshHistDraw\|initWithComputeShader" library/src/main/jni/cge/filters/cgeHistogramFilter.cpp | head -20Repository: wysaid/android-gpuimage-plus Length of output: 354 🏁 Script executed: sed -n '64,100p' library/src/main/jni/cge/filters/cgeHistogramFilter.cppRepository: wysaid/android-gpuimage-plus Length of output: 1371 Add The compute shader writes to 🔧 Suggested fix- glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
+ glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Composite the display texture onto the frame | ||||||
| // ----------------------------------------------------------------------- | ||||||
| #if USING_ALPHA | ||||||
| glEnable(GL_BLEND); | ||||||
| glBlendColor(1, 1, 1, 0.8f); | ||||||
| glBlendFunc(GL_ONE, GL_ONE_MINUS_CONSTANT_ALPHA); | ||||||
| #endif | ||||||
|
|
||||||
| handler->setAsTarget(); | ||||||
| glViewport( | ||||||
| static_cast<GLint>(m_position[0] * sz.width), | ||||||
| static_cast<GLint>(m_position[1] * sz.height), | ||||||
| static_cast<GLsizei>(m_size[0] * sz.width), | ||||||
| static_cast<GLsizei>(m_size[1] * sz.height)); | ||||||
|
|
||||||
| m_drawer->drawTexture(m_displayTexture->texture()); | ||||||
|
|
||||||
| #if USING_ALPHA | ||||||
| glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); | ||||||
| glDisable(GL_BLEND); | ||||||
| #endif | ||||||
| } | ||||||
|
|
||||||
| void CGEHistogramFilter::setFormPosition(float left, float top) | ||||||
| { | ||||||
| m_position = { left, top }; | ||||||
| } | ||||||
|
|
||||||
| void CGEHistogramFilter::setFormSize(float width, float height) | ||||||
| { | ||||||
| m_size = { width, height }; | ||||||
| } | ||||||
|
Comment on lines
+200
to
+208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enforce normalized bounds in histogram overlay setters. Line 200 and Line 205 accept unrestricted values even though the API contract is normalized 🔧 Suggested fix void CGEHistogramFilter::setFormPosition(float left, float top)
{
- m_position = { left, top };
+ left = left < 0.0f ? 0.0f : (left > 1.0f ? 1.0f : left);
+ top = top < 0.0f ? 0.0f : (top > 1.0f ? 1.0f : top);
+ m_position = { left, top };
}
void CGEHistogramFilter::setFormSize(float width, float height)
{
- m_size = { width, height };
+ width = width < 0.0f ? 0.0f : (width > 1.0f ? 1.0f : width);
+ height = height < 0.0f ? 0.0f : (height > 1.0f ? 1.0f : height);
+ m_size = { width, height };
}🤖 Prompt for AI Agents |
||||||
|
|
||||||
| } // namespace CGE | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /* | ||
| * cgeHistogramFilter.h | ||
| * | ||
| * Luminance histogram overlay filter using GLES 3.1 compute shaders | ||
| * with imageAtomicAdd for GPU-side accumulation. | ||
| * | ||
| * Rule syntax: @style hist <left> <top> <width> <height> | ||
| * | ||
| * e.g. "@style hist 0.05 0.05 0.25 0.25" | ||
| * Renders a 256×256 histogram in the top-left corner of the frame. | ||
| */ | ||
|
|
||
| #ifndef _CGE_HISTOGRAM_FILTER_H_ | ||
| #define _CGE_HISTOGRAM_FILTER_H_ | ||
|
|
||
| #include "cgeImageFilter.h" | ||
| #include "cgeTextureUtils.h" | ||
| #include "cgeVec.h" | ||
|
|
||
| namespace CGE | ||
| { | ||
| class CGEHistogramFilter : public CGEImageFilterInterface | ||
| { | ||
| public: | ||
| ~CGEHistogramFilter() override; | ||
|
|
||
| /** | ||
| * @brief Position of the top-left corner of the overlay (normalised, [0, 1]). | ||
| * Defaults to (0.1, 0.1). | ||
| */ | ||
| void setFormPosition(float left, float top); | ||
|
|
||
| /** | ||
| * @brief Size of the histogram overlay (normalised, [0, 1]). | ||
| * Defaults to (0.3, 0.3). | ||
| */ | ||
| void setFormSize(float width, float height); | ||
|
|
||
| bool init() override; | ||
|
|
||
| void render2Texture(CGEImageHandlerInterface* handler, | ||
| GLuint srcTexture, | ||
| GLuint vertexBufferID) override; | ||
|
|
||
| protected: | ||
| std::unique_ptr<TextureDrawer> m_drawer; | ||
| std::unique_ptr<TextureObject> m_displayTexture; ///< 256×256 rgba8 — drawn onto frame | ||
|
|
||
| GLuint m_histogramTex = 0; ///< 256×1 r32ui — atomic luminance counters | ||
|
|
||
| ProgramObject m_clearProgram; ///< pass 1: zero out histogram counters | ||
| ProgramObject m_countProgram; ///< pass 2: accumulate per-pixel luminance | ||
| ProgramObject m_drawProgram; ///< pass 3: render bar chart into display tex | ||
|
|
||
| Vec2f m_position; ///< normalised top-left | ||
| Vec2f m_size; ///< normalised width/height | ||
| }; | ||
| } // namespace CGE | ||
|
|
||
| #endif // _CGE_HISTOGRAM_FILTER_H_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Use project precision macros for inline GLSL compute sources.
Line 25, Line 39, and Line 64 currently build shaders with
CGE_SHADER_STRING(...)directly. Please switch these shader literals to the required precision helper macros for consistency with repository shader conventions.As per coding guidelines:
library/src/main/jni/**/*.{cpp,h,c}requires inline GLSL viaCGE_SHADER_STRING_PRECISION_H()/CGE_SHADER_STRING_PRECISION_M()macros.Also applies to: 39-55, 64-87
🧰 Tools
🪛 Cppcheck (2.19.0)
[error] 25-25: There is an unknown macro here somewhere. Configuration is required. If CGE_SHADER_STRING is a macro then please configure it.
(unknownMacro)
🤖 Prompt for AI Agents