2025-10-08 10:45:09 +02:00
# include "AsyncResourceManager.hpp"
# include "./resources/TextCmdResource.hpp"
# include "../helpers/Log.hpp"
# include "../helpers/MiscFunctions.hpp"
# include "../core/hyprlock.hpp"
# include "../config/ConfigManager.hpp"
# include <algorithm>
# include <cstdint>
# include <functional>
2025-10-09 11:59:54 +02:00
# include <hyprgraphics/resource/resources/ImageResource.hpp>
2025-10-14 14:47:11 +02:00
# include <hyprgraphics/resource/resources/StaticImageResource.hpp>
2025-10-09 11:59:54 +02:00
# include <hyprgraphics/image/Image.hpp>
2025-10-08 10:45:09 +02:00
# include <sys/eventfd.h>
# include <sys/poll.h>
using namespace Hyprgraphics ;
using namespace Hyprutils : : OS ;
static inline ResourceID scopeResourceID ( uint8_t scope , size_t in ) {
return ( in & ~ 0x0f ) | scope ;
}
ResourceID CAsyncResourceManager : : resourceIDForTextRequest ( const CTextResource : : STextResourceData & s ) {
// TODO: Currently ignores the font string and resulting distribution is probably not perfect.
const auto H1 = std : : hash < std : : string > { } ( s . text ) ;
const auto H2 = std : : hash < double > { } ( s . color . asRgb ( ) . r ) ;
const auto H3 = std : : hash < double > { } ( s . color . asRgb ( ) . g ) + s . fontSize ;
const auto H4 = std : : hash < double > { } ( s . color . asRgb ( ) . b ) + s . align ;
return scopeResourceID ( 1 , H1 ^ ( H2 < < 1 ) ^ ( H3 < < 2 ) ^ ( H4 < < 3 ) ) ;
}
ResourceID CAsyncResourceManager : : resourceIDForTextCmdRequest ( const CTextResource : : STextResourceData & s , size_t revision ) {
return scopeResourceID ( 2 , resourceIDForTextRequest ( s ) ^ ( revision < < 32 ) ) ;
}
ResourceID CAsyncResourceManager : : resourceIDForImageRequest ( const std : : string & path , size_t revision ) {
return scopeResourceID ( 3 , std : : hash < std : : string > { } ( path ) ^ ( revision < < 32 ) ) ;
}
2025-10-09 11:59:54 +02:00
ResourceID CAsyncResourceManager : : resourceIDForImageRequest ( const std : : span < const uint8_t > data ) {
auto sv = std : : string_view { reinterpret_cast < const char * > ( data . data ( ) ) , data . size ( ) } ;
return scopeResourceID ( 4 , std : : hash < std : : string_view > { } ( sv ) ) ;
}
2025-10-08 10:45:09 +02:00
ResourceID CAsyncResourceManager : : resourceIDForScreencopy ( const std : : string & port ) {
2025-10-09 11:59:54 +02:00
return scopeResourceID ( 5 , std : : hash < std : : string > { } ( port ) ) ;
2025-10-08 10:45:09 +02:00
}
ResourceID CAsyncResourceManager : : requestText ( const CTextResource : : STextResourceData & params , const AWP < IWidget > & widget ) {
const auto RESOURCEID = resourceIDForTextRequest ( params ) ;
if ( request ( RESOURCEID , widget ) ) {
Debug : : log ( TRACE , " Reusing text resource \" {} \" (resourceID: {}) " , params . text , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
return RESOURCEID ;
}
auto resource = makeAtomicShared < CTextResource > ( CTextResource : : STextResourceData { params } ) ;
CAtomicSharedPointer < IAsyncResource > resourceGeneric { resource } ;
Debug : : log ( TRACE , " Requesting text resource \" {} \" (resourceID: {}) " , params . text , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
enqueue ( RESOURCEID , resourceGeneric , widget ) ;
return RESOURCEID ;
}
ResourceID CAsyncResourceManager : : requestTextCmd ( const CTextResource : : STextResourceData & params , size_t revision , const AWP < IWidget > & widget ) {
const auto RESOURCEID = resourceIDForTextCmdRequest ( params , revision ) ;
if ( request ( RESOURCEID , widget ) ) {
Debug : : log ( TRACE , " Reusing text cmd resource \" {} \" revision {} (resourceID: {}) " , params . text , revision , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
return RESOURCEID ;
}
auto resource = makeAtomicShared < CTextCmdResource > ( CTextResource : : STextResourceData { params } ) ;
CAtomicSharedPointer < IAsyncResource > resourceGeneric { resource } ;
Debug : : log ( TRACE , " Requesting text cmd resource \" {} \" revision {} (resourceID: {}) " , params . text , revision , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
enqueue ( RESOURCEID , resourceGeneric , widget ) ;
return RESOURCEID ;
}
ResourceID CAsyncResourceManager : : requestImage ( const std : : string & path , size_t revision , const AWP < IWidget > & widget ) {
const auto RESOURCEID = resourceIDForImageRequest ( path , revision ) ;
if ( request ( RESOURCEID , widget ) ) {
Debug : : log ( TRACE , " Reusing image resource {} revision {} (resourceID: {}) " , path , revision , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
return RESOURCEID ;
}
auto resource = makeAtomicShared < CImageResource > ( absolutePath ( path , " " ) ) ;
CAtomicSharedPointer < IAsyncResource > resourceGeneric { resource } ;
Debug : : log ( TRACE , " Requesting image resource {} revision {} (resourceID: {}) " , path , revision , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
enqueue ( RESOURCEID , resourceGeneric , widget ) ;
return RESOURCEID ;
}
2025-10-09 11:59:54 +02:00
ResourceID CAsyncResourceManager : : requestImage ( const std : : span < const uint8_t > data , const AWP < IWidget > & widget ) {
const auto RESOURCEID = resourceIDForImageRequest ( data ) ;
if ( request ( RESOURCEID , widget ) ) {
Debug : : log ( TRACE , " Reusing image buffer (resourceID: {}) " , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
return RESOURCEID ;
}
2025-10-14 14:47:11 +02:00
auto resource = makeAtomicShared < CStaticImageResource > ( data , eImageFormat : : IMAGE_FORMAT_PNG ) ;
2025-10-09 11:59:54 +02:00
CAtomicSharedPointer < IAsyncResource > resourceGeneric { resource } ;
Debug : : log ( TRACE , " Requesting image (resourceID: {}) " , RESOURCEID , ( uintptr_t ) widget . get ( ) ) ;
enqueue ( RESOURCEID , resourceGeneric , widget ) ;
return RESOURCEID ;
}
2025-10-08 10:45:09 +02:00
ASP < CTexture > CAsyncResourceManager : : getAssetByID ( size_t id ) {
if ( ! m_assets . contains ( id ) )
return nullptr ;
return m_assets [ id ] . texture ;
}
void CAsyncResourceManager : : enqueueStaticAssets ( ) {
const auto CWIDGETS = g_pConfigManager - > getWidgetConfigs ( ) ;
for ( auto & c : CWIDGETS ) {
if ( c . type = = " background " | | c . type = = " image " ) {
std : : string path = std : : any_cast < Hyprlang : : STRING > ( c . values . at ( " path " ) ) ;
if ( path . empty ( ) | | path = = " screenshot " )
continue ;
requestImage ( path , 0 , nullptr ) ;
}
}
}
void CAsyncResourceManager : : enqueueScreencopyFrames ( ) {
if ( g_pHyprlock - > m_vOutputs . empty ( ) )
return ;
static const auto ANIMATIONSENABLED = g_pConfigManager - > getValue < Hyprlang : : INT > ( " animations:enabled " ) ;
const auto FADEINCFG = g_pConfigManager - > m_AnimationTree . getConfig ( " fadeIn " ) ;
const auto FADEOUTCFG = g_pConfigManager - > m_AnimationTree . getConfig ( " fadeOut " ) ;
const bool FADENEEDSSC = * ANIMATIONSENABLED & &
( ( FADEINCFG - > pValues & & FADEINCFG - > pValues - > internalEnabled ) | | // fadeIn or fadeOut enabled
( FADEOUTCFG - > pValues & & FADEOUTCFG - > pValues - > internalEnabled ) ) ;
const auto BGSCREENSHOT = std : : ranges : : any_of ( g_pConfigManager - > getWidgetConfigs ( ) , [ ] ( const auto & w ) { //
return w . type = = " background " & & std : : string { std : : any_cast < Hyprlang : : STRING > ( w . values . at ( " path " ) ) } = = " screenshot " ;
} ) ;
if ( ! BGSCREENSHOT & & ! FADENEEDSSC ) {
Debug : : log ( LOG , " Skipping screencopy " ) ;
return ;
}
for ( const auto & MON : g_pHyprlock - > m_vOutputs ) {
m_scFrames . emplace_back ( makeUnique < CScreencopyFrame > ( ) ) ;
auto * frame = m_scFrames . back ( ) . get ( ) ;
frame - > capture ( MON ) ;
m_assets . emplace ( frame - > m_resourceID , SPreloadedTexture { . texture = nullptr , . refs = 1 } ) ;
}
}
void CAsyncResourceManager : : screencopyToTexture ( const CScreencopyFrame & scFrame ) {
if ( ! scFrame . m_ready | | ! m_assets . contains ( scFrame . m_resourceID ) ) {
Debug : : log ( ERR , " Bogus call to CAsyncResourceManager::screencopyToTexture. This is a bug! " ) ;
return ;
}
m_assets [ scFrame . m_resourceID ] . texture = scFrame . m_asset ;
Debug : : log ( TRACE , " Done sc frame {} " , scFrame . m_resourceID ) ;
std : : erase_if ( m_scFrames , [ & scFrame ] ( const auto & f ) { return f . get ( ) = = & scFrame ; } ) ;
if ( m_scFrames . empty ( ) ) {
Debug : : log ( LOG , " Gathered all screencopy frames - removing dmabuf listeners " ) ;
g_pHyprlock - > removeDmabufListener ( ) ;
}
}
void CAsyncResourceManager : : gatherInitialResources ( wl_display * display ) {
const auto MAXDELAYMS = 2000 ; // 2 Seconds
const auto STARTGATHERTP = std : : chrono : : system_clock : : now ( ) ;
m_gatheredEventfd = CFileDescriptor { eventfd ( 0 , EFD_CLOEXEC ) } ;
int fdcount = 1 ;
pollfd pollfds [ 2 ] ;
pollfds [ 0 ] = {
. fd = wl_display_get_fd ( display ) ,
. events = POLLIN ,
} ;
if ( m_gatheredEventfd . isValid ( ) ) {
pollfds [ 1 ] = {
. fd = m_gatheredEventfd . get ( ) ,
. events = POLLIN ,
} ;
fdcount + + ;
}
bool gathered = false ;
while ( ! gathered ) {
wl_display_flush ( display ) ;
if ( wl_display_prepare_read ( display ) = = 0 ) {
if ( poll ( pollfds , fdcount , /* 100ms timeout */ 100 ) < 0 ) {
RASSERT ( errno = = EINTR , " [core] Polling fds failed with {} " , errno ) ;
wl_display_cancel_read ( display ) ;
continue ;
}
wl_display_read_events ( display ) ;
wl_display_dispatch_pending ( display ) ;
} else {
std : : this_thread : : sleep_for ( std : : chrono : : milliseconds ( 1 ) ) ;
wl_display_dispatch ( display ) ;
}
g_pHyprlock - > processTimers ( ) ;
if ( std : : chrono : : duration_cast < std : : chrono : : milliseconds > ( std : : chrono : : system_clock : : now ( ) - STARTGATHERTP ) . count ( ) > MAXDELAYMS ) {
Debug : : log ( WARN , " Gathering resources timed out after {} milliseconds. Backgrounds may be delayed and render `background:color` at first. " , MAXDELAYMS ) ;
break ;
}
gathered = m_resources . empty ( ) & & m_scFrames . empty ( ) ;
}
Debug : : log ( LOG , " Resources gathered after {} milliseconds " , std : : chrono : : duration_cast < std : : chrono : : milliseconds > ( std : : chrono : : system_clock : : now ( ) - STARTGATHERTP ) . count ( ) ) ;
}
bool CAsyncResourceManager : : checkIdPresent ( ResourceID id ) {
return m_assets . contains ( id ) ;
}
void CAsyncResourceManager : : unload ( ASP < CTexture > texture ) {
auto preload = std : : ranges : : find_if ( m_assets , [ texture ] ( const auto & a ) { return a . second . texture = = texture ; } ) ;
if ( preload = = m_assets . end ( ) )
return ;
preload - > second . refs - - ;
if ( preload - > second . refs = = 0 ) {
Debug : : log ( TRACE , " Releasing resourceID: {}! " , preload - > first ) ;
m_assets . erase ( preload - > first ) ;
}
}
void CAsyncResourceManager : : unloadById ( ResourceID id ) {
if ( ! m_assets . contains ( id ) )
return ;
m_assets [ id ] . refs - - ;
if ( m_assets [ id ] . refs = = 0 ) {
Debug : : log ( TRACE , " Releasing resourceID: {}! " , id ) ;
m_assets . erase ( id ) ;
}
}
bool CAsyncResourceManager : : request ( ResourceID id , const AWP < IWidget > & widget ) {
if ( ! m_assets . contains ( id ) ) {
// New asset!!
m_assets . emplace ( id , SPreloadedTexture { . texture = nullptr , . refs = 1 } ) ;
return false ;
}
m_assets [ id ] . refs + + ;
if ( m_assets [ id ] . texture ) {
// Asset already present. Dispatch the asset callback function.
const auto & TEXTURE = m_assets [ id ] . texture ;
if ( widget )
widget - > onAssetUpdate ( id , TEXTURE ) ;
// TODO: add a centalized mechanism to render in one place in the event loop to avoid duplicate render calls
g_pHyprlock - > addTimer ( std : : chrono : : milliseconds ( 0 ) , [ ] ( auto , auto ) { g_pHyprlock - > renderAllOutputs ( ) ; } , nullptr ) ;
} else if ( widget ) {
// Asset currently in-flight. Add the widget reference to in order for the callback to get dispatched later.
m_resourcesMutex . lock ( ) ;
if ( ! m_resources . contains ( id ) ) {
Debug : : log ( ERR , " In-flight resourceID: {} not found! This is a bug. " , id ) ;
m_resourcesMutex . unlock ( ) ;
return true ;
}
m_resources [ id ] . second . emplace_back ( widget ) ;
m_resourcesMutex . unlock ( ) ;
}
return true ;
}
void CAsyncResourceManager : : enqueue ( ResourceID resourceID , const ASP < IAsyncResource > & resource , const AWP < IWidget > & widget ) {
m_gatherer . enqueue ( resource ) ;
m_resourcesMutex . lock ( ) ;
if ( m_resources . contains ( resourceID ) )
Debug : : log ( ERR , " Resource already enqueued! This is a bug. " ) ;
m_resources [ resourceID ] = { resource , { widget } } ;
m_resourcesMutex . unlock ( ) ;
resource - > m_events . finished . listenStatic ( [ resourceID ] ( ) {
g_pHyprlock - > addTimer ( std : : chrono : : milliseconds ( 0 ) , [ ] ( auto , void * resourceID ) { g_asyncResourceManager - > onResourceFinished ( ( size_t ) resourceID ) ; } , ( void * ) resourceID ) ;
} ) ;
}
void CAsyncResourceManager : : onResourceFinished ( ResourceID id ) {
m_resourcesMutex . lock ( ) ;
if ( ! m_resources . contains ( id ) ) {
m_resourcesMutex . unlock ( ) ;
return ;
}
const auto RESOURCE = m_resources [ id ] . first ;
const auto WIDGETS = m_resources [ id ] . second ;
m_resources . erase ( id ) ;
m_resourcesMutex . unlock ( ) ;
if ( ! m_assets . contains ( id ) | | m_assets [ id ] . refs = = 0 ) // Not referenced? Drop it
return ;
if ( ! RESOURCE | | ! RESOURCE - > m_asset . cairoSurface )
return ;
Debug : : log ( TRACE , " Resource to texture id:{} " , id ) ;
const auto texture = makeAtomicShared < CTexture > ( ) ;
const cairo_status_t SURFACESTATUS = ( cairo_status_t ) RESOURCE - > m_asset . cairoSurface - > status ( ) ;
const auto CAIROFORMAT = cairo_image_surface_get_format ( RESOURCE - > m_asset . cairoSurface - > cairo ( ) ) ;
const GLint glIFormat = CAIROFORMAT = = CAIRO_FORMAT_RGB96F ? GL_RGB32F : GL_RGBA ;
const GLint glFormat = CAIROFORMAT = = CAIRO_FORMAT_RGB96F ? GL_RGB : GL_RGBA ;
const GLint glType = CAIROFORMAT = = CAIRO_FORMAT_RGB96F ? GL_FLOAT : GL_UNSIGNED_BYTE ;
if ( SURFACESTATUS ! = CAIRO_STATUS_SUCCESS ) {
Debug : : log ( ERR , " resourceID: {} invalid ({}) " , id , cairo_status_to_string ( SURFACESTATUS ) ) ;
texture - > m_iType = TEXTURE_INVALID ;
}
texture - > m_vSize = RESOURCE - > m_asset . pixelSize ;
texture - > allocate ( ) ;
glBindTexture ( GL_TEXTURE_2D , texture - > m_iTexID ) ;
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_LINEAR ) ;
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER , GL_LINEAR ) ;
if ( CAIROFORMAT ! = CAIRO_FORMAT_RGB96F ) {
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_SWIZZLE_R , GL_BLUE ) ;
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_SWIZZLE_B , GL_RED ) ;
}
glTexImage2D ( GL_TEXTURE_2D , 0 , glIFormat , texture - > m_vSize . x , texture - > m_vSize . y , 0 , glFormat , glType , RESOURCE - > m_asset . cairoSurface - > data ( ) ) ;
m_assets [ id ] . texture = texture ;
for ( const auto & widget : WIDGETS ) {
if ( widget )
widget - > onAssetUpdate ( id , texture ) ;
}
g_pHyprlock - > renderAllOutputs ( ) ;
if ( ! m_gathered & & ! g_pHyprlock - > m_bImmediateRender ) {
m_resourcesMutex . lock ( ) ;
if ( m_resources . empty ( ) ) {
m_gathered = true ;
if ( m_gatheredEventfd . isValid ( ) )
eventfd_write ( m_gatheredEventfd . get ( ) , 1 ) ;
m_gatheredEventfd . reset ( ) ;
}
m_resourcesMutex . unlock ( ) ;
}
}