diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index f43671dee5c23cb53937442fd2c3eef3244ce0de..9bd3a0a6b0a1c93007883a3838780043ac34e9ed 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -211,6 +211,7 @@ S32 LLImageGL::dataFormatBits(S32 dataformat)
     case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT:    return 8;
     case GL_LUMINANCE:						        return 8;
     case GL_ALPHA:							        return 8;
+    case GL_RED:                                    return 8;
     case GL_COLOR_INDEX:						    return 8;
     case GL_LUMINANCE_ALPHA:					    return 16;
     case GL_RGB:								    return 24;
@@ -260,6 +261,7 @@ S32 LLImageGL::dataFormatComponents(S32 dataformat)
 	  case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: return 4;
 	  case GL_LUMINANCE:						return 1;
 	  case GL_ALPHA:							return 1;
+      case GL_RED:                              return 1;
 	  case GL_COLOR_INDEX:						return 1;
 	  case GL_LUMINANCE_ALPHA:					return 2;
 	  case GL_RGB:								return 3;
@@ -1199,7 +1201,29 @@ BOOL LLImageGL::setSubImageFromFrameBuffer(S32 fb_x, S32 fb_y, S32 x_pos, S32 y_
 void LLImageGL::generateTextures(S32 numTextures, U32 *textures)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
-	glGenTextures(numTextures, textures);
+    static constexpr U32 pool_size = 1024;
+    static thread_local U32 name_pool[pool_size]; // pool of texture names
+    static thread_local U32 name_count = 0; // number of available names in the pool
+
+    if (name_count == 0)
+    {
+        LL_PROFILE_ZONE_NAMED("iglgt - reup pool");
+        // pool is emtpy, refill it
+        glGenTextures(pool_size, name_pool);
+        name_count = pool_size;
+    }
+
+    if (numTextures <= name_count)
+    {
+        //copy teture names off the end of the pool
+        memcpy(textures, name_pool + name_count - numTextures, sizeof(U32) * numTextures);
+        name_count -= numTextures;
+    }
+    else
+    {
+        LL_PROFILE_ZONE_NAMED("iglgt - pool miss");
+        glGenTextures(numTextures, textures);
+    }
 }
 
 // static
@@ -1221,15 +1245,18 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
     {
         if (pixformat == GL_ALPHA && pixtype == GL_UNSIGNED_BYTE)
         { //GL_ALPHA is deprecated, convert to RGBA
-            use_scratch = true;
-            scratch = new U32[width * height];
-
-            U32 pixel_count = (U32)(width * height);
-            for (U32 i = 0; i < pixel_count; i++)
+            if (pixels != nullptr)
             {
-                U8* pix = (U8*)&scratch[i];
-                pix[0] = pix[1] = pix[2] = 0;
-                pix[3] = ((U8*)pixels)[i];
+                use_scratch = true;
+                scratch = new U32[width * height];
+
+                U32 pixel_count = (U32)(width * height);
+                for (U32 i = 0; i < pixel_count; i++)
+                {
+                    U8* pix = (U8*)&scratch[i];
+                    pix[0] = pix[1] = pix[2] = 0;
+                    pix[3] = ((U8*)pixels)[i];
+                }
             }
 
             pixformat = GL_RGBA;
@@ -1238,18 +1265,21 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
 
         if (pixformat == GL_LUMINANCE_ALPHA && pixtype == GL_UNSIGNED_BYTE)
         { //GL_LUMINANCE_ALPHA is deprecated, convert to RGBA
-            use_scratch = true;
-            scratch = new U32[width * height];
-
-            U32 pixel_count = (U32)(width * height);
-            for (U32 i = 0; i < pixel_count; i++)
+            if (pixels != nullptr)
             {
-                U8 lum = ((U8*)pixels)[i * 2 + 0];
-                U8 alpha = ((U8*)pixels)[i * 2 + 1];
+                use_scratch = true;
+                scratch = new U32[width * height];
+
+                U32 pixel_count = (U32)(width * height);
+                for (U32 i = 0; i < pixel_count; i++)
+                {
+                    U8 lum = ((U8*)pixels)[i * 2 + 0];
+                    U8 alpha = ((U8*)pixels)[i * 2 + 1];
 
-                U8* pix = (U8*)&scratch[i];
-                pix[0] = pix[1] = pix[2] = lum;
-                pix[3] = alpha;
+                    U8* pix = (U8*)&scratch[i];
+                    pix[0] = pix[1] = pix[2] = lum;
+                    pix[3] = alpha;
+                }
             }
 
             pixformat = GL_RGBA;
@@ -1258,19 +1288,21 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
 
         if (pixformat == GL_LUMINANCE && pixtype == GL_UNSIGNED_BYTE)
         { //GL_LUMINANCE_ALPHA is deprecated, convert to RGB
-            use_scratch = true;
-            scratch = new U32[width * height];
-
-            U32 pixel_count = (U32)(width * height);
-            for (U32 i = 0; i < pixel_count; i++)
+            if (pixels != nullptr)
             {
-                U8 lum = ((U8*)pixels)[i];
+                use_scratch = true;
+                scratch = new U32[width * height];
 
-                U8* pix = (U8*)&scratch[i];
-                pix[0] = pix[1] = pix[2] = lum;
-                pix[3] = 255;
-            }
+                U32 pixel_count = (U32)(width * height);
+                for (U32 i = 0; i < pixel_count; i++)
+                {
+                    U8 lum = ((U8*)pixels)[i];
 
+                    U8* pix = (U8*)&scratch[i];
+                    pix[0] = pix[1] = pix[2] = lum;
+                    pix[3] = 255;
+                }
+            }
             pixformat = GL_RGBA;
             intformat = GL_RGB8;
         }
@@ -1308,6 +1340,10 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
         case GL_ALPHA8:
             intformat = GL_COMPRESSED_ALPHA;
             break;
+        case GL_RED:
+        case GL_R8:
+            intformat = GL_COMPRESSED_RED;
+            break;
         default:
             LL_WARNS() << "Could not compress format: " << std::hex << intformat << LL_ENDL;
             break;
@@ -2010,6 +2046,7 @@ void LLImageGL::calcAlphaChannelOffsetAndStride()
     case GL_LUMINANCE_ALPHA:
         mAlphaStride = 2;
         break;
+    case GL_RED:
     case GL_RGB:
     case GL_SRGB:
         mNeedsAlphaAndPickMask = FALSE;
diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp
index 401085a00bc9dc767762a1a5e9d219c06599b91a..04080105131c63a73bdb6e6b0c90820ac0d65611 100644
--- a/indra/llrender/llrendertarget.cpp
+++ b/indra/llrender/llrendertarget.cpp
@@ -170,6 +170,53 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, boo
 	return addColorAttachment(color_fmt);
 }
 
+void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name)
+{
+    LL_PROFILE_ZONE_SCOPED;
+    llassert(img != nullptr); // img must not be null
+    llassert(sUseFBO); // FBO support must be enabled
+    llassert(mDepth == 0); // depth buffers not supported with this mode
+    llassert(mTex.empty()); // mTex must be empty with this mode (binding target should be done via LLImageGL)
+
+    if (mFBO == 0)
+    {
+        glGenFramebuffers(1, (GLuint*)&mFBO);
+    }
+
+    mResX = img->getWidth();
+    mResY = img->getHeight();
+    mUsage = img->getTarget();
+
+    if (use_name == 0)
+    {
+        use_name = img->getTexName();
+    }
+
+    mTex.push_back(use_name);
+
+    glBindFramebuffer(GL_FRAMEBUFFER, mFBO);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+            LLTexUnit::getInternalType(mUsage), use_name, 0);
+        stop_glerror();
+
+    check_framebuffer_status();
+
+    glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO);
+}
+
+void LLRenderTarget::releaseColorAttachment()
+{
+    LL_PROFILE_ZONE_SCOPED;
+    llassert(mTex.size() == 1); //cannot use releaseColorAttachment with LLRenderTarget managed color targets
+    llassert(mFBO != 0);  // mFBO must be valid
+    
+    glBindFramebuffer(GL_FRAMEBUFFER, mFBO);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, LLTexUnit::getInternalType(mUsage), 0, 0);
+    glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO);
+
+    mTex.clear();
+}
+
 bool LLRenderTarget::addColorAttachment(U32 color_fmt)
 {
 	if (color_fmt == 0)
diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h
index 6c07ac5b1c7c8645af7e04c45fc731bdc97f3dc7..584f224dcaccc3893e1fb15954cfd171fce19a4f 100644
--- a/indra/llrender/llrendertarget.h
+++ b/indra/llrender/llrendertarget.h
@@ -81,6 +81,24 @@ class LLRenderTarget
 	// DO use for render targets that resize often and aren't likely to ruin someone's day if they break
 	void resize(U32 resx, U32 resy);
 
+    //point this render target at a particular LLImageGL
+    //   Intended usage:
+    //      LLRenderTarget target;
+    //      target.addColorAttachment(image);
+    //      target.bindTarget();
+    //      < issue GL calls>
+    //      target.flush();
+    //      target.releaseColorAttachment();
+    // 
+    // attachment -- LLImageGL to render into
+    // use_name -- optional texture name to target instead of attachment->getTexName()
+    // NOTE: setColorAttachment and releaseColorAttachment cannot be used in conjuction with
+    // addColorAttachment, allocateDepth, resize, etc.
+    void setColorAttachment(LLImageGL* attachment, LLGLuint use_name = 0);
+
+    // detach from current color attachment
+    void releaseColorAttachment();
+
 	//add color buffer attachment
 	//limit of 4 color attachments per render target
 	bool addColorAttachment(U32 color_fmt);
diff --git a/indra/newview/app_settings/shaders/class1/deferred/normgenF.glsl b/indra/newview/app_settings/shaders/class1/deferred/normgenF.glsl
index d0c06cd51f6e7a9341742e346ccb341afa7a239d..7a941674b87363f6ceeb7dc635cab45e55b37aa7 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/normgenF.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/normgenF.glsl
@@ -43,18 +43,18 @@ uniform float norm_scale;
 
 void main()
 {
-	float alpha = texture2D(alphaMap, vary_texcoord0).a;
+	float c = texture2D(alphaMap, vary_texcoord0).r;
 
-	vec3 right = vec3(norm_scale, 0, (texture2D(alphaMap, vary_texcoord0+vec2(stepX, 0)).a-alpha)*255);
-	vec3 left = vec3(-norm_scale, 0, (texture2D(alphaMap, vary_texcoord0-vec2(stepX, 0)).a-alpha)*255);
-	vec3 up = vec3(0, -norm_scale, (texture2D(alphaMap, vary_texcoord0-vec2(0, stepY)).a-alpha)*255);
-	vec3 down = vec3(0, norm_scale, (texture2D(alphaMap, vary_texcoord0+vec2(0, stepY)).a-alpha)*255);
+	vec3 right = vec3(norm_scale, 0, (texture2D(alphaMap, vary_texcoord0+vec2(stepX, 0)).r-c)*255);
+	vec3 left = vec3(-norm_scale, 0, (texture2D(alphaMap, vary_texcoord0-vec2(stepX, 0)).r-c)*255);
+	vec3 up = vec3(0, -norm_scale, (texture2D(alphaMap, vary_texcoord0-vec2(0, stepY)).r-c)*255);
+	vec3 down = vec3(0, norm_scale, (texture2D(alphaMap, vary_texcoord0+vec2(0, stepY)).r-c)*255);
 	
 	vec3 norm = cross(right, down) + cross(down, left) + cross(left,up) + cross(up, right);
 	
 	norm = normalize(norm);
 	norm *= 0.5;
 	norm += 0.5;	
-	
-	frag_color = vec4(norm, alpha);
+
+	frag_color = vec4(norm, c);
 }
diff --git a/indra/newview/lldrawpoolbump.cpp b/indra/newview/lldrawpoolbump.cpp
index 1d5419b515a1c6e3974a8108c0308c6d295439ac..2892fc6f9f533c3bf2270e48016ad085d7bfdb03 100644
--- a/indra/newview/lldrawpoolbump.cpp
+++ b/indra/newview/lldrawpoolbump.cpp
@@ -56,6 +56,7 @@
 LLStandardBumpmap gStandardBumpmapList[TEM_BUMPMAP_COUNT]; 
 LL::WorkQueue::weak_t LLBumpImageList::sMainQueue;
 LL::WorkQueue::weak_t LLBumpImageList::sTexUpdateQueue;
+LLRenderTarget LLBumpImageList::sRenderTarget;
 
 // static
 U32 LLStandardBumpmap::sStandardBumpmapCount = 0;
@@ -76,7 +77,7 @@ static S32 cube_channel = -1;
 static S32 diffuse_channel = -1;
 static S32 bump_channel = -1;
 
-#define LL_BUMPLIST_MULTITHREADED 0
+#define LL_BUMPLIST_MULTITHREADED 0 // TODO -- figure out why this doesn't work
 
 // static 
 void LLStandardBumpmap::init()
@@ -776,6 +777,8 @@ void LLBumpImageList::clear()
 	mBrightnessEntries.clear();
 	mDarknessEntries.clear();
 
+    sRenderTarget.release();
+
 	LLStandardBumpmap::clear();
 }
 
@@ -1032,6 +1035,8 @@ void LLBumpImageList::generateNormalMapFromAlpha(LLImageRaw* src, LLImageRaw* nr
 // static
 void LLBumpImageList::onSourceLoaded( BOOL success, LLViewerTexture *src_vi, LLImageRaw* src, LLUUID& source_asset_id, EBumpEffect bump_code )
 {
+    LL_PROFILE_ZONE_SCOPED;
+
 	if( success )
 	{
         LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL;
@@ -1201,145 +1206,111 @@ void LLBumpImageList::onSourceLoaded( BOOL success, LLViewerTexture *src_vi, LLI
 			}
 			else 
 			{ //convert to normal map
-				
-				//disable compression on normal maps to prevent errors below
-				bump->getGLTexture()->setAllowCompression(false);
-                bump->getGLTexture()->setUseMipMaps(TRUE);
-
-                auto* bump_ptr = bump.get();
-                auto* dst_ptr = dst_image.get();
+                LL_PROFILE_ZONE_NAMED("bil - create normal map");
+                LLImageGL* img = bump->getGLTexture();
+                LLImageRaw* dst_ptr = dst_image.get();
+                LLGLTexture* bump_ptr = bump.get();
 
-#if LL_BUMPLIST_MULTITHREADED
-                bump_ptr->ref();
                 dst_ptr->ref();
-#endif
-
-                bump_ptr->setExplicitFormat(GL_RGBA8, GL_ALPHA);
-
-                auto create_texture = [=]()
+                img->ref();
+                bump_ptr->ref();
+                auto create_func = [=]()
                 {
-#if LL_IMAGEGL_THREAD_CHECK
-                    bump_ptr->getGLTexture()->mActiveThread = LLThread::currentID();
-#endif
-                    LL_PROFILE_ZONE_NAMED("bil - create texture deferred");
+                    img->setUseMipMaps(TRUE);
+                    // upload dst_image to GPU (greyscale in red channel)
+                    img->setExplicitFormat(GL_RED, GL_RED);
+
                     bump_ptr->createGLTexture(0, dst_ptr);
+                    dst_ptr->unref();
                 };
 
-                auto gen_normal_map = [=]()
+                auto generate_func = [=]()
                 {
-#if LL_IMAGEGL_THREAD_CHECK
-                    bump_ptr->getGLTexture()->mActiveThread = LLThread::currentID();
-#endif
-                    LL_PROFILE_ZONE_NAMED("bil - generate normal map");
-                    if (gNormalMapGenProgram.mProgramObject == 0)
-                    {
-#if LL_BUMPLIST_MULTITHREADED
-                        bump_ptr->unref();
-                        dst_ptr->unref();
-#endif
-                        return;
-                    }
-                    gPipeline.mScreen.bindTarget();
-
-                    LLGLDepthTest depth(GL_FALSE);
-                    LLGLDisable cull(GL_CULL_FACE);
-                    LLGLDisable blend(GL_BLEND);
-                    gGL.setColorMask(TRUE, TRUE);
-                    gNormalMapGenProgram.bind();
-
-                    static LLStaticHashedString sNormScale("norm_scale");
-                    static LLStaticHashedString sStepX("stepX");
-                    static LLStaticHashedString sStepY("stepY");
-
-                    gNormalMapGenProgram.uniform1f(sNormScale, gSavedSettings.getF32("RenderNormalMapScale"));
-                    gNormalMapGenProgram.uniform1f(sStepX, 1.f / bump_ptr->getWidth());
-                    gNormalMapGenProgram.uniform1f(sStepY, 1.f / bump_ptr->getHeight());
+                    // Allocate an empty RGBA texture at "tex_name" the same size as bump
+                    //  Note: bump will still point at GPU copy of dst_image
+                    bump_ptr->setExplicitFormat(GL_RGBA, GL_RGBA);
+                    LLGLuint tex_name;
+                    img->createGLTexture(0, nullptr, 0, 0, true, &tex_name);
 
-                    LLVector2 v((F32)bump_ptr->getWidth() / gPipeline.mScreen.getWidth(),
-                        (F32)bump_ptr->getHeight() / gPipeline.mScreen.getHeight());
+                    // point render target at empty buffer
+                    sRenderTarget.setColorAttachment(img, tex_name);
 
-                    gGL.getTexUnit(0)->bind(bump_ptr);
-
-                    S32 width = bump_ptr->getWidth();
-                    S32 height = bump_ptr->getHeight();
-
-                    S32 screen_width = gPipeline.mScreen.getWidth();
-                    S32 screen_height = gPipeline.mScreen.getHeight();
-
-                    glViewport(0, 0, screen_width, screen_height);
-
-                    for (S32 left = 0; left < width; left += screen_width)
+                    // generate normal map in empty texture
                     {
-                        S32 right = left + screen_width;
-                        right = llmin(right, width);
+                        sRenderTarget.bindTarget();
 
-                        F32 left_tc = (F32)left / width;
-                        F32 right_tc = (F32)right / width;
+                        LLGLDepthTest depth(GL_FALSE);
+                        LLGLDisable cull(GL_CULL_FACE);
+                        LLGLDisable blend(GL_BLEND);
+                        gGL.setColorMask(TRUE, TRUE);
 
-                        for (S32 bottom = 0; bottom < height; bottom += screen_height)
-                        {
-                            S32 top = bottom + screen_height;
-                            top = llmin(top, height);
+                        gNormalMapGenProgram.bind();
 
-                            F32 bottom_tc = (F32)bottom / height;
-                            F32 top_tc = (F32)(bottom + screen_height) / height;
-                            top_tc = llmin(top_tc, 1.f);
+                        static LLStaticHashedString sNormScale("norm_scale");
+                        static LLStaticHashedString sStepX("stepX");
+                        static LLStaticHashedString sStepY("stepY");
 
-                            F32 screen_right = (F32)(right - left) / screen_width;
-                            F32 screen_top = (F32)(top - bottom) / screen_height;
+                        gNormalMapGenProgram.uniform1f(sNormScale, gSavedSettings.getF32("RenderNormalMapScale"));
+                        gNormalMapGenProgram.uniform1f(sStepX, 1.f / bump_ptr->getWidth());
+                        gNormalMapGenProgram.uniform1f(sStepY, 1.f / bump_ptr->getHeight());
 
-                            gGL.begin(LLRender::TRIANGLE_STRIP);
-                            gGL.texCoord2f(left_tc, bottom_tc);
-                            gGL.vertex2f(0, 0);
+                        gGL.getTexUnit(0)->bind(bump_ptr);
 
-                            gGL.texCoord2f(left_tc, top_tc);
-                            gGL.vertex2f(0, screen_top);
+                        gGL.begin(LLRender::TRIANGLE_STRIP);
+                        gGL.texCoord2f(0, 0);
+                        gGL.vertex2f(0, 0);
 
-                            gGL.texCoord2f(right_tc, bottom_tc);
-                            gGL.vertex2f(screen_right, 0);
+                        gGL.texCoord2f(0, 1);
+                        gGL.vertex2f(0, 1);
 
-                            gGL.texCoord2f(right_tc, top_tc);
-                            gGL.vertex2f(screen_right, screen_top);
+                        gGL.texCoord2f(1, 0);
+                        gGL.vertex2f(1, 0);
 
-                            gGL.end();
+                        gGL.texCoord2f(1, 1);
+                        gGL.vertex2f(1, 1);
 
-                            gGL.flush();
+                        gGL.end();
 
-                            S32 w = right - left;
-                            S32 h = top - bottom;
+                        gGL.flush();
 
-                            glCopyTexSubImage2D(GL_TEXTURE_2D, 0, left, bottom, 0, 0, w, h);
-                        }
-                    }
+                        gNormalMapGenProgram.unbind();
 
-                    glGenerateMipmap(GL_TEXTURE_2D);
+                        sRenderTarget.flush();
+                        sRenderTarget.releaseColorAttachment();
+                    }
 
-                    gPipeline.mScreen.flush();
+                    // point bump at normal map and free gpu copy of dst_image
+                    img->syncTexName(tex_name);
 
-                    gNormalMapGenProgram.unbind();
+                    // generate mipmap
+                    gGL.getTexUnit(0)->bind(img);
+                    glGenerateMipmap(GL_TEXTURE_2D);
+                    gGL.getTexUnit(0)->disable();
 
-                    //generateNormalMapFromAlpha(dst_image, nrm_image);
-#if LL_BUMPLIST_MULTITHREADED
                     bump_ptr->unref();
-                    dst_ptr->unref();
-#endif
+                    img->unref();
                 };
 
 #if LL_BUMPLIST_MULTITHREADED
-                auto main_queue = sMainQueue.lock();
-
-                if (LLImageGLThread::sEnabled)
-                { //dispatch creation to background thread
-                    main_queue->postTo(sTexUpdateQueue, create_texture, gen_normal_map);
+                auto main_queue = LLImageGLThread::sEnabled ? sMainQueue.lock() : nullptr;
+
+                if (main_queue)
+                { //dispatch texture upload to background thread, issue GPU commands to generate normal map on main thread
+                    main_queue->postTo(
+                        sTexUpdateQueue,
+                        create_func,
+                        generate_func);
                 }
                 else
 #endif
-                {
-                    create_texture();
-                    gen_normal_map();
+                { // immediate upload texture and generate normal map
+                    create_func();
+                    generate_func();
                 }
+
+
 			}
-		
+
 			iter->second = bump; // derefs (and deletes) old image
 			//---------------------------------------------------
 		}
diff --git a/indra/newview/lldrawpoolbump.h b/indra/newview/lldrawpoolbump.h
index 6e218597387188f560d96d1e55f773b7f6b373dc..e8a027967bcca922e44c560f2471874a1c22bed1 100644
--- a/indra/newview/lldrawpoolbump.h
+++ b/indra/newview/lldrawpoolbump.h
@@ -163,6 +163,7 @@ class LLBumpImageList
 	bump_image_map_t mDarknessEntries;
     static LL::WorkQueue::weak_t sMainQueue;
     static LL::WorkQueue::weak_t sTexUpdateQueue;
+    static LLRenderTarget sRenderTarget;
 };
 
 extern LLBumpImageList gBumpImageList;