/* * Copyright (C) 2005-2018 Team Kodi * This file is part of Kodi - https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSES/README.md for more information. */ #include "GUITexture.h" #include "GUILargeTextureManager.h" #include "TextureManager.h" #include "utils/MathUtils.h" #include "utils/StringUtils.h" #include "windowing/GraphicContext.h" #include CreateGUITextureFunc CGUITexture::m_createGUITextureFunc; DrawQuadFunc CGUITexture::m_drawQuadFunc; CTextureInfo::CTextureInfo() { orientation = 0; useLarge = false; } CTextureInfo::CTextureInfo(const std::string &file): filename(file) { orientation = 0; useLarge = false; } void CGUITexture::Register(const CreateGUITextureFunc& createFunction, const DrawQuadFunc& drawQuadFunction) { m_createGUITextureFunc = createFunction; m_drawQuadFunc = drawQuadFunction; } CGUITexture* CGUITexture::CreateTexture( float posX, float posY, float width, float height, const CTextureInfo& texture) { if (!m_createGUITextureFunc) throw std::runtime_error( "No GUITexture Create function available. Did you forget to register?"); return m_createGUITextureFunc(posX, posY, width, height, texture); } void CGUITexture::DrawQuad(const CRect& coords, UTILS::COLOR::Color color, CTexture* texture, const CRect* texCoords) { if (!m_drawQuadFunc) throw std::runtime_error( "No GUITexture DrawQuad function available. Did you forget to register?"); m_drawQuadFunc(coords, color, texture, texCoords); } CGUITexture::CGUITexture( float posX, float posY, float width, float height, const CTextureInfo& texture) : m_height(height), m_info(texture) { m_posX = posX; m_posY = posY; m_width = width; // defaults m_visible = true; m_diffuseColor = 0xffffffff; m_alpha = 0xff; m_vertex.SetRect(m_posX, m_posY, m_posX + m_width, m_posY + m_height); m_frameWidth = 0; m_frameHeight = 0; m_texCoordsScaleU = 1.0f; m_texCoordsScaleV = 1.0f; m_diffuseU = 1.0f; m_diffuseV = 1.0f; m_diffuseScaleU = 1.0f; m_diffuseScaleV = 1.0f; // anim gifs ResetAnimState(); m_allocateDynamically = false; m_isAllocated = NO; m_invalid = true; m_use_cache = true; } CGUITexture::CGUITexture(const CGUITexture& right) : m_visible(right.m_visible), m_diffuseColor(right.m_diffuseColor), m_posX(right.m_posX), m_posY(right.m_posY), m_width(right.m_width), m_height(right.m_height), m_use_cache(right.m_use_cache), m_alpha(right.m_alpha), m_allocateDynamically(right.m_allocateDynamically), m_info(right.m_info), m_aspect(right.m_aspect) { // defaults m_vertex.SetRect(m_posX, m_posY, m_posX + m_width, m_posY + m_height); m_frameWidth = 0; m_frameHeight = 0; m_texCoordsScaleU = 1.0f; m_texCoordsScaleV = 1.0f; m_diffuseU = 1.0f; m_diffuseV = 1.0f; m_diffuseScaleU = 1.0f; m_diffuseScaleV = 1.0f; ResetAnimState(); m_isAllocated = NO; m_invalid = true; } bool CGUITexture::AllocateOnDemand() { if (m_visible) { // visible, so make sure we're allocated if (!IsAllocated() || (m_isAllocated == LARGE && !m_texture.size())) return AllocResources(); } else { // hidden, so deallocate as applicable if (m_allocateDynamically && IsAllocated()) FreeResources(); // reset animated textures (animgifs) ResetAnimState(); } return false; } bool CGUITexture::Process(unsigned int currentTime) { bool changed = false; // check if we need to allocate our resources changed |= AllocateOnDemand(); if (m_texture.size() > 1) changed |= UpdateAnimFrame(currentTime); if (m_invalid) changed |= CalculateSize(); if (m_isAllocated) changed |= !ReadyToRender(); return changed; } void CGUITexture::Render() { if (!m_visible || !m_texture.size()) return; // see if we need to clip the image if (m_vertex.Width() > m_width || m_vertex.Height() > m_height) { if (!CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height)) return; } // set our draw color #define MIX_ALPHA(a,c) (((a * (c >> 24)) / 255) << 24) | (c & 0x00ffffff) // diffuse color UTILS::COLOR::Color color = (m_info.diffuseColor) ? (UTILS::COLOR::Color)m_info.diffuseColor : m_diffuseColor; if (m_alpha != 0xFF) color = MIX_ALPHA(m_alpha, color); color = CServiceBroker::GetWinSystem()->GetGfxContext().MergeColor(color); // setup our renderer Begin(color); // compute the texture coordinates float u1, u2, u3, v1, v2, v3; u1 = m_info.border.x1; u2 = m_frameWidth - m_info.border.x2; u3 = m_frameWidth; v1 = m_info.border.y1; v2 = m_frameHeight - m_info.border.y2; v3 = m_frameHeight; if (!m_texture.m_texCoordsArePixels) { u1 *= m_texCoordsScaleU; u2 *= m_texCoordsScaleU; u3 *= m_texCoordsScaleU; v1 *= m_texCoordsScaleV; v2 *= m_texCoordsScaleV; v3 *= m_texCoordsScaleV; } //! @todo The diffuse coloring applies to all vertices, which will //! look weird for stuff with borders, as will the -ve height/width //! for flipping // left segment (0,0,u1,v3) if (m_info.border.x1) { if (m_info.border.y1) Render(m_vertex.x1, m_vertex.y1, m_vertex.x1 + m_info.border.x1, m_vertex.y1 + m_info.border.y1, 0, 0, u1, v1, u3, v3); Render(m_vertex.x1, m_vertex.y1 + m_info.border.y1, m_vertex.x1 + m_info.border.x1, m_vertex.y2 - m_info.border.y2, 0, v1, u1, v2, u3, v3); if (m_info.border.y2) Render(m_vertex.x1, m_vertex.y2 - m_info.border.y2, m_vertex.x1 + m_info.border.x1, m_vertex.y2, 0, v2, u1, v3, u3, v3); } // middle segment (u1,0,u2,v3) if (m_info.border.y1) Render(m_vertex.x1 + m_info.border.x1, m_vertex.y1, m_vertex.x2 - m_info.border.x2, m_vertex.y1 + m_info.border.y1, u1, 0, u2, v1, u3, v3); if (m_info.m_infill) Render(m_vertex.x1 + m_info.border.x1, m_vertex.y1 + m_info.border.y1, m_vertex.x2 - m_info.border.x2, m_vertex.y2 - m_info.border.y2, u1, v1, u2, v2, u3, v3); if (m_info.border.y2) Render(m_vertex.x1 + m_info.border.x1, m_vertex.y2 - m_info.border.y2, m_vertex.x2 - m_info.border.x2, m_vertex.y2, u1, v2, u2, v3, u3, v3); // right segment if (m_info.border.x2) { // have a left border if (m_info.border.y1) Render(m_vertex.x2 - m_info.border.x2, m_vertex.y1, m_vertex.x2, m_vertex.y1 + m_info.border.y1, u2, 0, u3, v1, u3, v3); Render(m_vertex.x2 - m_info.border.x2, m_vertex.y1 + m_info.border.y1, m_vertex.x2, m_vertex.y2 - m_info.border.y2, u2, v1, u3, v2, u3, v3); if (m_info.border.y2) Render(m_vertex.x2 - m_info.border.x2, m_vertex.y2 - m_info.border.y2, m_vertex.x2, m_vertex.y2, u2, v2, u3, v3, u3, v3); } // close off our renderer End(); if (m_vertex.Width() > m_width || m_vertex.Height() > m_height) CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); } void CGUITexture::Render(float left, float top, float right, float bottom, float u1, float v1, float u2, float v2, float u3, float v3) { CRect diffuse(u1, v1, u2, v2); CRect texture(u1, v1, u2, v2); CRect vertex(left, top, right, bottom); CServiceBroker::GetWinSystem()->GetGfxContext().ClipRect(vertex, texture, m_diffuse.size() ? &diffuse : NULL); if (vertex.IsEmpty()) return; // nothing to render int orientation = GetOrientation(); OrientateTexture(texture, u3, v3, orientation); if (m_diffuse.size()) { // flip the texture as necessary. Diffuse just gets flipped according to m_info.orientation. // Main texture gets flipped according to GetOrientation(). diffuse.x1 *= m_diffuseScaleU / u3; diffuse.x2 *= m_diffuseScaleU / u3; diffuse.y1 *= m_diffuseScaleV / v3; diffuse.y2 *= m_diffuseScaleV / v3; diffuse += m_diffuseOffset; OrientateTexture(diffuse, m_diffuseU, m_diffuseV, m_info.orientation); } float x[4], y[4], z[4]; #define ROUND_TO_PIXEL(x) static_cast(MathUtils::round_int(static_cast(x))) x[0] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x1, vertex.y1)); y[0] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x1, vertex.y1)); z[0] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x1, vertex.y1)); x[1] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x2, vertex.y1)); y[1] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x2, vertex.y1)); z[1] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x2, vertex.y1)); x[2] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x2, vertex.y2)); y[2] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x2, vertex.y2)); z[2] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x2, vertex.y2)); x[3] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x1, vertex.y2)); y[3] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x1, vertex.y2)); z[3] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x1, vertex.y2)); if (y[2] == y[0]) y[2] += 1.0f; if (x[2] == x[0]) x[2] += 1.0f; if (y[3] == y[1]) y[3] += 1.0f; if (x[3] == x[1]) x[3] += 1.0f; Draw(x, y, z, texture, diffuse, orientation); } bool CGUITexture::AllocResources() { if (m_info.filename.empty()) return false; if (m_texture.size()) return false; // already have our texture // reset our animstate ResetAnimState(); bool changed = false; bool useLarge = m_info.useLarge || !CServiceBroker::GetGUI()->GetTextureManager().CanLoad(m_info.filename); if (useLarge) { // we want to use the large image loader, but we first check for bundled textures if (!IsAllocated()) { CTextureArray texture; texture = CServiceBroker::GetGUI()->GetTextureManager().Load(m_info.filename, true); if (texture.size()) { m_isAllocated = NORMAL; m_texture = texture; changed = true; } } if (m_isAllocated != NORMAL) { // use our large image background loader CTextureArray texture; if (CServiceBroker::GetGUI()->GetLargeTextureManager().GetImage(m_info.filename, texture, !IsAllocated(), m_use_cache)) { m_isAllocated = LARGE; if (!texture.size()) // not ready as yet return false; m_texture = texture; changed = true; } else m_isAllocated = LARGE_FAILED; } } else if (!IsAllocated()) { CTextureArray texture = CServiceBroker::GetGUI()->GetTextureManager().Load(m_info.filename); // set allocated to true even if we couldn't load the image to save // us hitting the disk every frame m_isAllocated = texture.size() ? NORMAL : NORMAL_FAILED; if (!texture.size()) return false; m_texture = texture; changed = true; } m_frameWidth = (float)m_texture.m_width; m_frameHeight = (float)m_texture.m_height; // load the diffuse texture (if necessary) if (!m_info.diffuse.empty()) { m_diffuse = CServiceBroker::GetGUI()->GetTextureManager().Load(m_info.diffuse); } CalculateSize(); // call our implementation Allocate(); return changed; } bool CGUITexture::CalculateSize() { if (m_currentFrame >= m_texture.size()) return false; m_texCoordsScaleU = 1.0f / m_texture.m_texWidth; m_texCoordsScaleV = 1.0f / m_texture.m_texHeight; if (m_width == 0) m_width = m_frameWidth; if (m_height == 0) m_height = m_frameHeight; float newPosX = m_posX; float newPosY = m_posY; float newWidth = m_width; float newHeight = m_height; if (m_aspect.ratio != CAspectRatio::AR_STRETCH && m_frameWidth && m_frameHeight) { // to get the pixel ratio, we must use the SCALED output sizes float pixelRatio = CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio(); float fSourceFrameRatio = m_frameWidth / m_frameHeight; if (GetOrientation() & 4) fSourceFrameRatio = m_frameHeight / m_frameWidth; float fOutputFrameRatio = fSourceFrameRatio / pixelRatio; // maximize the width newHeight = m_width / fOutputFrameRatio; if ((m_aspect.ratio == CAspectRatio::AR_SCALE && newHeight < m_height) || (m_aspect.ratio == CAspectRatio::AR_KEEP && newHeight > m_height)) { newHeight = m_height; newWidth = newHeight * fOutputFrameRatio; } if (m_aspect.ratio == CAspectRatio::AR_CENTER) { // keep original size + center newWidth = m_frameWidth / sqrt(pixelRatio); newHeight = m_frameHeight * sqrt(pixelRatio); } if (m_aspect.align & ASPECT_ALIGN_LEFT) newPosX = m_posX; else if (m_aspect.align & ASPECT_ALIGN_RIGHT) newPosX = m_posX + m_width - newWidth; else newPosX = m_posX + (m_width - newWidth) * 0.5f; if (m_aspect.align & ASPECT_ALIGNY_TOP) newPosY = m_posY; else if (m_aspect.align & ASPECT_ALIGNY_BOTTOM) newPosY = m_posY + m_height - newHeight; else newPosY = m_posY + (m_height - newHeight) * 0.5f; } m_vertex.SetRect(newPosX, newPosY, newPosX + newWidth, newPosY + newHeight); // scale the diffuse coords as well if (m_diffuse.size()) { // calculate scaling for the texcoords if (m_diffuse.m_texCoordsArePixels) { m_diffuseU = float(m_diffuse.m_width); m_diffuseV = float(m_diffuse.m_height); } else { m_diffuseU = float(m_diffuse.m_width) / float(m_diffuse.m_texWidth); m_diffuseV = float(m_diffuse.m_height) / float(m_diffuse.m_texHeight); } if (m_aspect.scaleDiffuse) { m_diffuseScaleU = m_diffuseU; m_diffuseScaleV = m_diffuseV; m_diffuseOffset = CPoint(0,0); } else // stretching diffuse { // scale diffuse up or down to match output rect size, rather than image size //(m_fX, mfY) -> (m_fX + m_fNW, m_fY + m_fNH) //(0,0) -> (m_fU*m_diffuseScaleU, m_fV*m_diffuseScaleV) // x = u/(m_fU*m_diffuseScaleU)*m_fNW + m_fX // -> u = (m_posX - m_fX) * m_fU * m_diffuseScaleU / m_fNW m_diffuseScaleU = m_diffuseU * m_vertex.Width() / m_width; m_diffuseScaleV = m_diffuseV * m_vertex.Height() / m_height; m_diffuseOffset = CPoint((m_vertex.x1 - m_posX) / m_vertex.Width() * m_diffuseScaleU, (m_vertex.y1 - m_posY) / m_vertex.Height() * m_diffuseScaleV); } } m_invalid = false; return true; } void CGUITexture::FreeResources(bool immediately /* = false */) { if (m_isAllocated == LARGE || m_isAllocated == LARGE_FAILED) CServiceBroker::GetGUI()->GetLargeTextureManager().ReleaseImage(m_info.filename, immediately || (m_isAllocated == LARGE_FAILED)); else if (m_isAllocated == NORMAL && m_texture.size()) CServiceBroker::GetGUI()->GetTextureManager().ReleaseTexture(m_info.filename, immediately); if (m_diffuse.size()) CServiceBroker::GetGUI()->GetTextureManager().ReleaseTexture(m_info.diffuse, immediately); m_diffuse.Reset(); m_texture.Reset(); ResetAnimState(); m_texCoordsScaleU = 1.0f; m_texCoordsScaleV = 1.0f; // call our implementation Free(); m_isAllocated = NO; } void CGUITexture::DynamicResourceAlloc(bool allocateDynamically) { m_allocateDynamically = allocateDynamically; } void CGUITexture::SetInvalid() { m_invalid = true; } bool CGUITexture::UpdateAnimFrame(unsigned int currentTime) { bool changed = false; unsigned int delay = m_texture.m_delays[m_currentFrame]; if (m_lasttime == 0) { m_lasttime = currentTime; } else { if ((currentTime - m_lasttime) >= delay) { if (m_currentFrame + 1 >= m_texture.size()) { if (m_texture.m_loops > 0) { if (m_currentLoop + 1 < m_texture.m_loops) { m_currentLoop++; m_currentFrame = 0; m_lasttime = currentTime; changed = true; } } else { // 0 == loop forever m_currentFrame = 0; m_lasttime = currentTime; changed = true; } } else { m_currentFrame++; m_lasttime = currentTime; changed = true; } } } return changed; } bool CGUITexture::SetVisible(bool visible) { bool changed = m_visible != visible; m_visible = visible; return changed; } bool CGUITexture::SetAlpha(unsigned char alpha) { bool changed = m_alpha != alpha; m_alpha = alpha; return changed; } bool CGUITexture::SetDiffuseColor(UTILS::COLOR::Color color, const CGUIListItem* item /* = nullptr */) { bool changed = m_diffuseColor != color; m_diffuseColor = color; changed |= m_info.diffuseColor.Update(item); return changed; } bool CGUITexture::ReadyToRender() const { return m_texture.size() > 0; } void CGUITexture::OrientateTexture(CRect& rect, float width, float height, int orientation) { switch (orientation & 3) { case 0: // default break; case 1: // flip in X direction rect.x1 = width - rect.x1; rect.x2 = width - rect.x2; break; case 2: // rotate 180 degrees rect.x1 = width - rect.x1; rect.x2 = width - rect.x2; rect.y1 = height - rect.y1; rect.y2 = height - rect.y2; break; case 3: // flip in Y direction rect.y1 = height - rect.y1; rect.y2 = height - rect.y2; break; } if (orientation & 4) { // we need to swap x and y coordinates but only within the width,height block float temp = rect.x1; rect.x1 = rect.y1 * width/height; rect.y1 = temp * height/width; temp = rect.x2; rect.x2 = rect.y2 * width/height; rect.y2 = temp * height/width; } } void CGUITexture::ResetAnimState() { m_lasttime = 0; m_currentFrame = 0; m_currentLoop = 0; } bool CGUITexture::SetWidth(float width) { if (width < m_info.border.x1 + m_info.border.x2) width = m_info.border.x1 + m_info.border.x2; if (m_width != width) { m_width = width; m_invalid = true; return true; } else return false; } bool CGUITexture::SetHeight(float height) { if (height < m_info.border.y1 + m_info.border.y2) height = m_info.border.y1 + m_info.border.y2; if (m_height != height) { m_height = height; m_invalid = true; return true; } else return false; } bool CGUITexture::SetPosition(float posX, float posY) { if (m_posX != posX || m_posY != posY) { m_posX = posX; m_posY = posY; m_invalid = true; return true; } else return false; } bool CGUITexture::SetAspectRatio(const CAspectRatio& aspect) { if (m_aspect != aspect) { m_aspect = aspect; m_invalid = true; return true; } else return false; } bool CGUITexture::SetFileName(const std::string& filename) { if (m_info.filename == filename) return false; // Don't completely free resources here - we may be just changing // filenames mid-animation FreeResources(); m_info.filename = filename; // disable large loader and cache for gifs if (StringUtils::EndsWithNoCase(m_info.filename, ".gif")) { m_info.useLarge = false; SetUseCache(false); } // Don't allocate resources here as this is done at render time return true; } void CGUITexture::SetUseCache(const bool useCache) { m_use_cache = useCache; } int CGUITexture::GetOrientation() const { // multiply our orientations static char orient_table[] = { 0, 1, 2, 3, 4, 5, 6, 7, 1, 0, 3, 2, 5, 4, 7, 6, 2, 3, 0, 1, 6, 7, 4, 5, 3, 2, 1, 0, 7, 6, 5, 4, 4, 7, 6, 5, 0, 3, 2, 1, 5, 6, 7, 4, 1, 2, 3, 0, 6, 5, 4, 7, 2, 1, 0, 3, 7, 4, 5, 6, 3, 0, 1, 2 }; return (int)orient_table[8 * m_info.orientation + m_texture.m_orientation]; }