@@ -292,8 +292,6 @@ static float _colorMaxA(){
292292
293293color::color (int gray) { value = _makeColor ((float )gray, _colorMaxA ()).value ; }
294294color::color (int gray, int a) { value = _makeColor ((float )gray, (float )a).value ; }
295- color::color (int r, int g, int b) { value = _makeColor ((float )r,(float )g,(float )b,_colorMaxA ()).value ; }
296- color::color (int r,int g,int b,int a){ value = _makeColor ((float )r,(float )g,(float )b,(float )a).value ; }
297295color::color (float gray) { value = _makeColor (gray, _colorMaxA ()).value ; }
298296color::color (float gray, float a) { value = _makeColor (gray, a).value ; }
299297color::color (float r,float g,float b){ value = _makeColor (r,g,b,_colorMaxA ()).value ; }
@@ -579,6 +577,7 @@ void PApplet::fullScreen() {
579577 }
580578}
581579void PApplet::frameRate (int fps){currentFrameRate=fps;targetFrameTime=1.0 /fps;}
580+ void PApplet::vsync (bool on){if (gWindow ) glfwSwapInterval (on?1 :0 );}
582581void PApplet::noLoop (){looping=false ;}
583582void PApplet::loop () {looping=true ;}
584583void PApplet::redraw (){redrawOnce=true ;}
@@ -722,20 +721,24 @@ void PApplet::setBg(float r,float g,float b,float a){
722721 glClear (GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT );
723722}
724723void PApplet::background (float gray, float a) {
724+ _backgroundCalledThisFrame = true ;
725725 color c = makeColor (gray, a);
726726 unsigned int v = c.value ;
727727 setBg ((v>>16 &0xFF )/255 .f , (v>>8 &0xFF )/255 .f , (v&0xFF )/255 .f , (v>>24 &0xFF )/255 .f );
728728}
729729void PApplet::background (float r, float g, float b, float a) {
730+ _backgroundCalledThisFrame = true ;
730731 color c = makeColor (r, g, b, a);
731732 unsigned int v = c.value ;
732733 setBg ((v>>16 &0xFF )/255 .f , (v>>8 &0xFF )/255 .f , (v&0xFF )/255 .f , (v>>24 &0xFF )/255 .f );
733734}
734735void PApplet::background (color c) {
736+ _backgroundCalledThisFrame = true ;
735737 unsigned int v = c.value ;
736738 setBg ((v>>16 &0xFF )/255 .f , (v>>8 &0xFF )/255 .f , (v&0xFF )/255 .f , (v>>24 &0xFF )/255 .f );
737739}
738740void PApplet::background (const PImage& img) {
741+ _backgroundCalledThisFrame = true ;
739742 // Draw image as full-canvas background
740743 if (img.width == 0 || img.height == 0 ) return ;
741744 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
@@ -1900,19 +1903,28 @@ bool PApplet::loadTTFFile(const std::string& path) {
19001903
19011904void PApplet::bakeAtlas (float pixelSize) {
19021905 if (!g_ttf.loaded ) return ;
1903- if (std::fabs (pixelSize - g_ttf.bakeSize ) < 0 .5f ) return ;
1904- g_ttf.bakeSize = pixelSize;
1905- // Use a large atlas to guarantee all glyphs fit at any size
1906+ // Cache key: rounded to nearest 0.5px to avoid float noise
1907+ int cacheKey = (int )std::round (pixelSize * 2 .0f );
1908+ // Check cache — if already baked this size, just activate it
1909+ auto it = g_ttf.atlasCache .find (cacheKey);
1910+ if (it != g_ttf.atlasCache .end ()) {
1911+ g_ttf.current = &it->second ;
1912+ g_ttf.texID = g_ttf.current ->texID ;
1913+ g_ttf.chars = g_ttf.current ->chars ;
1914+ g_ttf.bakeSize = pixelSize;
1915+ return ;
1916+ }
1917+ // Not cached — bake a new atlas for this size
1918+ TTFAtlas atlas;
19061919 g_ttf.atlasW = 1024 ; g_ttf.atlasH = 1024 ;
19071920 std::vector<unsigned char > bitmap (g_ttf.atlasW * g_ttf.atlasH );
19081921 int ret = stbtt_BakeFontBitmap (
19091922 g_ttf.data .data (), 0 , pixelSize,
19101923 bitmap.data (), g_ttf.atlasW , g_ttf.atlasH ,
1911- 32 , 96 , g_ttf .chars );
1924+ 32 , 96 , atlas .chars );
19121925 if (ret == 0 ) { g_ttf.loaded = false ; return ; }
1913- // Upload as RGBA texture
1914- if (g_ttf.texID == 0 ) glGenTextures (1 , &g_ttf.texID );
1915- glBindTexture (GL_TEXTURE_2D , g_ttf.texID );
1926+ glGenTextures (1 , &atlas.texID );
1927+ glBindTexture (GL_TEXTURE_2D , atlas.texID );
19161928 glTexParameteri (GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER , GL_LINEAR );
19171929 glTexParameteri (GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_LINEAR );
19181930 std::vector<unsigned char > rgba (g_ttf.atlasW * g_ttf.atlasH * 4 );
@@ -1923,6 +1935,12 @@ void PApplet::bakeAtlas(float pixelSize) {
19231935 glTexImage2D (GL_TEXTURE_2D , 0 , GL_RGBA , g_ttf.atlasW , g_ttf.atlasH ,
19241936 0 , GL_RGBA , GL_UNSIGNED_BYTE , rgba.data ());
19251937 glBindTexture (GL_TEXTURE_2D , 0 );
1938+ // Store in cache
1939+ g_ttf.atlasCache [cacheKey] = std::move (atlas);
1940+ g_ttf.current = &g_ttf.atlasCache [cacheKey];
1941+ g_ttf.texID = g_ttf.current ->texID ;
1942+ g_ttf.chars = g_ttf.current ->chars ;
1943+ g_ttf.bakeSize = pixelSize;
19261944}
19271945#endif // PROCESSING_HAS_STB_TRUETYPE
19281946
@@ -2563,17 +2581,29 @@ static void mouse_btn_cb(GLFWwindow*, int btn, int action, int mods) {
25632581 p->g_currentMods = mods;
25642582 if (action == GLFW_PRESS ) {
25652583 p->_mousePressed = true ;
2584+ if (btn>=0 &&btn<8 ) p->mouseButtons [btn]=true ;
2585+ { int mb=(btn==GLFW_MOUSE_BUTTON_LEFT )?37 :(btn==GLFW_MOUSE_BUTTON_RIGHT )?39 :3 ;
2586+ if (mb<128 ) p->mouseDown [mb]=true ; }
25662587 if (btn == GLFW_MOUSE_BUTTON_LEFT ) p->mouseButton = LEFT ;
25672588 else if (btn == GLFW_MOUSE_BUTTON_RIGHT ) p->mouseButton = RIGHT ;
25682589 else p->mouseButton = CENTER ;
2590+ p->_eventDrewSomething =true ;
2591+ p->_eventDrewSomething =true ;
25692592 p->mousePressed ();
25702593 p->mouseWasPressed = true ;
25712594 } else if (action == GLFW_RELEASE ) {
2572- p->_mousePressed = false ;
2595+ if (btn>=0 &&btn<8 ) p->mouseButtons [btn]=false ;
2596+ { int mb=(btn==GLFW_MOUSE_BUTTON_LEFT )?37 :(btn==GLFW_MOUSE_BUTTON_RIGHT )?39 :3 ;
2597+ if (mb<128 ) p->mouseDown [mb]=false ; }
2598+ bool anyMouse=false ;
2599+ for (int i=0 ;i<8 ;i++) if (p->mouseButtons [i]){anyMouse=true ;break ;}
2600+ p->_mousePressed =anyMouse;
2601+ p->mouseButton = p->mouseButtons [GLFW_MOUSE_BUTTON_LEFT ] ? LEFT :
2602+ p->mouseButtons [GLFW_MOUSE_BUTTON_RIGHT ] ? RIGHT :
2603+ p->mouseButtons [GLFW_MOUSE_BUTTON_MIDDLE ] ? CENTER : -1 ;
25732604 p->mouseReleased ();
2574- if (p->mouseWasPressed && p->_onMouseClicked ) p->_onMouseClicked ();
2575- p->mouseWasPressed = false ;
2576- p->mouseButton = -1 ;
2605+ if (p->mouseWasPressed ) p->mouseClicked ();
2606+ p->mouseWasPressed =false ;
25772607 }
25782608}
25792609static void scroll_cb (GLFWwindow*,double ,double yoffset){
@@ -2591,6 +2621,7 @@ static void char_cb(GLFWwindow*, unsigned int codepoint) {
25912621 // Fire deferred keyPressed() now that p->key has the correct char value
25922622 if (p->g_pendingKeyPressed ) {
25932623 p->g_pendingKeyPressed = false ;
2624+ p->_eventDrewSomething =true ;
25942625 p->keyPressed ();
25952626 }
25962627 // keyTyped() -- Processing standard: only printable chars, no action keys
@@ -2645,11 +2676,62 @@ static int glfw_to_java_keycode(int k) {
26452676}
26462677
26472678// Current modifier state -- set from key_cb mods parameter (reliable on Windows)
2679+
2680+ // Map GLFW keycode -> Processing/Java keyCode constant
2681+ static int glfw_to_processing_keycode (int k) {
2682+ if (k >= GLFW_KEY_A && k <= GLFW_KEY_Z ) return ' A' + (k - GLFW_KEY_A ); // 65-90
2683+ if (k >= GLFW_KEY_0 && k <= GLFW_KEY_9 ) return ' 0' + (k - GLFW_KEY_0 ); // 48-57
2684+ switch (k) {
2685+ case GLFW_KEY_UP : return 38 ;
2686+ case GLFW_KEY_DOWN : return 40 ;
2687+ case GLFW_KEY_LEFT : return 37 ;
2688+ case GLFW_KEY_RIGHT : return 39 ;
2689+ case GLFW_KEY_LEFT_SHIFT :
2690+ case GLFW_KEY_RIGHT_SHIFT : return 16 ; // SHIFT
2691+ case GLFW_KEY_LEFT_CONTROL :
2692+ case GLFW_KEY_RIGHT_CONTROL :return 17 ; // CONTROL
2693+ case GLFW_KEY_LEFT_ALT :
2694+ case GLFW_KEY_RIGHT_ALT : return 18 ; // ALT
2695+ case GLFW_KEY_SPACE : return 32 ;
2696+ case GLFW_KEY_ENTER :
2697+ case GLFW_KEY_KP_ENTER : return 10 ;
2698+ case GLFW_KEY_BACKSPACE : return 8 ;
2699+ case GLFW_KEY_TAB : return 9 ;
2700+ case GLFW_KEY_ESCAPE : return 27 ;
2701+ case GLFW_KEY_DELETE : return 127 ;
2702+ case GLFW_KEY_HOME : return 36 ;
2703+ case GLFW_KEY_END : return 35 ;
2704+ case GLFW_KEY_PAGE_UP : return 33 ;
2705+ case GLFW_KEY_PAGE_DOWN : return 34 ;
2706+ case GLFW_KEY_INSERT : return 155 ;
2707+ case GLFW_KEY_F1 : return 112 ;
2708+ case GLFW_KEY_F2 : return 113 ;
2709+ case GLFW_KEY_F3 : return 114 ;
2710+ case GLFW_KEY_F4 : return 115 ;
2711+ case GLFW_KEY_F5 : return 116 ;
2712+ case GLFW_KEY_F6 : return 117 ;
2713+ case GLFW_KEY_F7 : return 118 ;
2714+ case GLFW_KEY_F8 : return 119 ;
2715+ case GLFW_KEY_F9 : return 120 ;
2716+ case GLFW_KEY_F10 : return 121 ;
2717+ case GLFW_KEY_F11 : return 122 ;
2718+ case GLFW_KEY_F12 : return 123 ;
2719+ default : return -1 ;
2720+ }
2721+ }
2722+
26482723static void key_cb (GLFWwindow* w, int k, int /* scancode*/ , int action, int mods) {
26492724 auto * p = PApplet::g_papplet; if (!p) return ;
26502725 p->g_currentMods = mods; // capture before callbacks fire
26512726 if (action == GLFW_PRESS || action == GLFW_REPEAT ) {
26522727 p->_keyPressed = true ;
2728+ if (k>=0 &&k<349 ) p->keys [k]=true ;
2729+ { int pk=glfw_to_processing_keycode (k); if (pk>=0 &&pk<256 ) {
2730+ p->keysDown [pk]=true ;
2731+ // Also store lowercase so K['w'] and K['W'] both work
2732+ if (pk>=' A' &&pk<=' Z' ) p->keysDown [pk+32 ]=true ;
2733+ if (pk>=' a' &&pk<=' z' ) p->keysDown [pk-32 ]=true ;
2734+ } }
26532735 p->g_pendingKeyPressed = false ; // reset for each new p->key event
26542736
26552737 // Translate GLFW code -> Java KeyEvent.VK_* value (Processing reference standard)
@@ -2703,14 +2785,23 @@ static void key_cb(GLFWwindow* w, int k, int /*scancode*/, int action, int mods)
27032785
27042786 // Fire keyPressed() now unless deferred to char_cb
27052787 if (!p->g_pendingKeyPressed ) {
2788+ if (action==GLFW_PRESS ) p->_eventDrewSomething =true ;
27062789 p->keyPressed ();
27072790 // Java Processing: ESC closes the sketch unless keyPressed() set p->key=0
27082791 if (p->key == (char )27 && p->gWindow )
27092792 glfwSetWindowShouldClose (p->gWindow , GLFW_TRUE );
27102793 }
27112794
27122795 } else if (action == GLFW_RELEASE ) {
2713- p->_keyPressed = false ;
2796+ if (k>=0 &&k<349 ) p->keys [k]=false ;
2797+ { int pk=glfw_to_processing_keycode (k); if (pk>=0 &&pk<256 ) {
2798+ p->keysDown [pk]=false ;
2799+ if (pk>=' A' &&pk<=' Z' ) p->keysDown [pk+32 ]=false ;
2800+ if (pk>=' a' &&pk<=' z' ) p->keysDown [pk-32 ]=false ;
2801+ } }
2802+ // check if any key still held
2803+ bool anyHeld=false ; for (int i=0 ;i<349 ;i++) if (p->keys [i]){anyHeld=true ;break ;}
2804+ p->_keyPressed =anyHeld;
27142805 p->g_currentMods = mods; // update on release too
27152806 p->keyReleased ();
27162807 }
@@ -2799,7 +2890,25 @@ void PApplet::run(){
27992890 gWindow =glfwCreateWindow (winWidth,winHeight,g_sketchName.c_str (),nullptr ,nullptr );
28002891 // Prevent freeze when dragging title bar on Windows
28012892 glfwSetWindowRefreshCallback (gWindow ,[](GLFWwindow* w){
2802- glClear (GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT );
2893+ // Originally just glClear()+swap "to prevent freeze when dragging the
2894+ // title bar on Windows" -- but this unconditionally WIPES the screen
2895+ // on every OS/WM-triggered refresh event (resize, focus change,
2896+ // workspace switch, tiling re-layout, etc.), with no attempt to
2897+ // redraw actual sketch content. On tiling WMs like i3 that send
2898+ // refresh events frequently, this is what made noLoop() sketches
2899+ // (which never swap again after their one real draw) appear to
2900+ // "show the image, then go blank a few frames later" -- the WM's
2901+ // very next refresh event was clearing it out from under them.
2902+ // Restoring from the persist FBO (the last frame we know is
2903+ // correct) instead of blank-clearing keeps the window responsive
2904+ // during the same blocking scenarios (e.g. title-bar drag) while
2905+ // never destroying real content.
2906+ auto * p = PApplet::g_papplet;
2907+ if (p && p->persistFBO ) {
2908+ p->restoreFromPersist ();
2909+ } else {
2910+ glClear (GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT );
2911+ }
28032912 glfwSwapBuffers (w);
28042913 });
28052914 if (!gWindow ){
@@ -2811,6 +2920,7 @@ void PApplet::run(){
28112920 glfwTerminate (); return ;
28122921 }
28132922 glfwMakeContextCurrent (gWindow );
2923+ glfwSwapInterval (1 ); // vsync on by default
28142924 GLenum glewErr = glewInit ();
28152925 if (glewErr != GLEW_OK ){
28162926#ifdef _WIN32
@@ -2966,6 +3076,19 @@ void PApplet::run(){
29663076 flushPoints (); // flush any pending points before swap
29673077 saveToPersist (); // save back buffer before swap for next frame restore
29683078 glfwSwapBuffers (gWindow );
3079+ // For noLoop() sketches, this is the ONLY draw that will ever happen --
3080+ // the main loop below never swaps again (looping||redrawOnce stays
3081+ // false forever after this point). With double buffering, a single
3082+ // swap leaves the correct image in only ONE of the two buffers; the
3083+ // other buffer still holds stale content from the settle loop above.
3084+ // If anything outside our control (window manager/compositor on
3085+ // tiling WMs like i3, focus changes, etc.) forces an implicit buffer
3086+ // flip later, that stale buffer becomes visible and the sketch
3087+ // appears to "go blank" after a few frames. Restoring from the
3088+ // persist FBO we just saved and swapping again primes BOTH buffers
3089+ // with the correct content, so no swap-parity assumption is needed.
3090+ restoreFromPersist ();
3091+ glfwSwapBuffers (gWindow );
29693092 }
29703093
29713094 redrawOnce = looping;
@@ -3022,7 +3145,7 @@ void PApplet::run(){
30223145 // Restore previous frame content from persist FBO.
30233146 // Sketches that call background() will overwrite this.
30243147 // Sketches that don't (like Continuous Lines) preserve their drawn content.
3025- restoreFromPersist ();
3148+ if (!_backgroundCalledThisFrame) restoreFromPersist ();
30263149 glClear (GL_DEPTH_BUFFER_BIT );
30273150 }
30283151
@@ -3075,14 +3198,19 @@ void PApplet::run(){
30753198 // Reset tint state each frame (Processing Java behavior)
30763199 doTint=false ; tintR=1 ; tintG=1 ; tintB=1 ; tintA=1 ;
30773200 glGetError ();
3201+ _backgroundCalledThisFrame = false ;
30783202 ++frameCount; this ->draw (); pmouseX=mouseX; pmouseY=mouseY; mouseDX=0 ; mouseDY=0 ;
30793203 glGetError (); // consume any GL errors
30803204 }
30813205
3082- saveToPersist ();
3206+ // Only save persist FBO if sketch uses persistence (no background() call)
3207+ if (!_backgroundCalledThisFrame) saveToPersist ();
30833208 glfwSwapBuffers (gWindow );
30843209 }
30853210 glfwPollEvents ();
3211+ // Only re-save persist if an event callback drew something.
3212+ // Checked via _eventDrewSomething flag set in keyPressed/mousePressed.
3213+ if (persistFBO && _eventDrewSomething) { saveToPersist (); _eventDrewSomething=false ; }
30863214 auto now=std::chrono::steady_clock::now ();
30873215 double elapsed=std::chrono::duration<double >(now-last).count ();
30883216 if (elapsed>0 ) measuredFrameRate=measuredFrameRate*0 .9f +(float )(1.0 /elapsed)*0 .1f ;
0 commit comments