/******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2010 by Fredrik Höglund Copyright (C) 2010 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "lanczosfilter.h" #include "effects.h" #ifdef KWIN_HAVE_OPENGL_COMPOSITING #include #endif #include #include #include #include namespace KWin { LanczosFilter::LanczosFilter( QObject* parent ) : QObject( parent ) #ifdef KWIN_HAVE_OPENGL_COMPOSITING , m_offscreenTex( 0 ) , m_offscreenTarget( 0 ) , m_shader( new LanczosShader( this ) ) #endif , m_inited( false) { } LanczosFilter::~LanczosFilter() { #ifdef KWIN_HAVE_OPENGL_COMPOSITING delete m_offscreenTarget; delete m_offscreenTex; #endif } void LanczosFilter::init() { if (m_inited) return; m_inited = true; #ifdef KWIN_HAVE_OPENGL_COMPOSITING // check the blacklist KSharedConfigPtr config = KSharedConfig::openConfig( "kwinrc" ); KConfigGroup blacklist = config->group( "Blacklist" ).group( "Lanczos" ); if( effects->checkDriverBlacklist( blacklist ) ) { kDebug() << "Lanczos Filter disabled by driver blacklist"; return; } if( !m_shader->init() ) { delete m_shader; m_shader = 0; } #endif } void LanczosFilter::updateOffscreenSurfaces() { #ifdef KWIN_HAVE_OPENGL_COMPOSITING int w = displayWidth(); int h = displayHeight(); if ( !GLTexture::NPOTTextureSupported() ) { w = nearestPowerOfTwo( w ); h = nearestPowerOfTwo( h ); } if ( !m_offscreenTex || m_offscreenTex->width() != w || m_offscreenTex->height() != h ) { if ( m_offscreenTex ) { delete m_offscreenTex; delete m_offscreenTarget; } m_offscreenTex = new GLTexture( w, h ); m_offscreenTex->setFilter( GL_LINEAR ); m_offscreenTex->setWrapMode( GL_CLAMP_TO_EDGE ); m_offscreenTarget = new GLRenderTarget( m_offscreenTex ); } #endif } static float sinc( float x ) { return std::sin( x * M_PI ) / ( x * M_PI ); } static float lanczos( float x, float a ) { if ( qFuzzyCompare( x + 1.0, 1.0 ) ) return 1.0; if ( qAbs( x ) >= a ) return 0.0; return sinc( x ) * sinc( x / a ); } #ifdef KWIN_HAVE_OPENGL_COMPOSITING void LanczosShader::createKernel( float delta, int *size ) { const float a = 2.0; // The two outermost samples always fall at points where the lanczos // function returns 0, so we'll skip them. const int sampleCount = qBound( 3, qCeil(delta * a) * 2 + 1 - 2, 49 ); const int center = sampleCount / 2; const int kernelSize = center + 1; const float factor = 1.0 / delta; QVector values( kernelSize ); float sum = 0; for ( int i = 0; i < kernelSize; i++ ) { const float val = lanczos( i * factor, a ); sum += i > 0 ? val * 2 : val; values[i] = val; } memset(m_kernel, 0, 25 * sizeof(QVector4D)); // Normalize the kernel for ( int i = 0; i < kernelSize; i++ ) { const float val = values[i] / sum; m_kernel[i] = QVector4D( val, val, val, val ); } *size = kernelSize; } void LanczosShader::createOffsets( int count, float width, Qt::Orientation direction ) { memset(m_offsets, 0, 25 * sizeof(QVector2D)); for ( int i = 0; i < count; i++ ) { m_offsets[i] = ( direction == Qt::Horizontal ) ? QVector2D( i / width, 0 ) : QVector2D( 0, i / width ); } } #endif void LanczosFilter::performPaint( EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data ) { #ifdef KWIN_HAVE_OPENGL_COMPOSITING if( effects->compositingType() == KWin::OpenGLCompositing && KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects ) { if (!m_inited) init(); const QRect screenRect = Workspace::self()->clientArea( ScreenArea, w->screen(), w->desktop() ); // window geometry may not be bigger than screen geometry to fit into the FBO if ( m_shader && w->width() <= screenRect.width() && w->height() <= screenRect.height() ) { double left = 0; double top = 0; double right = w->width(); double bottom = w->height(); foreach( const WindowQuad& quad, data.quads ) { // we need this loop to include the decoration padding left = qMin(left, quad.left()); top = qMin(top, quad.top()); right = qMax(right, quad.right()); bottom = qMax(bottom, quad.bottom()); } double width = right - left; double height = bottom - top; if( width > screenRect.width() || height > screenRect.height() ) { // window with padding does not fit into the framebuffer // so cut of the shadow left = 0; top = 0; width = w->width(); height = w->height(); } int tx = data.xTranslate + w->x() + left*data.xScale; int ty = data.yTranslate + w->y() + top*data.yScale; int tw = width*data.xScale; int th = height*data.yScale; const QRect textureRect(tx, ty, tw, th); int sw = width; int sh = height; GLTexture *cachedTexture = static_cast< GLTexture*>(w->data( LanczosCacheRole ).value()); if( cachedTexture ) { if( cachedTexture->width() == tw && cachedTexture->height() == th ) { cachedTexture->bind(); data.opacity *= 0.99; prepareRenderStates( cachedTexture, data.opacity, data.brightness, data.saturation ); cachedTexture->render( textureRect, textureRect ); restoreRenderStates( cachedTexture, data.opacity, data.brightness, data.saturation ); cachedTexture->unbind(); m_timer.start( 5000, this ); return; } else { // offscreen texture not matching - delete delete cachedTexture; cachedTexture = 0; w->setData( LanczosCacheRole, QVariant() ); } } WindowPaintData thumbData = data; thumbData.xScale = 1.0; thumbData.yScale = 1.0; thumbData.xTranslate = -w->x() - left; thumbData.yTranslate = -w->y() - top; thumbData.brightness = 1.0; thumbData.opacity = 1.0; thumbData.saturation = 1.0; // Bind the offscreen FBO and draw the window on it unscaled updateOffscreenSurfaces(); effects->pushRenderTarget( m_offscreenTarget ); glClear( GL_COLOR_BUFFER_BIT ); w->sceneWindow()->performPaint( mask, QRegion(0, 0, sw, sh), thumbData ); // Create a scratch texture and copy the rendered window into it GLTexture tex( sw, sh ); tex.setFilter( GL_LINEAR ); tex.setWrapMode( GL_CLAMP_TO_EDGE ); tex.bind(); glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - sh, sw, sh ); // Set up the shader for horizontal scaling float dx = sw / float(tw); int kernelSize; m_shader->createKernel( dx, &kernelSize ); m_shader->createOffsets( kernelSize, sw, Qt::Horizontal ); m_shader->bind(); m_shader->setUniforms(); // Draw the window back into the FBO, this time scaled horizontally glClear( GL_COLOR_BUFFER_BIT ); glBegin( GL_QUADS ); glTexCoord2f( 0, 0 ); glVertex2i( 0, 0 ); // Top left glTexCoord2f( 1, 0 ); glVertex2i( tw, 0 ); // Top right glTexCoord2f( 1, 1 ); glVertex2i( tw, sh ); // Bottom right glTexCoord2f( 0, 1 ); glVertex2i( 0, sh ); // Bottom left glEnd(); // At this point we don't need the scratch texture anymore tex.unbind(); tex.discard(); // create scratch texture for second rendering pass GLTexture tex2( tw, sh ); tex2.setFilter( GL_LINEAR ); tex2.setWrapMode( GL_CLAMP_TO_EDGE ); tex2.bind(); glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - sh, tw, sh ); // Set up the shader for vertical scaling float dy = sh / float(th); m_shader->createKernel( dy, &kernelSize ); m_shader->createOffsets( kernelSize, m_offscreenTex->height(), Qt::Vertical ); m_shader->setUniforms(); // Now draw the horizontally scaled window in the FBO at the right // coordinates on the screen, while scaling it vertically and blending it. glClear( GL_COLOR_BUFFER_BIT ); glBegin( GL_QUADS ); glTexCoord2f( 0, 0 ); glVertex2i( 0, 0 ); // Top left glTexCoord2f( 1, 0 ); glVertex2i( tw, 0 ); // Top right glTexCoord2f( 1, 1 ); glVertex2i( tw, th ); // Bottom right glTexCoord2f( 0, 1 ); glVertex2i( 0, th ); // Bottom left glEnd(); tex2.unbind(); tex2.discard(); m_shader->unbind(); // create cache texture GLTexture *cache = new GLTexture( tw, th ); cache->setFilter( GL_LINEAR ); cache->setWrapMode( GL_CLAMP_TO_EDGE ); cache->bind(); glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - th, tw, th ); effects->popRenderTarget(); prepareRenderStates( cache, data.opacity, data.brightness, data.saturation ); cache->render( textureRect, textureRect ); restoreRenderStates( cache, data.opacity, data.brightness, data.saturation ); cache->unbind(); w->setData( LanczosCacheRole, QVariant::fromValue( static_cast( cache ))); // Delete the offscreen surface after 5 seconds m_timer.start( 5000, this ); return; } } // if ( effects->compositingType() == KWin::OpenGLCompositing ) #endif w->sceneWindow()->performPaint( mask, region, data ); } // End of function void LanczosFilter::timerEvent( QTimerEvent *event ) { #ifdef KWIN_HAVE_OPENGL_COMPOSITING if (event->timerId() == m_timer.timerId()) { m_timer.stop(); delete m_offscreenTarget; delete m_offscreenTex; m_offscreenTarget = 0; m_offscreenTex = 0; foreach( EffectWindow* w, effects->stackingOrder() ) { QVariant cachedTextureVariant = w->data( LanczosCacheRole ); if( cachedTextureVariant.isValid() ) { GLTexture *cachedTexture = static_cast< GLTexture*>(cachedTextureVariant.value()); delete cachedTexture; cachedTexture = 0; w->setData( LanczosCacheRole, QVariant() ); } } } #endif } void LanczosFilter::prepareRenderStates( GLTexture* tex, double opacity, double brightness, double saturation ) { #ifdef KWIN_HAVE_OPENGL_COMPOSITING const bool alpha = false; // true; WORKAROUND - "true" causes issues with at least nvidia chips and translucent windows "overbrightning" // setup blending of transparent windows glPushAttrib( GL_ENABLE_BIT ); glEnable( GL_BLEND ); glBlendFunc( GL_ONE, GL_ONE_MINUS_SRC_ALPHA ); if( saturation != 1.0 && tex->saturationSupported()) { // First we need to get the color from [0; 1] range to [0.5; 1] range glActiveTexture( GL_TEXTURE0 ); glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE ); glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_CONSTANT ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_ALPHA ); const float scale_constant[] = { 1.0, 1.0, 1.0, 0.5}; glTexEnvfv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, scale_constant ); tex->bind(); // Then we take dot product of the result of previous pass and // saturation_constant. This gives us completely unsaturated // (greyscale) image // Note that both operands have to be in range [0.5; 1] since opengl // automatically substracts 0.5 from them glActiveTexture( GL_TEXTURE1 ); float saturation_constant[] = { 0.5 + 0.5*0.30, 0.5 + 0.5*0.59, 0.5 + 0.5*0.11, saturation }; glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE ); glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_DOT3_RGB ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR ); glTexEnvfv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, saturation_constant ); tex->bind(); // Finally we need to interpolate between the original image and the // greyscale image to get wanted level of saturation glActiveTexture( GL_TEXTURE2 ); glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE ); glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE0 ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_CONSTANT ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_ALPHA ); glTexEnvfv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, saturation_constant ); // Also replace alpha by primary color's alpha here glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PRIMARY_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA ); // And make primary color contain the wanted opacity glColor4f( opacity, opacity, opacity, opacity ); tex->bind(); if( alpha || brightness != 1.0f ) { glActiveTexture( GL_TEXTURE3 ); glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE ); glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PRIMARY_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR ); // The color has to be multiplied by both opacity and brightness float opacityByBrightness = opacity * brightness; glColor4f( opacityByBrightness, opacityByBrightness, opacityByBrightness, opacity ); if( alpha ) { // Multiply original texture's alpha by our opacity glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE0 ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PRIMARY_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA ); } else { // Alpha will be taken from previous stage glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA ); } tex->bind(); } glActiveTexture(GL_TEXTURE0 ); } else if( opacity != 1.0 || brightness != 1.0 ) { // the window is additionally configured to have its opacity adjusted, // do it float opacityByBrightness = opacity * brightness; if( alpha) { glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); glColor4f( opacityByBrightness, opacityByBrightness, opacityByBrightness, opacity); } else { // Multiply color by brightness and replace alpha by opacity float constant[] = { opacityByBrightness, opacityByBrightness, opacityByBrightness, opacity }; glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE ); glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT ); glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR ); glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE ); glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_CONSTANT ); glTexEnvfv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, constant ); } } #endif } void LanczosFilter::restoreRenderStates( GLTexture* tex, double opacity, double brightness, double saturation ) { #ifdef KWIN_HAVE_OPENGL_COMPOSITING if( opacity != 1.0 || saturation != 1.0 || brightness != 1.0f ) { if( saturation != 1.0 && tex->saturationSupported()) { glActiveTexture(GL_TEXTURE3); glDisable( tex->target()); glActiveTexture(GL_TEXTURE2); glDisable( tex->target()); glActiveTexture(GL_TEXTURE1); glDisable( tex->target()); glActiveTexture(GL_TEXTURE0); } } glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE ); glColor4f( 0, 0, 0, 0 ); glPopAttrib(); // ENABLE_BIT #endif } /************************************************ * LanczosShader ************************************************/ #ifdef KWIN_HAVE_OPENGL_COMPOSITING LanczosShader::LanczosShader( QObject* parent ) : QObject( parent ) , m_shader( 0 ) , m_arbProgram( 0 ) { } LanczosShader::~LanczosShader() { delete m_shader; if( m_arbProgram ) { glDeleteProgramsARB(1, &m_arbProgram); m_arbProgram = 0; } } void LanczosShader::bind() { if( m_shader ) m_shader->bind(); else { glEnable(GL_FRAGMENT_PROGRAM_ARB); glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, m_arbProgram); } } void LanczosShader::unbind() { if( m_shader ) m_shader->unbind(); else { int boundObject; glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_PROGRAM_BINDING_ARB, &boundObject); if( boundObject == m_arbProgram ) { glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); glDisable(GL_FRAGMENT_PROGRAM_ARB); } } } void LanczosShader::setUniforms() { if( m_shader ) { glUniform1i( m_uTexUnit, 0 ); glUniform2fv( m_uOffsets, 25, (const GLfloat*)m_offsets ); glUniform4fv( m_uKernel, 25, (const GLfloat*)m_kernel ); } else { for( int i=0; i<25; ++i ) { glProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, i, m_offsets[i].x(), m_offsets[i].y(), 0, 0 ); } for( int i=0; i<25; ++i ) { glProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, i+25, m_kernel[i].x(), m_kernel[i].y(), m_kernel[i].z(), m_kernel[i].w() ); } } } bool LanczosShader::init() { if ( GLShader::fragmentShaderSupported() && GLShader::vertexShaderSupported() && GLRenderTarget::supported() ) { m_shader = new GLShader(":/resources/lanczos-vertex.glsl", ":/resources/lanczos-fragment.glsl"); if (m_shader->isValid()) { m_shader->bind(); m_uTexUnit = m_shader->uniformLocation("texUnit"); m_uKernel = m_shader->uniformLocation("kernel"); m_uOffsets = m_shader->uniformLocation("offsets"); m_shader->unbind(); return true; } else { kDebug(1212) << "Shader is not valid"; m_shader = 0; // try ARB shader } } // try to create an ARB Shader if( !hasGLExtension("GL_ARB_fragment_program") ) return false; QByteArray text; QTextStream stream(&text); stream << "!!ARBfp1.0\n"; stream << "TEMP coord;\n"; // temporary variable to store texcoord stream << "TEMP color;\n"; // temporary variable to store fetched texture colors stream << "TEMP sum;\n"; // variable to render the final result stream << "TEX sum, fragment.texcoord, texture[0], 2D;\n"; // sum = texture2D(texUnit, gl_TexCoord[0].st) stream << "MUL sum, sum, program.local[25];\n"; // sum = sum * kernel[0] for( int i=1; i<25; ++i ) { stream << "ADD coord, fragment.texcoord, program.local[" << i << "];\n"; // coord = gl_TexCoord[0] + offset[i] stream << "TEX color, coord, texture[0], 2D;\n"; // color = texture2D(texUnit, coord) stream << "MAD sum, color, program.local[" << (25+i) << "], sum;\n"; // sum += color * kernel[i] stream << "SUB coord, fragment.texcoord, program.local[" << i << "];\n"; // coord = gl_TexCoord[0] - offset[i] stream << "TEX color, coord, texture[0], 2D;\n"; // color = texture2D(texUnit, coord) stream << "MAD sum, color, program.local[" << (25+i) << "], sum;\n"; // sum += color * kernel[i] } stream << "MOV result.color, sum;\n"; // gl_FragColor = sum stream << "END\n"; stream.flush(); glEnable(GL_FRAGMENT_PROGRAM_ARB); glGenProgramsARB(1, &m_arbProgram); glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, m_arbProgram); glProgramStringARB(GL_FRAGMENT_PROGRAM_ARB, GL_PROGRAM_FORMAT_ASCII_ARB, text.length(), text.constData()); if( glGetError() ) { const char *error = (const char*)glGetString(GL_PROGRAM_ERROR_STRING_ARB); kError() << "Failed to compile fragment program:" << error; return false; } glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); glDisable(GL_FRAGMENT_PROGRAM_ARB); kDebug( 1212 ) << "ARB Shader compiled, id: " << m_arbProgram; return true; } #endif } // namespace