/* LICENSE ------- Copyright 2005-2013 Nullsoft, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Nullsoft nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "textmgr.h" #include "support.h" #include "utility.h" #define MAX_MSG_CHARS (65536*2) #define SafeRelease(x) { if (x) {x->Release(); x=NULL;} } wchar_t g_szMsgPool[2][MAX_MSG_CHARS]; /* NOTES ON CTextManager *** -desktop mode was SLOOOW when songtitles are on!, esp. since anim. songtitles... -> decided to cache output of ID3DXFont by rendering to a (vidmem) texture, ** only when things change. ** That became CTextManager. -uses GDI-based ID3DXFont to draw text to a 2nd (VIDEO MEMORY) surface, but each frame, it only draws what is necessary (what's changed since last frame). It then blits that image (additively) to the back buffer each frame. (note that dark boxes wouldn't work w/additive drawing, since they're black, so those have to be manually drawn (as black boxes) by the plugin shell, AS WELL AS entered into the CTextManager queue as dark boxes, to handle erasure, dirty rectangles, etc.) PROS/CONS: (+) Supports all GDI features: italics, kerning, international fonts, formatting, &, etc. (-) takes a lot of memory (-) if texture can't be created @ proper size, fonts will appear too big -> so don't use texture at all, in that case. -> at least this way it will work well on all newer cards [w/memory] (-) it's still going to crawl *when the text changes*, because d3dx will upload textures to vidmem & blit them *once for each change*. OTHER CONCERNS/KIV: -what if m_lpDDSText can't be created @ actual size of window? If it's bigger, that's ok; but if it's smaller, that should result in a clipped area for the text - hmm.... */ CTextManager::CTextManager() { } CTextManager::~CTextManager() { } void CTextManager::Init(LPDIRECT3DDEVICE9 lpDevice, IDirect3DTexture9* lpTextSurface, int bAdditive) { m_lpDevice = lpDevice; m_lpTextSurface = lpTextSurface; m_blit_additively = bAdditive; m_b = 0; m_nMsg[0] = 0; m_nMsg[1] = 0; m_next_msg_start_ptr = g_szMsgPool[m_b]; } void CTextManager::Finish() { } void CTextManager::ClearAll() { m_nMsg[m_b] = 0; m_next_msg_start_ptr = g_szMsgPool[m_b]; } void CTextManager::DrawBox(LPRECT pRect, DWORD boxColor) { if (!pRect) return; if ((m_nMsg[m_b] < MAX_MSGS) && (DWORD)m_next_msg_start_ptr - (DWORD)g_szMsgPool[m_b] + 0 + 1 < MAX_MSG_CHARS) { *m_next_msg_start_ptr = 0; m_msg[m_b][m_nMsg[m_b]].msg = m_next_msg_start_ptr; m_msg[m_b][m_nMsg[m_b]].pfont = NULL; m_msg[m_b][m_nMsg[m_b]].rect = *pRect; m_msg[m_b][m_nMsg[m_b]].flags = 0; m_msg[m_b][m_nMsg[m_b]].color = 0xFFFFFFFF; m_msg[m_b][m_nMsg[m_b]].bgColor = boxColor; m_nMsg[m_b]++; m_next_msg_start_ptr += 1; } } int CTextManager::DrawText(LPD3DXFONT pFont, char* szText, RECT* pRect, DWORD flags, DWORD color, bool bBox, DWORD boxColor) { // these aren't supported by D3DX9: flags &= ~(DT_WORD_ELLIPSIS | DT_END_ELLIPSIS | DT_NOPREFIX); if (!(pFont && pRect && szText)) return 0; if (flags & DT_CALCRECT) return pFont->DrawText(NULL, szText, -1, pRect, flags, color); if (!m_lpDevice /*|| !m_lpTextSurface*/) return 0; int len = strlen(szText); if ((m_nMsg[m_b] < MAX_MSGS) && (DWORD)m_next_msg_start_ptr - (DWORD)g_szMsgPool[m_b] + len + 1 < MAX_MSG_CHARS) { wcscpy(m_next_msg_start_ptr, AutoWide(szText)); m_msg[m_b][m_nMsg[m_b]].msg = m_next_msg_start_ptr; m_msg[m_b][m_nMsg[m_b]].pfont = pFont; m_msg[m_b][m_nMsg[m_b]].rect = *pRect; m_msg[m_b][m_nMsg[m_b]].flags = flags; m_msg[m_b][m_nMsg[m_b]].color = color; m_msg[m_b][m_nMsg[m_b]].bgColor = boxColor; // shrink rects on new frame's text strings; important for deletions int h = pFont->DrawText(NULL, szText, len, &m_msg[m_b][m_nMsg[m_b]].rect, flags | DT_CALCRECT, color); m_nMsg[m_b]++; m_next_msg_start_ptr += len + 1; if (bBox) { // adds a message with no text, but the rect is the same as the text, so it creates a black box DrawBox(&m_msg[m_b][m_nMsg[m_b]-1].rect, boxColor); // now swap it with the text that precedes it, so it draws first, and becomes a background td_string x = m_msg[m_b][m_nMsg[m_b]-1]; m_msg[m_b][m_nMsg[m_b]-1] = m_msg[m_b][m_nMsg[m_b]-2]; m_msg[m_b][m_nMsg[m_b]-2] = x; } return h; } // no room for more text? ok, but still return accurate info: RECT r2 = *pRect; int h = pFont->DrawText(NULL, szText, len, &r2, flags | DT_CALCRECT, color); return h; } int CTextManager::DrawTextW(LPD3DXFONT pFont, wchar_t* szText, RECT* pRect, DWORD flags, DWORD color, bool bBox, DWORD boxColor) { // these aren't supported by D3DX9: flags &= ~(DT_WORD_ELLIPSIS | DT_END_ELLIPSIS | DT_NOPREFIX); if (!(pFont && pRect && szText)) return 0; if (flags & DT_CALCRECT) return pFont->DrawTextW(NULL, szText, -1, pRect, flags, color); if (!m_lpDevice /*|| !m_lpTextSurface*/) return 0; int len = wcslen(szText); if ((m_nMsg[m_b] < MAX_MSGS) && (DWORD)m_next_msg_start_ptr - (DWORD)g_szMsgPool[m_b] + len + 1 < MAX_MSG_CHARS) { wcscpy(m_next_msg_start_ptr, szText); m_msg[m_b][m_nMsg[m_b]].msg = m_next_msg_start_ptr; m_msg[m_b][m_nMsg[m_b]].pfont = pFont; m_msg[m_b][m_nMsg[m_b]].rect = *pRect; m_msg[m_b][m_nMsg[m_b]].flags = flags; m_msg[m_b][m_nMsg[m_b]].color = color; m_msg[m_b][m_nMsg[m_b]].bgColor = boxColor; // shrink rects on new frame's text strings; important for deletions int h = pFont->DrawTextW(NULL, szText, len, &m_msg[m_b][m_nMsg[m_b]].rect, flags | DT_CALCRECT, color); m_nMsg[m_b]++; m_next_msg_start_ptr += len + 1; if (bBox) { // adds a message with no text, but the rect is the same as the text, so it creates a black box DrawBox(&m_msg[m_b][m_nMsg[m_b]-1].rect, boxColor); // now swap it with the text that precedes it, so it draws first, and becomes a background td_string x = m_msg[m_b][m_nMsg[m_b]-1]; m_msg[m_b][m_nMsg[m_b]-1] = m_msg[m_b][m_nMsg[m_b]-2]; m_msg[m_b][m_nMsg[m_b]-2] = x; } return h; } // no room for more text? ok, but still return accurate info: RECT r2 = *pRect; int h = pFont->DrawTextW(NULL, szText, len, &r2, flags | DT_CALCRECT, color); return h; } #define MATCH(i,j) ( m_msg[m_b][i].pfont == m_msg[1-m_b][j].pfont && \ m_msg[m_b][i].flags == m_msg[1-m_b][j].flags && \ m_msg[m_b][i].color == m_msg[1-m_b][j].color && \ m_msg[m_b][i].bgColor == m_msg[1-m_b][j].bgColor && \ memcmp(&m_msg[m_b][i].rect, &m_msg[1-m_b][j].rect, sizeof(RECT))==0 && \ wcscmp(m_msg[m_b][i].msg, m_msg[1-m_b][j].msg)==0 ) void CTextManager::DrawNow() { if (!m_lpDevice) return; if (m_nMsg[m_b] > 0 || m_nMsg[1-m_b] > 0) // second condition req'd for clearing text in VJ mode { D3DXMATRIX Ortho2D; pMatrixOrthoLH(&Ortho2D, 2.0f, -2.0f, 0.0f, 1.0f); m_lpDevice->SetTransform(D3DTS_PROJECTION, &Ortho2D); #define NUM_DIRTY_RECTS 3 RECT dirty_rect[NUM_DIRTY_RECTS]; int dirty_rects_ready = 0; int bRTT = (m_lpTextSurface==NULL) ? 0 : 1; LPDIRECT3DSURFACE9 pBackBuffer=NULL;//, pZBuffer=NULL; D3DSURFACE_DESC desc_backbuf, desc_text_surface; // clear added/deleted flags void* last_dark_box = NULL; for (int i=0; i0 && m_nMsg[1-m_b]==0)) { bRedrawText = 2; // redraw ALL } else { // try to synchronize the text strings from last frame + this frame, // and label additions & deletions. algorithm will catch: // -insertion of any # of items in one spot // -deletion of any # of items from one spot // -changes to 1 item // -changes to 2 consecutive items // (provided that the 2 text strings immediately bounding the // additions/deletions/change(s) are left unchanged.) // in any other case, all the text is just re-rendered. int i = 0; int j = 0; while (i < m_nMsg[m_b] && j < m_nMsg[1-m_b]) { // MATCH macro: first idx is record # for current stuff; second idx is record # for prev frame stuff. if (MATCH(i,j)) { i++; j++; } else { int continue_now = 0; // scan to see if something was added: for (int i2=i+1; i2=m_nMsg[m_b]-chgd || j>=m_nMsg[1-m_b]-chgd) { // only a few items left in one of the lists -> just finish it bRedrawText = 1; break_now = 1; break; } if (i just re-render whole thing bRedrawText = 2; // redraw ALL break; } } if (bRedrawText < 2) { while (i < m_nMsg[m_b]) { m_msg[m_b][i].added = 1; bRedrawText = 1; i++; } while (j < m_nMsg[1-m_b]) { m_msg[1-m_b][j].deleted = 1; bRedrawText = 1; j++; } } } // ------------------------------------------------------------ // 0. remember old render target & get surface descriptions m_lpDevice->GetRenderTarget( 0, &pBackBuffer ); pBackBuffer->GetDesc(&desc_backbuf); if (bRTT) { //if (m_lpDevice->GetDepthStencilSurface( &pZBuffer ) != D3D_OK) // pZBuffer = NULL; // ok if return val != D3D_OK - just means there is no zbuffer. if (m_lpTextSurface->GetLevelDesc(0, &desc_text_surface) != D3D_OK) bRTT = 0; m_lpDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE ); m_lpDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); m_lpDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_CURRENT ); m_lpDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE ); m_lpDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1 ); m_lpDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE ); m_lpDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE ); m_lpDevice->SetRenderState( D3DRS_ZENABLE, FALSE ); } else { desc_text_surface = desc_backbuf; } if (bRTT && bRedrawText) do { // 1. change render target m_lpDevice->SetTexture(0, NULL); IDirect3DSurface9* pNewTarget = NULL; if (m_lpTextSurface->GetSurfaceLevel(0, &pNewTarget) != D3D_OK) { bRTT = 0; break; } if (m_lpDevice->SetRenderTarget(0, pNewTarget) != D3D_OK) { pNewTarget->Release(); bRTT = 0; break; } //m_lpDevice->SetDepthStencilSurface( ??? ); pNewTarget->Release(); m_lpDevice->SetTexture(0, NULL); // 2. clear to black //m_lpDevice->SetTexture(0, NULL); m_lpDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); m_lpDevice->SetVertexShader( NULL ); m_lpDevice->SetFVF( WFVERTEX_FORMAT ); m_lpDevice->SetPixelShader( NULL ); WFVERTEX v3[4]; if (bRedrawText==2) { DWORD clearcolor = m_msg[m_b][j].bgColor;//0xFF000000;// | ((rand()%32)<<16) | ((rand()%32)<<8) | ((rand()%32)); for (int i=0; i<4; i++) { v3[i].x = -1.0f + 2.0f*(i%2); v3[i].y = -1.0f + 2.0f*(i/2); v3[i].z = 0; v3[i].Diffuse = clearcolor; } m_lpDevice->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v3, sizeof(WFVERTEX)); } else { // 1. erase (draw black box over) any old text items deleted. // also, update the dirty rects; stuff that was ABOVE/BELOW these guys will need redrawn! // (..picture them staggered) for (int j=0; jDrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v3, sizeof(WFVERTEX)); //---------------------------------- // special case: // if something is erased, but it's totally inside a dark box, // then don't add it to the dirty rectangle. td_string* pDarkBox = (td_string*)m_msg[1-m_b][j].prev_dark_box_ptr; int add_to_dirty_rect = 1; while (pDarkBox && add_to_dirty_rect) { RECT t; UnionRect(&t, &pDarkBox->rect, &m_msg[1-m_b][j].rect); if (EqualRect(&t, &pDarkBox->rect)) add_to_dirty_rect = 0; pDarkBox = (td_string*)pDarkBox->prev_dark_box_ptr; } // also, update dirty rects // first, check to see if this shares area or a border w/any of the going dirty rects, // and if so, expand that dirty rect. if (add_to_dirty_rect) { int done = 0; RECT t; RECT r1 = m_msg[1-m_b][j].rect; RECT r2 = m_msg[1-m_b][j].rect; r2.top -= 1; r2.left -= 1; r2.right += 1; r2.bottom += 1; for (i=0; i dirty_rect[i].right) dx = r1.left - dirty_rect[i].right; else if (dirty_rect[i].left > r1.right) dx = dirty_rect[i].left - r1.right; if (r1.top > dirty_rect[i].bottom) dy = r1.top - dirty_rect[i].bottom; else if (dirty_rect[i].top > r1.bottom) dy = dirty_rect[i].top - r1.bottom; float dist = sqrtf((float)(dx*dx + dy*dy)); if (i==0 || dist < nearest_dist) { nearest_dist = dist; nearest_id = i; } } //...and expand it to include this one. UnionRect(&t, &r1, &dirty_rect[nearest_id]); dirty_rect[nearest_id] = t; } } } // 2. erase AND REDRAW any of *this* frame's text that falls in dirty rects // from erasures of *prev* frame's deleted text: for (j=0; jDrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v3, sizeof(WFVERTEX)); m_msg[m_b][j].deleted = 1; m_msg[m_b][j].added = 1; bRedrawText = 1; } } } } } while (0); // 3. render text to TEXT surface if (bRedrawText) { m_lpDevice->SetTexture(0, NULL); m_lpDevice->SetTexture(1, NULL); m_lpDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); m_lpDevice->SetVertexShader( NULL ); m_lpDevice->SetPixelShader( NULL ); m_lpDevice->SetFVF( WFVERTEX_FORMAT ); for (int i=0; iDrawTextW(NULL, m_msg[m_b][i].msg, -1, &m_msg[m_b][i].rect, m_msg[m_b][i].flags, m_msg[m_b][i].color); else if (m_msg[m_b][i].added || bRedrawText==2 || !bRTT) { WFVERTEX v3[4]; float x0 = -1.0f + 2.0f*m_msg[m_b][i].rect.left/(float)desc_text_surface.Width; float x1 = -1.0f + 2.0f*m_msg[m_b][i].rect.right/(float)desc_text_surface.Width; float y0 = -1.0f + 2.0f*m_msg[m_b][i].rect.top/(float)desc_text_surface.Height; float y1 = -1.0f + 2.0f*m_msg[m_b][i].rect.bottom/(float)desc_text_surface.Height; for (int k=0; k<4; k++) { v3[k].x = (k%2) ? x0 : x1; v3[k].y = (k/2) ? y0 : y1; v3[k].z = 0; v3[k].Diffuse = m_msg[m_b][i].bgColor;//0xFF303000; } m_lpDevice->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v3, sizeof(WFVERTEX)); } } if (bRTT) { // 4. restore render target if (bRedrawText) { m_lpDevice->SetTexture(0, NULL); m_lpDevice->SetRenderTarget( 0, pBackBuffer );//, pZBuffer ); //m_lpDevice->SetDepthStencilSurface( pZBuffer ); } // 5. blit text surface to backbuffer m_lpDevice->SetTexture(0, m_lpTextSurface); m_lpDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, m_blit_additively ? TRUE : FALSE); m_lpDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE); m_lpDevice->SetRenderState(D3DRS_DESTBLEND, m_blit_additively ? D3DBLEND_ONE : D3DBLEND_ZERO); m_lpDevice->SetVertexShader( NULL ); m_lpDevice->SetPixelShader( NULL ); m_lpDevice->SetFVF( SPRITEVERTEX_FORMAT ); SPRITEVERTEX v3[4]; ZeroMemory(v3, sizeof(SPRITEVERTEX)*4); float fx = desc_text_surface.Width / (float)desc_backbuf.Width ; float fy = desc_text_surface.Height / (float)desc_backbuf.Height; for (int i=0; i<4; i++) { v3[i].x = (i%2==0) ? -1 : -1 + 2*fx; v3[i].y = (i/2==0) ? -1 : -1 + 2*fy; v3[i].z = 0; v3[i].tu = ((i%2==0) ? 0.0f : 1.0f) + 0.5f/desc_text_surface.Width; // FIXES BLURRY TEXT even when bilinear interp. is on (which can't be turned off on all cards!) v3[i].tv = ((i/2==0) ? 0.0f : 1.0f) + 0.5f/desc_text_surface.Height; // FIXES BLURRY TEXT even when bilinear interp. is on (which can't be turned off on all cards!) v3[i].Diffuse = 0xFFFFFFFF; } DWORD oldblend[3]; //m_lpDevice->GetTextureStageState(0, D3DTSS_MAGFILTER, &oldblend[0]); //m_lpDevice->GetTextureStageState(1, D3DTSS_MINFILTER, &oldblend[1]); //m_lpDevice->GetTextureStageState(2, D3DTSS_MIPFILTER, &oldblend[2]); m_lpDevice->GetSamplerState(0, D3DSAMP_MAGFILTER, &oldblend[0]); m_lpDevice->GetSamplerState(1, D3DSAMP_MINFILTER, &oldblend[1]); m_lpDevice->GetSamplerState(2, D3DSAMP_MIPFILTER, &oldblend[2]); m_lpDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT); m_lpDevice->SetSamplerState(1, D3DSAMP_MINFILTER, D3DTEXF_POINT); m_lpDevice->SetSamplerState(2, D3DSAMP_MIPFILTER, D3DTEXF_POINT); m_lpDevice->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v3, sizeof(SPRITEVERTEX)); m_lpDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, oldblend[0]); m_lpDevice->SetSamplerState(1, D3DSAMP_MINFILTER, oldblend[1]); m_lpDevice->SetSamplerState(2, D3DSAMP_MIPFILTER, oldblend[2]); m_lpDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); } SafeRelease(pBackBuffer); //SafeRelease(pZBuffer); m_lpDevice->SetTexture(0, NULL); m_lpDevice->SetTexture(1, NULL); m_lpDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); m_lpDevice->SetVertexShader( NULL ); m_lpDevice->SetPixelShader( NULL ); m_lpDevice->SetFVF( SPRITEVERTEX_FORMAT ); //D3DXMATRIX ident; //D3DXMatrixIdentity(&ident); //m_lpDevice->SetTransform(D3DTS_PROJECTION, &ident); } // flip: m_b = 1 - m_b; ClearAll(); }