=== modified file 'build.gradle'
--- old/build.gradle	2026-08-25 04:59:26 +0000
+++ new/build.gradle	2026-09-25 06:45:17 +0000
@@ -232,6 +232,7 @@
 ** 121 TG  20260805 Aligned commons-io, commons-lang3, commons-text and commons-configuration2
 **                  across the client, server and convert configurations.
 ** 122 TG  20260825 Upgraded to the latest FWD-H2-1.65.
+** 123 TG  20260924 ASM is a client and server dependency, used for the generated widget config accessors.
 */
 
 /*
@@ -778,6 +779,23 @@
 }
 
 // ============================================================================
+// Widget config accessors
+// ============================================================================
+tasks.register('generateConfigAccessors', JavaExec) {
+    dependsOn 'aspectjWeave'
+    def outDir = "${layout.buildDirectory.get()}/config-accessors"
+    outputs.dir(outDir)
+    classpath = files("${layout.buildDirectory.get()}/classes.aop") +
+                configurations.fwdAllRuntime +
+                configurations.aspectjCompile
+    mainClass.set('com.goldencode.p2j.ui.ConfigAccessorGenerator')
+    args outDir
+    doFirst {
+        delete outDir
+    }
+}
+
+// ============================================================================
 // Lifecycle task: gates all consumers of build/classes.aop/.
 // Any task that writes to classes.aop should be added as a dependency here.
 // ============================================================================
@@ -824,6 +842,7 @@
     from("${layout.buildDirectory.get()}/admin") {
         include 'com/goldencode/p2j/admin/**'
     }
+    from(tasks.named('generateConfigAccessors'))
     // Include the empty SPI directory to match reference
     from("${layout.buildDirectory.get()}/classes.aop") {
         include 'com/goldencode/p2j/spi/'
@@ -1638,6 +1657,7 @@
     fwdClientServer group: 'org.reflections', name: 'reflections', version: '0.10.2'
     fwdClientServer group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.14'
     fwdClientServer group: 'org.javassist', name: 'javassist', version: '3.30.2-GA'
+    fwdClientServer group: 'org.ow2.asm', name: 'asm', version: '9.8'
     fwdClientServer group: 'org.apache.bcel', name: 'bcel', version: '6.10.0'
     fwdClientServer group: 'com.webauthn4j', name: 'webauthn4j-core', version: '0.31.7.RELEASE'
     fwdClientServer group: 'com.google.guava', name: 'guava-gwt', version: '33.4.6-jre'
@@ -1656,7 +1676,6 @@
     }
     
     fwdServer group: 'org.postgresql', name: 'postgresql', version: '42.7.8'
-    fwdServer group: 'org.ow2.asm', name: 'asm', version: '9.8'
     fwdServer group: 'com.mchange', name: 'c3p0', version: '0.14.1'
     fwdServer group: 'cglib', name: 'cglib', version: '3.3.0'
     fwdServer group: 'net.sf.ehcache', name: 'ehcache-core', version: '2.6.11'

=== modified file 'src/com/goldencode/p2j/aspects/ui/ConfigFieldSetterAspect.java'
--- old/src/com/goldencode/p2j/aspects/ui/ConfigFieldSetterAspect.java	2026-08-04 06:56:06 +0000
+++ new/src/com/goldencode/p2j/aspects/ui/ConfigFieldSetterAspect.java	2026-09-25 09:30:43 +0000
@@ -19,6 +19,7 @@
 **                  of concrete class. This is needed to allow client-side code to be executed in
 **                  the server context.
 ** 010 CA  20260804 Changes to allow the FWD Client to run in the same JVM as the FWD server.
+** 011 TG  20260925 Configs with a virtual ID are skipped; the config caches its tracking config manager.
 */ 
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -130,7 +131,13 @@
            "&& !within(com.goldencode.p2j.ui.*Entity) ")
    public void beforeSetField(WidgetConfig cfg, JoinPoint.StaticPart sp)
    {
+      if (WidgetId.virtualWidget(cfg.id))
+      {
+         // not tracked, see ConfigManager.trackConfig
+         return;
+      }
+      
       String field = sp.getSignature().getName();
-      ClientConfigManager.getInstance().addDirtyConfig(cfg, field);
+      cfg.trackingManager().addDirtyConfig(cfg, field);
    }
 }

=== modified file 'src/com/goldencode/p2j/aspects/ui/LocationCacheAspect.java'
--- old/src/com/goldencode/p2j/aspects/ui/LocationCacheAspect.java	2026-08-04 09:10:20 +0000
+++ new/src/com/goldencode/p2j/aspects/ui/LocationCacheAspect.java	2026-09-25 08:51:10 +0000
@@ -9,6 +9,7 @@
 ** 002 CA  20180209 A fix - iajc compiler requires absolute names in the aspect definition. 
 ** 003 HC  20240806 Added support for displaying frame widget in an overlay window.
 ** 004 CA  20260804 Changes to allow the FWD Client to run in the same JVM as the FWD server.
+** 005 TG  20260925 The layout depth is a mutable per-context counter, read once per join point.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -88,11 +89,11 @@
     * Tracks the depth of layout-related nested API calls.  When is zero, it will force the
     * {@link AbstractWidget#reflectLocationChanged() cached values to be reset.}
     */
-   private static final ContextLocal<Integer> local = new ContextLocal<>()
+   private static final ContextLocal<int[]> local = new ContextLocal<>()
    {
-      protected Integer initialValue() 
+      protected int[] initialValue() 
       {
-         return Integer.valueOf(0);
+         return new int[1];
       }
    };
    
@@ -118,29 +119,25 @@
                                                Container<?> container)
    throws Throwable
    {
-      int layoutDepth = local.get();
+      int[] layoutDepth = local.get();
       AbstractWidget<?> widget = (AbstractWidget<?>) container;
       
-      if (layoutDepth == 0)
+      if (layoutDepth[0] == 0)
       {
          // ensure nothing is cached
          widget.reflectLocationChanged();
       }
 
-      layoutDepth += 1;
+      layoutDepth[0] += 1;
       
       try
       {
-         local.set(layoutDepth);
-
          return thisJoinPoint.proceed();
       }
       finally
       {
          // no need to reflectLocationChange, as nothing gets cached while the depth is non-zero
-         layoutDepth -= 1;
-
-         local.set(layoutDepth);
+         layoutDepth[0] -= 1;
       }
    }
 
@@ -163,30 +160,26 @@
    public Object disableLocationCacheInContainer(ProceedingJoinPoint thisJoinPoint)
    throws Throwable
    {
-      int layoutDepth = local.get();
+      int[] layoutDepth = local.get();
       
       AbstractWidget<?> widget = (AbstractWidget<?>) thisJoinPoint.getThis();
 
-      if (layoutDepth == 0)
+      if (layoutDepth[0] == 0)
       {
          // ensure nothing is cached
          widget.reflectLocationChanged();
       }
 
-      layoutDepth += 1;
-
+      layoutDepth[0] += 1;
+      
       try
       {
-         local.set(layoutDepth);
-         
          return thisJoinPoint.proceed();
       }
       finally
       {
          // no need to reflectLocationChange, as nothing gets cached while the depth is non-zero
-         layoutDepth -= 1;
-         
-         local.set(layoutDepth);
+         layoutDepth[0] -= 1;
       }
    }
    
@@ -208,7 +201,7 @@
    public Object cacheScreenLocation(ProceedingJoinPoint thisJoinPoint)
    throws Throwable
    {
-      int layoutDepth = local.get();
+      int layoutDepth = local.get()[0];
       
       AbstractWidget<?> widget = (AbstractWidget<?>) thisJoinPoint.getThis();
 
@@ -249,7 +242,7 @@
    public Object cacheScreenPhysicalLocation(ProceedingJoinPoint thisJoinPoint)
    throws Throwable
    {
-      int layoutDepth = local.get();
+      int layoutDepth = local.get()[0];
       
       AbstractWidget<?> widget = (AbstractWidget<?>) thisJoinPoint.getThis();
       

=== modified file 'src/com/goldencode/p2j/aspects/ui/SyncConfigChangesAspect.java'
--- old/src/com/goldencode/p2j/aspects/ui/SyncConfigChangesAspect.java	2026-04-07 07:12:52 +0000
+++ new/src/com/goldencode/p2j/aspects/ui/SyncConfigChangesAspect.java	2026-09-25 09:30:39 +0000
@@ -13,6 +13,7 @@
 **                  of concrete class. This is needed to allow client-side code to be executed in
 **                  the server context.
 ** 005 RNC 20260402 Fixed javadoc warnings.
+** 006 TG  20260925 The trackingManager() cache assignment is not a config value change.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -151,6 +152,7 @@
     */
    @Before("target(config) && set(* *..WidgetConfig+.*)" +
            "&& !withincode(void readExternal(..)) " +
+           "&& !withincode(* com.goldencode.p2j.ui.WidgetConfig.trackingManager()) " +
            "&& !withincode(void applyConfig(..)) " +
            "&& !withincode(private void com.goldencode.p2j.ui.LabelConfig.setDefaults(..)) " +
            "&& !withincode(com.goldencode.p2j.ui.*Config.new(..))" +

=== modified file 'src/com/goldencode/p2j/main/StandardServer.java'
--- old/src/com/goldencode/p2j/main/StandardServer.java	2026-09-04 16:47:47 +0000
+++ new/src/com/goldencode/p2j/main/StandardServer.java	2026-09-25 09:00:59 +0000
@@ -297,6 +297,7 @@
 ** 146 TG  20260703          Registered ChunkTransfer as its own network group so a peer (e.g. an
 **                           appserver agent) can pull streamed binary payloads from this server.
 ** 147 CA  20260803          FWD Client can run entirely in the FWD Server JVM now, even in non-MSA mode.
+** 148 TG  20260925          Server-side batch client thread uses the ContextLocal read cache.
 */
 
 /*
@@ -1062,6 +1063,9 @@
             }
 
             session = SessionManager.get().initializeServerSession(context);
+            
+            // this thread stays in the new session's context until it terminates
+            ContextLocal.setReadCache(true);
          }
          catch (Exception e)
          {
@@ -1158,6 +1162,8 @@
          }
          finally
          {
+            ContextLocal.setReadCache(false);
+            
             try
             {
                // needs to be done first
@@ -1202,7 +1208,7 @@
          }
       };
       
-      Thread launcher = new Thread(runner);
+      Thread launcher = new ContextLocal.DedicatedThread(runner);
       launcher.setName("Launcher for server-side batch process " + account);
       launcher.setDaemon(true);
       launcher.start();

=== modified file 'src/com/goldencode/p2j/net/SessionManager.java'
--- old/src/com/goldencode/p2j/net/SessionManager.java	2026-08-03 15:52:49 +0000
+++ new/src/com/goldencode/p2j/net/SessionManager.java	2026-09-25 09:42:07 +0000
@@ -105,6 +105,7 @@
 ** 047 AS  20260617          Harden session context ID seeding with SecureRandom.
 ** 048 CA  20260803          When a FWD Client runs in the same JVM as the FWD Server, a mock net session is
 **                           required to allow session-level listeners to execute.
+** 049 TG  20260925          get() and isAsyncThread() do not use JVM-wide locks.
 */
 
 /*
@@ -223,6 +224,9 @@
    /** Singleton instance of this class */
    private static SessionManager instance = null;
    
+   /** The {@link #instance} after it was fully constructed, read without locking by {@link #get}. */
+   private static volatile SessionManager published = null;
+   
    /** Topology database for this network node */
    protected final Map<InetSocketAddress, Queue> topology = new HashMap<>();
    
@@ -275,7 +279,7 @@
    protected BootstrapConfig config = null;
 
    /** A set containing threads which always send async requests. */
-   private final Set<Thread> asyncThreads = Collections.newSetFromMap(new IdentityHashMap<>());
+   private final Set<Thread> asyncThreads = ConcurrentHashMap.newKeySet();
    
    /** The handler used to process interrupt requests. */
    private InterruptHandler interruptHandler;
@@ -335,9 +339,27 @@
     * @return  Singleton session manager, if it has been created;  otherwise,
     *          <code>null</code>.
     */
-   public synchronized static SessionManager get()
+   public static SessionManager get()
    {
-      return instance;
+      SessionManager sm = published;
+      if (sm != null)
+      {
+         return sm;
+      }
+      
+      // the instance is created while holding this lock; do not publish it if this thread is the one
+      // constructing it
+      boolean nested = Thread.holdsLock(SessionManager.class);
+      synchronized (SessionManager.class)
+      {
+         sm = instance;
+         if (!nested)
+         {
+            published = sm;
+         }
+      }
+      
+      return sm;
    }
    
    /**
@@ -348,10 +370,7 @@
     */
    public final void registerAsyncThread(Thread thread)
    {
-      synchronized (asyncThreads)
-      {
-         asyncThreads.add(thread);
-      }
+      asyncThreads.add(thread);
    }
    
    /**
@@ -362,10 +381,7 @@
     */
    public final void deregisterAsyncThread(Thread thread)
    {
-      synchronized (asyncThreads)
-      {
-         asyncThreads.remove(thread);
-      }
+      asyncThreads.remove(thread);
    }
    
    /**
@@ -379,10 +395,8 @@
     */
    public final boolean isAsyncThread(Thread thread)
    {
-      synchronized (asyncThreads)
-      {
-         return asyncThreads.contains(thread);
-      }
+      // checked twice for each call of an in-JVM client, without a JVM-wide lock
+      return !asyncThreads.isEmpty() && asyncThreads.contains(thread);
    }
    
    /**

=== modified file 'src/com/goldencode/p2j/security/ContextLocal.java'
--- old/src/com/goldencode/p2j/security/ContextLocal.java	2026-08-04 09:10:20 +0000
+++ new/src/com/goldencode/p2j/security/ContextLocal.java	2026-09-25 09:08:04 +0000
@@ -88,6 +88,7 @@
 ** 029 CA  20240903          For standalone mode, 'fallback' thread-local var must be assigned on first usage
 **                           in get/set.
 ** 030 CA  20260803          Standalone FWD Client JVMs must also clean up this data.
+** 031 TG  20260925          Added DedicatedThread, which can use the read cache like conversation threads.
 */
 
 /*
@@ -811,7 +812,54 @@
     */
    private static boolean hasReadCache()
    {
-      return SessionManager.isConversationThread();
+      if (SessionManager.isConversationThread())
+      {
+         return true;
+      }
+      
+      Thread t = Thread.currentThread();
+      return t instanceof DedicatedThread && ((DedicatedThread) t).readCache;
+   }
+   
+   /**
+    * Enable or disable the read cache for the current thread, which must be a {@link DedicatedThread}.
+    * The cache must be enabled only while the thread stays in the same security context.
+    * 
+    * @param    on
+    *           <code>true</code> to enable the read cache.
+    */
+   public static void setReadCache(boolean on)
+   {
+      Thread t = Thread.currentThread();
+      if (!(t instanceof DedicatedThread))
+      {
+         throw new IllegalStateException("The read cache can be enabled only on a DedicatedThread.");
+      }
+      
+      ((DedicatedThread) t).readCache = on;
+   }
+   
+   /**
+    * A thread which is dedicated to a single security context for (a part of) its lifetime, like a
+    * conversation thread.  While it is in that context, it can enable the read cache used to optimize
+    * {@link ContextLocal#get()}, via {@link ContextLocal#setReadCache}.
+    */
+   public static class DedicatedThread
+   extends Thread
+   {
+      /** Flag indicating if the read cache is enabled; accessed only by this thread. */
+      private boolean readCache = false;
+      
+      /**
+       * Create a new thread.
+       * 
+       * @param    target
+       *           The code to run.
+       */
+      public DedicatedThread(Runnable target)
+      {
+         super(target);
+      }
    }
    
    /**

=== modified file 'src/com/goldencode/p2j/ui/ClientConfigManager.java'
--- old/src/com/goldencode/p2j/ui/ClientConfigManager.java	2026-08-03 15:52:49 +0000
+++ new/src/com/goldencode/p2j/ui/ClientConfigManager.java	2026-09-25 10:20:48 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2014-2026, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- ---------------------------------Description----------------------------------
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
 ** 001 CA  20140929 Created initial version.
 ** 002 CA  20141014 Separated data which needs to survive the appserver agent's context reset into 
 **                  a different container.
@@ -41,6 +41,9 @@
 ** 023 TG  20260414 Replaced ReflectASM with MethodHandles.
 ** 024 CA  20260803 FWD Client can run entirely in the FWD Server JVM now - so any constant field is marked as
 **                  an explicit constant.
+** 025 TG  20260924 Field access via generated ConfigAccessor; added setConfigFields for batched updates.
+** 026 TG  20260925 Added setBackupField(s), to apply server-pushed values to a backup config.
+** 027 TG  20260925 Added snapshotConfig and firstDifference, used by FrameLayoutCache.
 */
 
 /*
@@ -198,6 +201,147 @@
    }
 
    /**
+    * Set the widget config fields to the specified values.
+    *
+    * @param    wid
+    *           The widget ID.
+    * @param    wcfg
+    *           The widget configuration; if this is a down widget, this will be used.
+    * @param    fids
+    *           The config field ids.
+    * @param    values
+    *           The new config field values.
+    */
+   public void setConfigFields(WidgetId wid, WidgetConfig wcfg, Integer[] fids, Object[] values)
+   {
+      if (!(wid instanceof WidgetDownId))
+      {
+         wcfg = getActiveConfig(wid);
+      }
+      WidgetConfigDef wdef = getConfigDef(wid.asInt());
+
+      int[] ids = new int[fids.length];
+      for (int i = 0; i < fids.length; i++)
+      {
+         ids[i] = fids[i];
+      }
+
+      setWidgetConfigs(wcfg, wdef, ids, values);
+   }
+
+   /**
+    * Apply the values of the specified fields, pushed by the server-side, to the backup config of a
+    * (non-down) widget, so the backup config reflects what the server-side knows.  Only fields tracked
+    * for changed values (immutable types) are applied; the other fields are always sent when dirty.
+    *
+    * @param    wid
+    *           The widget ID.
+    * @param    fname
+    *           The config field name.
+    * @param    value
+    *           The value pushed by the server-side.
+    */
+   public void setBackupField(WidgetId wid, String fname, Object value)
+   {
+      WidgetConfig backup = (wid instanceof WidgetDownId) ? null : backupConfigs.get(wid);
+      if (backup == null)
+      {
+         return;
+      }
+      
+      WidgetConfigDef wdef = getConfigDef(wid.asInt());
+      Integer fid = wdef.fieldIds.get(fname);
+      if (fid != null && wdef.trackChanged[fid])
+      {
+         // the backup config has no owner, it must not be registered for state synchronization
+         super.setWidgetConfig(backup, value, wdef, fid);
+      }
+   }
+
+   /**
+    * Apply the values of the specified fields, pushed by the server-side, to the backup config of a
+    * (non-down) widget; see {@link #setBackupField}.
+    *
+    * @param    wid
+    *           The widget ID.
+    * @param    fids
+    *           The config field ids.
+    * @param    values
+    *           The values pushed by the server-side.
+    */
+   public void setBackupFields(WidgetId wid, Integer[] fids, Object[] values)
+   {
+      WidgetConfig backup = (wid instanceof WidgetDownId) ? null : backupConfigs.get(wid);
+      if (backup == null)
+      {
+         return;
+      }
+      
+      WidgetConfigDef wdef = getConfigDef(wid.asInt());
+      for (int i = 0; i < fids.length; i++)
+      {
+         int fid = fids[i];
+         if (wdef.trackChanged[fid])
+         {
+            super.setWidgetConfig(backup, values[i], wdef, fid);
+         }
+      }
+   }
+
+   /**
+    * Create a copy of the given config, which is not registered anywhere and has no ID.  It is meant
+    * for comparisons, see {@link #firstDifference}.
+    *
+    * @param    cfg
+    *           The config to copy.
+    *
+    * @return   The copy.
+    */
+   public WidgetConfig snapshotConfig(WidgetConfig cfg)
+   {
+      WidgetConfig copy = getConfigDef(cfg.cfgClass).newInstance();
+      ConfigSyncManager.applyUnregistered(copy, cfg);
+      
+      return copy;
+   }
+   
+   /**
+    * Find the first field which has different values in the given configs of the same class.
+    *
+    * @param    a
+    *           The first config.
+    * @param    b
+    *           The second config.
+    * @param    ignored
+    *           The names of the fields which are not compared.
+    *
+    * @return   The name of the first different field, or <code>null</code> if all compared fields have
+    *           the same values.
+    */
+   public String firstDifference(WidgetConfig a, WidgetConfig b, Set<String> ignored)
+   {
+      if (a.cfgClass != b.cfgClass)
+      {
+         return "<class>";
+      }
+      
+      WidgetConfigDef wdef = getConfigDef(a.cfgClass);
+      for (int fid = 0; fid < wdef.fieldCount; fid++)
+      {
+         if (!wdef.accessor.same(a, b, fid))
+         {
+            String name = wdef.fieldNames.get(fid);
+            if (!ignored.contains(name))
+            {
+               return name;
+            }
+         }
+      }
+      
+      return null;
+   }
+
+   /**
     * Inform the client-side that it can no longer send config updates.
     */
    public void deactivateConfigUpdates()
@@ -289,13 +433,7 @@
 
          try
          {
-            Object activeVal = wdef.fields[fid].get(cfg);
-            Object backupVal = wdef.fields[fid].get(backup);
-
-            boolean changed = !((activeVal == null && backupVal == null) ||
-                                (activeVal != null && activeVal.equals(backupVal)));
-            
-            if (changed)
+            if (!wdef.accessor.same(cfg, backup, fid))
             {
                addDirtyConfig(cfg, fname);
             }
@@ -367,6 +505,28 @@
       ConfigSyncManager.registerConfig(null, cfg);
       super.setWidgetConfig(cfg, value, wdef, fid);
    }
+
+   /**
+    * Set the specified widget configuration fields to the given values.
+    *
+    * @param    cfg
+    *           The widget configuration to be updated.
+    * @param    wdef
+    *           The definition of the widget configuration.
+    * @param    fids
+    *           The IDs of the changed fields.
+    * @param    values
+    *           The new field values.
+    */
+   @Override
+   <T extends WidgetConfig> void setWidgetConfigs(T               cfg,
+                                                  WidgetConfigDef wdef,
+                                                  int[]           fids,
+                                                  Object[]        values)
+   {
+      ConfigSyncManager.registerConfig(null, cfg);
+      super.setWidgetConfigs(cfg, wdef, fids, values);
+   }
    
    /**
     * Helper method to replace the entry in the given map, to use the new key. 

=== added file 'src/com/goldencode/p2j/ui/ConfigAccessor.java'
--- old/src/com/goldencode/p2j/ui/ConfigAccessor.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/ui/ConfigAccessor.java	2026-09-25 06:45:17 +0000
@@ -0,0 +1,526 @@
+/*
+** Module   : ConfigAccessor.java
+** Abstract : Indexed access to the tracked fields of a widget configuration class.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
+** 001 TG  20260924 Created initial version.
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+** 
+** Additional terms under GNU Affero GPL version 3 section 7:
+** 
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+** 
+**   0. Attribution Requirement.
+** 
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+** 
+**   1. No License To Use Trademarks.
+** 
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+** 
+**   2. No Misrepresentation of Affiliation.
+** 
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+** 
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+** 
+**   3. No Misrepresentation of Source or Origin.
+** 
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+
+package com.goldencode.p2j.ui;
+
+import java.util.*;
+
+/**
+ * Provides access to the tracked fields of a single {@link WidgetConfig} class, using the field IDs
+ * defined by the associated {@link WidgetConfigDef}.
+ * <p>
+ * Implementations are generated by {@link ConfigAccessorGenerator}, one class for each widget
+ * configuration class, so that each field access compiles to a direct field instruction.  The
+ * accessors of the built-in configuration classes are generated at build time; see
+ * {@link ConfigAccessorFactory}.
+ * The bulk operations run their loop inside the generated class, so that the per-field calls are
+ * bound to a single receiver type.
+ * <p>
+ * The public static helpers are invoked by the generated code and reproduce the conversions
+ * applied by a {@link java.lang.invoke.VarHandle} invoked with {@code Object} values, and the
+ * equality applied by {@link Objects#equals(Object, Object)} on boxed values.
+ */
+public abstract class ConfigAccessor
+{
+   /**
+    * Constructor for the sub-classes.
+    */
+   protected ConfigAccessor()
+   {
+   }
+
+   /**
+    * Unbox a value assigned to a {@code boolean} field.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static boolean unboxBoolean(Object v)
+   {
+      if (v instanceof Boolean)
+      {
+         return ((Boolean) v).booleanValue();
+      }
+
+      throw wrongType(v, boolean.class);
+   }
+
+   /**
+    * Unbox a value assigned to a {@code byte} field.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static byte unboxByte(Object v)
+   {
+      if (v instanceof Byte)
+      {
+         return ((Byte) v).byteValue();
+      }
+
+      throw wrongType(v, byte.class);
+   }
+
+   /**
+    * Unbox a value assigned to a {@code short} field, allowing primitive widening.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static short unboxShort(Object v)
+   {
+      if (v instanceof Short)
+      {
+         return ((Short) v).shortValue();
+      }
+
+      if (v instanceof Byte)
+      {
+         return ((Byte) v).byteValue();
+      }
+
+      throw wrongType(v, short.class);
+   }
+
+   /**
+    * Unbox a value assigned to a {@code char} field.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static char unboxChar(Object v)
+   {
+      if (v instanceof Character)
+      {
+         return ((Character) v).charValue();
+      }
+
+      throw wrongType(v, char.class);
+   }
+
+   /**
+    * Unbox a value assigned to an {@code int} field, allowing primitive widening.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static int unboxInt(Object v)
+   {
+      if (v instanceof Integer)
+      {
+         return ((Integer) v).intValue();
+      }
+
+      if (v instanceof Short || v instanceof Byte)
+      {
+         return ((Number) v).intValue();
+      }
+
+      if (v instanceof Character)
+      {
+         return ((Character) v).charValue();
+      }
+
+      throw wrongType(v, int.class);
+   }
+
+   /**
+    * Unbox a value assigned to a {@code long} field, allowing primitive widening.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static long unboxLong(Object v)
+   {
+      if (v instanceof Long)
+      {
+         return ((Long) v).longValue();
+      }
+
+      if (v instanceof Integer || v instanceof Short || v instanceof Byte)
+      {
+         return ((Number) v).longValue();
+      }
+
+      if (v instanceof Character)
+      {
+         return ((Character) v).charValue();
+      }
+
+      throw wrongType(v, long.class);
+   }
+
+   /**
+    * Unbox a value assigned to a {@code float} field, allowing primitive widening.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static float unboxFloat(Object v)
+   {
+      if (v instanceof Float)
+      {
+         return ((Float) v).floatValue();
+      }
+
+      if (v instanceof Long || v instanceof Integer || v instanceof Short || v instanceof Byte)
+      {
+         return ((Number) v).floatValue();
+      }
+
+      if (v instanceof Character)
+      {
+         return ((Character) v).charValue();
+      }
+
+      throw wrongType(v, float.class);
+   }
+
+   /**
+    * Unbox a value assigned to a {@code double} field, allowing primitive widening.
+    *
+    * @param    v
+    *           The value.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    * @throws   ClassCastException
+    *           If the value can not be converted.
+    */
+   public static double unboxDouble(Object v)
+   {
+      if (v instanceof Double)
+      {
+         return ((Double) v).doubleValue();
+      }
+
+      if (v instanceof Float   ||
+          v instanceof Long    ||
+          v instanceof Integer ||
+          v instanceof Short   ||
+          v instanceof Byte)
+      {
+         return ((Number) v).doubleValue();
+      }
+
+      if (v instanceof Character)
+      {
+         return ((Character) v).charValue();
+      }
+
+      throw wrongType(v, double.class);
+   }
+
+   /**
+    * Compare two {@code int} compatible field values.
+    *
+    * @param    a
+    *           The first value.
+    * @param    b
+    *           The second value.
+    *
+    * @return   {@code true} if the values are equal.
+    */
+   public static boolean same(int a, int b)
+   {
+      return a == b;
+   }
+
+   /**
+    * Compare two {@code long} field values.
+    *
+    * @param    a
+    *           The first value.
+    * @param    b
+    *           The second value.
+    *
+    * @return   {@code true} if the values are equal.
+    */
+   public static boolean same(long a, long b)
+   {
+      return a == b;
+   }
+
+   /**
+    * Compare two {@code float} field values, as {@link Float#equals(Object)} does.
+    *
+    * @param    a
+    *           The first value.
+    * @param    b
+    *           The second value.
+    *
+    * @return   {@code true} if the values are equal.
+    */
+   public static boolean same(float a, float b)
+   {
+      return Float.floatToIntBits(a) == Float.floatToIntBits(b);
+   }
+
+   /**
+    * Compare two {@code double} field values, as {@link Double#equals(Object)} does.
+    *
+    * @param    a
+    *           The first value.
+    * @param    b
+    *           The second value.
+    *
+    * @return   {@code true} if the values are equal.
+    */
+   public static boolean same(double a, double b)
+   {
+      return Double.doubleToLongBits(a) == Double.doubleToLongBits(b);
+   }
+
+   /**
+    * Compare two reference field values.
+    *
+    * @param    a
+    *           The first value.
+    * @param    b
+    *           The second value.
+    *
+    * @return   {@code true} if the values are equal.
+    */
+   public static boolean same(Object a, Object b)
+   {
+      return Objects.equals(a, b);
+   }
+
+   /**
+    * Create the exception thrown for a field ID which is not defined by the config class.
+    *
+    * @param    fid
+    *           The field ID.
+    *
+    * @return   See above.
+    */
+   public static RuntimeException unknownField(int fid)
+   {
+      return new IllegalArgumentException("Unknown widget config field ID " + fid);
+   }
+
+   /**
+    * Get the fingerprint of the fields this accessor was generated for, as computed by
+    * {@link ConfigAccessorFactory#fingerprint}.
+    *
+    * @return   See above.
+    */
+   public abstract long fingerprint();
+
+   /**
+    * Get the value of a field.
+    *
+    * @param    cfg
+    *           The widget config.
+    * @param    fid
+    *           The field ID.
+    *
+    * @return   The field value, boxed if the field is primitive.
+    */
+   public abstract Object get(WidgetConfig cfg, int fid);
+
+   /**
+    * Set the value of a field.
+    *
+    * @param    cfg
+    *           The widget config.
+    * @param    fid
+    *           The field ID.
+    * @param    value
+    *           The new value, boxed if the field is primitive.
+    */
+   public abstract void set(WidgetConfig cfg, int fid, Object value);
+
+   /**
+    * Check if a field has the same value in two configs.
+    *
+    * @param    a
+    *           The first widget config.
+    * @param    b
+    *           The second widget config.
+    * @param    fid
+    *           The field ID.
+    *
+    * @return   {@code true} if the values are equal.
+    */
+   public abstract boolean same(WidgetConfig a, WidgetConfig b, int fid);
+
+   /**
+    * Collect the values of the given fields from the active config.  A field which is tracked
+    * for changed value is skipped when its value is the same in the backup config.
+    *
+    * @param    active
+    *           The active widget config.
+    * @param    backup
+    *           The backup widget config.
+    * @param    fids
+    *           The IDs of the candidate fields.
+    * @param    count
+    *           The number of candidate fields in {@code fids}.
+    * @param    track
+    *           Flags indicating the fields tracked for changed value, indexed by field ID, or
+    *           {@code null} to collect all candidate fields.
+    * @param    outFids
+    *           Receives the IDs of the collected fields.
+    * @param    outVals
+    *           Receives the values of the collected fields.
+    *
+    * @return   The number of collected fields.
+    */
+   public abstract int collectChanges(WidgetConfig active,
+                                      WidgetConfig backup,
+                                      int[]        fids,
+                                      int          count,
+                                      boolean[]    track,
+                                      int[]        outFids,
+                                      Object[]     outVals);
+
+   /**
+    * Set the values of the given fields.
+    *
+    * @param    cfg
+    *           The widget config.
+    * @param    fids
+    *           The field IDs.
+    * @param    values
+    *           The new values, boxed for primitive fields.
+    * @param    count
+    *           The number of fields to set.
+    */
+   public abstract void applyUpdates(WidgetConfig cfg, int[] fids, Object[] values, int count);
+
+   /**
+    * Create the exception thrown when a value can not be assigned to a primitive field.
+    *
+    * @param    v
+    *           The value.
+    * @param    type
+    *           The primitive field type.
+    *
+    * @return   See above.
+    *
+    * @throws   NullPointerException
+    *           If the value is {@code null}.
+    */
+   private static RuntimeException wrongType(Object v, Class<?> type)
+   {
+      if (v == null)
+      {
+         throw new NullPointerException("Can not assign null to a " + type.getName() + " field");
+      }
+
+      return new ClassCastException("Cannot convert " + v.getClass().getName() + " to " + type.getName());
+   }
+}

=== added file 'src/com/goldencode/p2j/ui/ConfigAccessorFactory.java'
--- old/src/com/goldencode/p2j/ui/ConfigAccessorFactory.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/ui/ConfigAccessorFactory.java	2026-09-25 06:45:17 +0000
@@ -0,0 +1,211 @@
+/*
+** Module   : ConfigAccessorFactory.java
+** Abstract : Resolution of the prebuilt or runtime generated widget configuration field accessors.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
+** 001 TG  20260924 Created initial version.
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+** 
+** Additional terms under GNU Affero GPL version 3 section 7:
+** 
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+** 
+**   0. Attribution Requirement.
+** 
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+** 
+**   1. No License To Use Trademarks.
+** 
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+** 
+**   2. No Misrepresentation of Affiliation.
+** 
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+** 
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+** 
+**   3. No Misrepresentation of Source or Origin.
+** 
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+
+package com.goldencode.p2j.ui;
+
+import java.lang.reflect.*;
+import java.nio.charset.*;
+
+import com.goldencode.p2j.util.logging.*;
+
+/**
+ * Resolves the {@link ConfigAccessor} for a widget configuration class.
+ * <p>
+ * The accessors of the built-in configuration classes are generated at build time by
+ * {@link ConfigAccessorGenerator} and packaged in the FWD jar, named with the
+ * {@link #PREBUILT_PREFIX} prefix followed by the configuration class name.  Being regular classes
+ * loaded from the jar, they can be included in an AppCDS archive.  A prebuilt accessor is used only
+ * if its {@link ConfigAccessor#fingerprint() fingerprint} matches the fields of the configuration
+ * class; otherwise, and for the configuration classes without a prebuilt accessor, the accessor is
+ * generated at runtime as a hidden class.
+ * <p>
+ * This class does not reference the bytecode generation library, so it is loaded only when an
+ * accessor needs to be generated at runtime.
+ */
+final class ConfigAccessorFactory
+{
+   /** The class name prefix of the prebuilt accessors. */
+   static final String PREBUILT_PREFIX = "com.goldencode.p2j.ui.ConfigAccessors$";
+
+   /** Logger */
+   private static final CentralLogger LOG = CentralLogger.get(ConfigAccessorFactory.class.getSimpleName());
+
+   /** The package of the built-in widget configuration classes. */
+   private static final String CONFIG_PACKAGE = WidgetConfig.class.getPackageName();
+
+   /**
+    * Private constructor, this class has only static members.
+    */
+   private ConfigAccessorFactory()
+   {
+   }
+
+   /**
+    * Get the accessor for the given widget configuration class.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    *
+    * @return   See above.
+    */
+   static ConfigAccessor create(Class<? extends WidgetConfig> cfgCls, Field[] fields)
+   {
+      long           fp  = fingerprint(fields);
+      ConfigAccessor acc = loadPrebuilt(cfgCls);
+
+      if (acc != null)
+      {
+         if (acc.fingerprint() == fp)
+         {
+            return acc;
+         }
+
+         LOG.warning("The prebuilt config accessor for " + cfgCls.getName() +
+                     " does not match the config fields, generating it at runtime.");
+      }
+
+      return ConfigAccessorGenerator.define(cfgCls, fields, fp);
+   }
+
+   /**
+    * Get the name of the prebuilt accessor of the given widget configuration class.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    *
+    * @return   The class name, or {@code null} if the class is not a built-in configuration class.
+    */
+   static String prebuiltName(Class<?> cfgCls)
+   {
+      if (!CONFIG_PACKAGE.equals(cfgCls.getPackageName()))
+      {
+         return null;
+      }
+
+      return PREBUILT_PREFIX + cfgCls.getName().substring(CONFIG_PACKAGE.length() + 1);
+   }
+
+   /**
+    * Compute the fingerprint of the tracked fields: a 64-bit FNV-1a hash of the field names and
+    * type descriptors, in field ID order.
+    *
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    *
+    * @return   See above.
+    */
+   static long fingerprint(Field[] fields)
+   {
+      long hash = 0xcbf29ce484222325L;
+      for (Field f : fields)
+      {
+         String sig = f.getName() + ':' + f.getType().descriptorString() + ';';
+         for (byte b : sig.getBytes(StandardCharsets.UTF_8))
+         {
+            hash ^= (b & 0xff);
+            hash *= 0x100000001b3L;
+         }
+      }
+
+      return hash;
+   }
+
+   /**
+    * Load and instantiate the prebuilt accessor of the given widget configuration class.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    *
+    * @return   The accessor, or {@code null} if there is no prebuilt accessor for the class.
+    */
+   private static ConfigAccessor loadPrebuilt(Class<? extends WidgetConfig> cfgCls)
+   {
+      String name = prebuiltName(cfgCls);
+      if (name == null)
+      {
+         return null;
+      }
+
+      Class<?> cls;
+      try
+      {
+         cls = Class.forName(name, true, ConfigAccessor.class.getClassLoader());
+      }
+      catch (ClassNotFoundException e)
+      {
+         return null;
+      }
+
+      try
+      {
+         return (ConfigAccessor) cls.getDeclaredConstructor().newInstance();
+      }
+      catch (ReflectiveOperationException e)
+      {
+         throw new IllegalStateException("Could not instantiate the config accessor " + name, e);
+      }
+   }
+}

=== added file 'src/com/goldencode/p2j/ui/ConfigAccessorGenerator.java'
--- old/src/com/goldencode/p2j/ui/ConfigAccessorGenerator.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/ui/ConfigAccessorGenerator.java	2026-09-25 06:45:17 +0000
@@ -0,0 +1,650 @@
+/*
+** Module   : ConfigAccessorGenerator.java
+** Abstract : Bytecode generation of the widget configuration field accessors.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
+** 001 TG  20260924 Created initial version.
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+** 
+** Additional terms under GNU Affero GPL version 3 section 7:
+** 
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+** 
+**   0. Attribution Requirement.
+** 
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+** 
+**   1. No License To Use Trademarks.
+** 
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+** 
+**   2. No Misrepresentation of Affiliation.
+** 
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+** 
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+** 
+**   3. No Misrepresentation of Source or Origin.
+** 
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+
+package com.goldencode.p2j.ui;
+
+import java.io.*;
+import java.lang.invoke.*;
+import java.lang.reflect.*;
+import java.nio.file.*;
+import java.util.*;
+
+import org.objectweb.asm.*;
+import org.objectweb.asm.Type;
+
+/**
+ * Generates the bytecode of the {@link ConfigAccessor} for a widget configuration class.
+ * <p>
+ * Each operation dispatches the field ID with a {@code tableswitch} to a direct {@code getfield} or
+ * {@code putfield} instruction, and the bulk operations invoke the per-field operations of the same
+ * final class, so the JIT can compile them without any dynamic dispatch.
+ * <p>
+ * The accessors of the {@link ConfigManager#CONFIG_CLASSES built-in configuration classes} are
+ * generated at build time by {@link #main}, as regular classes packaged in the FWD jar, so they
+ * can be included in an AppCDS archive.  The accessors of any other configuration class (external
+ * widgets) are {@link #define defined} at runtime as hidden classes.
+ */
+final class ConfigAccessorGenerator
+{
+   /** Internal name of the {@link ConfigAccessor} class. */
+   private static final String ACCESSOR = Type.getInternalName(ConfigAccessor.class);
+
+   /** Descriptor of the {@link ConfigAccessor#get} method. */
+   private static final String GET_DESC =
+      "(Lcom/goldencode/p2j/ui/WidgetConfig;I)Ljava/lang/Object;";
+
+   /** Descriptor of the {@link ConfigAccessor#set} method. */
+   private static final String SET_DESC =
+      "(Lcom/goldencode/p2j/ui/WidgetConfig;ILjava/lang/Object;)V";
+
+   /** Descriptor of the {@link ConfigAccessor#same(WidgetConfig, WidgetConfig, int)} method. */
+   private static final String SAME_DESC =
+      "(Lcom/goldencode/p2j/ui/WidgetConfig;Lcom/goldencode/p2j/ui/WidgetConfig;I)Z";
+
+   /** Descriptor of the {@link ConfigAccessor#collectChanges} method. */
+   private static final String COLLECT_DESC =
+      "(Lcom/goldencode/p2j/ui/WidgetConfig;Lcom/goldencode/p2j/ui/WidgetConfig;[II[Z[I[Ljava/lang/Object;)I";
+
+   /** Descriptor of the {@link ConfigAccessor#applyUpdates} method. */
+   private static final String APPLY_DESC =
+      "(Lcom/goldencode/p2j/ui/WidgetConfig;[I[Ljava/lang/Object;I)V";
+
+   /**
+    * Private constructor, this class has only static members.
+    */
+   private ConfigAccessorGenerator()
+   {
+   }
+
+   /**
+    * Define the accessor of the given widget configuration class as a hidden class, in the package
+    * of the configuration class, and instantiate it.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    * @param    fingerprint
+    *           The fingerprint of the tracked fields.
+    *
+    * @return   See above.
+    *
+    * @throws   IllegalStateException
+    *           If the accessor class can not be generated.
+    */
+   static ConfigAccessor define(Class<? extends WidgetConfig> cfgCls, Field[] fields, long fingerprint)
+   {
+      for (Field f : fields)
+      {
+         if (!isLinkable(cfgCls, f.getType()))
+         {
+            throw new IllegalStateException("Config field " + f + " has a type not accessible from " +
+                                            cfgCls.getName());
+         }
+      }
+
+      try
+      {
+         MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(cfgCls, MethodHandles.lookup());
+         String self = Type.getInternalName(cfgCls) + "$$ConfigAccessor";
+         byte[] bytes = generateClass(self, cfgCls, fields, fingerprint);
+         MethodHandles.Lookup hidden = lookup.defineHiddenClass(bytes, true);
+         MethodType ctorType = MethodType.methodType(void.class);
+
+         return (ConfigAccessor) hidden.findConstructor(hidden.lookupClass(), ctorType).invoke();
+      }
+      catch (Throwable t)
+      {
+         throw new IllegalStateException("Could not generate the config accessor for " + cfgCls.getName(),
+                                         t);
+      }
+   }
+
+   /**
+    * Check if the given type can be referenced by a class in the package of the widget
+    * configuration class.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    * @param    type
+    *           The field type.
+    *
+    * @return   See above.
+    */
+   private static boolean isLinkable(Class<?> cfgCls, Class<?> type)
+   {
+      while (type.isArray())
+      {
+         type = type.getComponentType();
+      }
+
+      if (type.isPrimitive() || Modifier.isPublic(type.getModifiers()))
+      {
+         return true;
+      }
+
+      return type.getClassLoader() == cfgCls.getClassLoader() &&
+             type.getPackageName().equals(cfgCls.getPackageName());
+   }
+
+   /**
+    * Generate the bytecode of the accessor class.
+    *
+    * @param    self
+    *           The internal name of the accessor class.
+    * @param    cfgCls
+    *           The widget configuration class.
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    * @param    fingerprint
+    *           The fingerprint of the tracked fields.
+    *
+    * @return   See above.
+    */
+   private static byte[] generateClass(String                        self,
+                                       Class<? extends WidgetConfig> cfgCls,
+                                       Field[]                       fields,
+                                       long                          fingerprint)
+   {
+      String cfg = Type.getInternalName(cfgCls);
+
+      ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)
+      {
+         @Override
+         protected String getCommonSuperClass(String type1, String type2)
+         {
+            return "java/lang/Object";
+         }
+      };
+
+      cw.visit(Opcodes.V17,
+               Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER,
+               self,
+               null,
+               ACCESSOR,
+               null);
+
+      MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(Opcodes.ALOAD, 0);
+      mv.visitMethodInsn(Opcodes.INVOKESPECIAL, ACCESSOR, "<init>", "()V", false);
+      mv.visitInsn(Opcodes.RETURN);
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+
+      mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "fingerprint", "()J", null, null);
+      mv.visitCode();
+      mv.visitLdcInsn(fingerprint);
+      mv.visitInsn(Opcodes.LRETURN);
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+
+      generateGet(cw, cfg, fields);
+      generateSet(cw, cfg, fields);
+      generateSame(cw, cfg, fields);
+      generateCollectChanges(cw, self);
+      generateApplyUpdates(cw, self);
+
+      cw.visitEnd();
+
+      return cw.toByteArray();
+   }
+
+   /**
+    * Generate the {@link ConfigAccessor#get} method.
+    *
+    * @param    cw
+    *           The class writer.
+    * @param    cfg
+    *           The internal name of the widget configuration class.
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    */
+   private static void generateGet(ClassWriter cw, String cfg, Field[] fields)
+   {
+      MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "get", GET_DESC, null, null);
+      mv.visitCode();
+      mv.visitVarInsn(Opcodes.ALOAD, 1);
+      mv.visitTypeInsn(Opcodes.CHECKCAST, cfg);
+      mv.visitVarInsn(Opcodes.ASTORE, 3);
+
+      Label[] cases = switchOnField(mv, 2, fields.length);
+      for (int fid = 0; fid < fields.length; fid++)
+      {
+         Field f = fields[fid];
+         mv.visitLabel(cases[fid]);
+         mv.visitVarInsn(Opcodes.ALOAD, 3);
+         mv.visitFieldInsn(Opcodes.GETFIELD, cfg, f.getName(), Type.getDescriptor(f.getType()));
+         box(mv, f.getType());
+         mv.visitInsn(Opcodes.ARETURN);
+      }
+      throwUnknownField(mv, cases[fields.length], 2);
+
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+   }
+
+   /**
+    * Generate the {@link ConfigAccessor#set} method.
+    *
+    * @param    cw
+    *           The class writer.
+    * @param    cfg
+    *           The internal name of the widget configuration class.
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    */
+   private static void generateSet(ClassWriter cw, String cfg, Field[] fields)
+   {
+      MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "set", SET_DESC, null, null);
+      mv.visitCode();
+      mv.visitVarInsn(Opcodes.ALOAD, 1);
+      mv.visitTypeInsn(Opcodes.CHECKCAST, cfg);
+      mv.visitVarInsn(Opcodes.ASTORE, 4);
+
+      Label[] cases = switchOnField(mv, 2, fields.length);
+      for (int fid = 0; fid < fields.length; fid++)
+      {
+         Field f = fields[fid];
+         mv.visitLabel(cases[fid]);
+         mv.visitVarInsn(Opcodes.ALOAD, 4);
+         mv.visitVarInsn(Opcodes.ALOAD, 3);
+         unbox(mv, f.getType());
+         mv.visitFieldInsn(Opcodes.PUTFIELD, cfg, f.getName(), Type.getDescriptor(f.getType()));
+         mv.visitInsn(Opcodes.RETURN);
+      }
+      throwUnknownField(mv, cases[fields.length], 2);
+
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+   }
+
+   /**
+    * Generate the {@link ConfigAccessor#same(WidgetConfig, WidgetConfig, int)} method.
+    *
+    * @param    cw
+    *           The class writer.
+    * @param    cfg
+    *           The internal name of the widget configuration class.
+    * @param    fields
+    *           The tracked fields, indexed by field ID.
+    */
+   private static void generateSame(ClassWriter cw, String cfg, Field[] fields)
+   {
+      MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "same", SAME_DESC, null, null);
+      mv.visitCode();
+      mv.visitVarInsn(Opcodes.ALOAD, 1);
+      mv.visitTypeInsn(Opcodes.CHECKCAST, cfg);
+      mv.visitVarInsn(Opcodes.ASTORE, 4);
+      mv.visitVarInsn(Opcodes.ALOAD, 2);
+      mv.visitTypeInsn(Opcodes.CHECKCAST, cfg);
+      mv.visitVarInsn(Opcodes.ASTORE, 5);
+
+      Label[] cases = switchOnField(mv, 3, fields.length);
+      for (int fid = 0; fid < fields.length; fid++)
+      {
+         Field  f    = fields[fid];
+         String desc = Type.getDescriptor(f.getType());
+         mv.visitLabel(cases[fid]);
+         mv.visitVarInsn(Opcodes.ALOAD, 4);
+         mv.visitFieldInsn(Opcodes.GETFIELD, cfg, f.getName(), desc);
+         mv.visitVarInsn(Opcodes.ALOAD, 5);
+         mv.visitFieldInsn(Opcodes.GETFIELD, cfg, f.getName(), desc);
+         mv.visitMethodInsn(Opcodes.INVOKESTATIC, ACCESSOR, "same", sameDescriptor(f.getType()), false);
+         mv.visitInsn(Opcodes.IRETURN);
+      }
+      throwUnknownField(mv, cases[fields.length], 3);
+
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+   }
+
+   /**
+    * Generate the {@link ConfigAccessor#collectChanges} method.
+    *
+    * @param    cw
+    *           The class writer.
+    * @param    self
+    *           The internal name of the accessor class.
+    */
+   private static void generateCollectChanges(ClassWriter cw, String self)
+   {
+      MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "collectChanges", COLLECT_DESC, null, null);
+      mv.visitCode();
+
+      Label loop = new Label();
+      Label add  = new Label();
+      Label next = new Label();
+      Label end  = new Label();
+
+      mv.visitInsn(Opcodes.ICONST_0);
+      mv.visitVarInsn(Opcodes.ISTORE, 9);
+      mv.visitInsn(Opcodes.ICONST_0);
+      mv.visitVarInsn(Opcodes.ISTORE, 8);
+
+      mv.visitLabel(loop);
+      mv.visitVarInsn(Opcodes.ILOAD, 8);
+      mv.visitVarInsn(Opcodes.ILOAD, 4);
+      mv.visitJumpInsn(Opcodes.IF_ICMPGE, end);
+
+      mv.visitVarInsn(Opcodes.ALOAD, 3);
+      mv.visitVarInsn(Opcodes.ILOAD, 8);
+      mv.visitInsn(Opcodes.IALOAD);
+      mv.visitVarInsn(Opcodes.ISTORE, 10);
+
+      mv.visitVarInsn(Opcodes.ALOAD, 5);
+      mv.visitJumpInsn(Opcodes.IFNULL, add);
+      mv.visitVarInsn(Opcodes.ALOAD, 5);
+      mv.visitVarInsn(Opcodes.ILOAD, 10);
+      mv.visitInsn(Opcodes.BALOAD);
+      mv.visitJumpInsn(Opcodes.IFEQ, add);
+      mv.visitVarInsn(Opcodes.ALOAD, 0);
+      mv.visitVarInsn(Opcodes.ALOAD, 1);
+      mv.visitVarInsn(Opcodes.ALOAD, 2);
+      mv.visitVarInsn(Opcodes.ILOAD, 10);
+      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, self, "same", SAME_DESC, false);
+      mv.visitJumpInsn(Opcodes.IFNE, next);
+
+      mv.visitLabel(add);
+      mv.visitVarInsn(Opcodes.ALOAD, 6);
+      mv.visitVarInsn(Opcodes.ILOAD, 9);
+      mv.visitVarInsn(Opcodes.ILOAD, 10);
+      mv.visitInsn(Opcodes.IASTORE);
+      mv.visitVarInsn(Opcodes.ALOAD, 7);
+      mv.visitVarInsn(Opcodes.ILOAD, 9);
+      mv.visitVarInsn(Opcodes.ALOAD, 0);
+      mv.visitVarInsn(Opcodes.ALOAD, 1);
+      mv.visitVarInsn(Opcodes.ILOAD, 10);
+      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, self, "get", GET_DESC, false);
+      mv.visitInsn(Opcodes.AASTORE);
+      mv.visitIincInsn(9, 1);
+
+      mv.visitLabel(next);
+      mv.visitIincInsn(8, 1);
+      mv.visitJumpInsn(Opcodes.GOTO, loop);
+
+      mv.visitLabel(end);
+      mv.visitVarInsn(Opcodes.ILOAD, 9);
+      mv.visitInsn(Opcodes.IRETURN);
+
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+   }
+
+   /**
+    * Generate the {@link ConfigAccessor#applyUpdates} method.
+    *
+    * @param    cw
+    *           The class writer.
+    * @param    self
+    *           The internal name of the accessor class.
+    */
+   private static void generateApplyUpdates(ClassWriter cw, String self)
+   {
+      MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "applyUpdates", APPLY_DESC, null, null);
+      mv.visitCode();
+
+      Label loop = new Label();
+      Label end  = new Label();
+
+      mv.visitInsn(Opcodes.ICONST_0);
+      mv.visitVarInsn(Opcodes.ISTORE, 5);
+
+      mv.visitLabel(loop);
+      mv.visitVarInsn(Opcodes.ILOAD, 5);
+      mv.visitVarInsn(Opcodes.ILOAD, 4);
+      mv.visitJumpInsn(Opcodes.IF_ICMPGE, end);
+
+      mv.visitVarInsn(Opcodes.ALOAD, 0);
+      mv.visitVarInsn(Opcodes.ALOAD, 1);
+      mv.visitVarInsn(Opcodes.ALOAD, 2);
+      mv.visitVarInsn(Opcodes.ILOAD, 5);
+      mv.visitInsn(Opcodes.IALOAD);
+      mv.visitVarInsn(Opcodes.ALOAD, 3);
+      mv.visitVarInsn(Opcodes.ILOAD, 5);
+      mv.visitInsn(Opcodes.AALOAD);
+      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, self, "set", SET_DESC, false);
+      mv.visitIincInsn(5, 1);
+      mv.visitJumpInsn(Opcodes.GOTO, loop);
+
+      mv.visitLabel(end);
+      mv.visitInsn(Opcodes.RETURN);
+
+      mv.visitMaxs(0, 0);
+      mv.visitEnd();
+   }
+
+   /**
+    * Emit a {@code tableswitch} on the field ID.
+    *
+    * @param    mv
+    *           The method visitor.
+    * @param    fidVar
+    *           The local variable holding the field ID.
+    * @param    count
+    *           The number of fields.
+    *
+    * @return   The labels for each field ID, followed by the label of the default case.
+    */
+   private static Label[] switchOnField(MethodVisitor mv, int fidVar, int count)
+   {
+      Label[] labels = new Label[count + 1];
+      for (int i = 0; i <= count; i++)
+      {
+         labels[i] = new Label();
+      }
+
+      if (count > 0)
+      {
+         mv.visitVarInsn(Opcodes.ILOAD, fidVar);
+         mv.visitTableSwitchInsn(0, count - 1, labels[count], Arrays.copyOf(labels, count));
+      }
+
+      return labels;
+   }
+
+   /**
+    * Emit the default case, which throws the exception for an unknown field ID.
+    *
+    * @param    mv
+    *           The method visitor.
+    * @param    label
+    *           The label of the default case.
+    * @param    fidVar
+    *           The local variable holding the field ID.
+    */
+   private static void throwUnknownField(MethodVisitor mv, Label label, int fidVar)
+   {
+      mv.visitLabel(label);
+      mv.visitVarInsn(Opcodes.ILOAD, fidVar);
+      mv.visitMethodInsn(Opcodes.INVOKESTATIC,
+                         ACCESSOR,
+                         "unknownField",
+                         "(I)Ljava/lang/RuntimeException;",
+                         false);
+      mv.visitInsn(Opcodes.ATHROW);
+   }
+
+   /**
+    * Emit the boxing of a field value, if the field is primitive.
+    *
+    * @param    mv
+    *           The method visitor.
+    * @param    type
+    *           The field type.
+    */
+   private static void box(MethodVisitor mv, Class<?> type)
+   {
+      if (!type.isPrimitive())
+      {
+         return;
+      }
+
+      Type   prim    = Type.getType(type);
+      String wrapper = Type.getInternalName(wrapper(type));
+      mv.visitMethodInsn(Opcodes.INVOKESTATIC,
+                         wrapper,
+                         "valueOf",
+                         "(" + prim.getDescriptor() + ")L" + wrapper + ";",
+                         false);
+   }
+
+   /**
+    * Emit the conversion of an {@code Object} value to the field type.
+    *
+    * @param    mv
+    *           The method visitor.
+    * @param    type
+    *           The field type.
+    */
+   private static void unbox(MethodVisitor mv, Class<?> type)
+   {
+      if (type.isPrimitive())
+      {
+         String name = type.getName();
+         String method = "unbox" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
+         mv.visitMethodInsn(Opcodes.INVOKESTATIC,
+                            ACCESSOR,
+                            method,
+                            "(Ljava/lang/Object;)" + Type.getDescriptor(type),
+                            false);
+      }
+      else if (type != Object.class)
+      {
+         mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(type));
+      }
+   }
+
+   /**
+    * Get the descriptor of the static {@code ConfigAccessor.same} helper for the field type.
+    *
+    * @param    type
+    *           The field type.
+    *
+    * @return   See above.
+    */
+   private static String sameDescriptor(Class<?> type)
+   {
+      if (type == long.class || type == float.class || type == double.class)
+      {
+         String d = Type.getDescriptor(type);
+         return "(" + d + d + ")Z";
+      }
+
+      if (type.isPrimitive())
+      {
+         return "(II)Z";
+      }
+
+      return "(Ljava/lang/Object;Ljava/lang/Object;)Z";
+   }
+
+   /**
+    * Get the wrapper class of a primitive type.
+    *
+    * @param    type
+    *           The primitive type.
+    *
+    * @return   See above.
+    */
+   private static Class<?> wrapper(Class<?> type)
+   {
+      return MethodType.methodType(type).wrap().returnType();
+   }
+
+   /**
+    * Generate the accessor classes of the built-in widget configuration classes.
+    *
+    * @param    args
+    *           The output directory of the class files.
+    *
+    * @throws   IOException
+    *           If a class file can not be written.
+    */
+   public static void main(String[] args)
+   throws IOException
+   {
+      if (args.length != 1)
+      {
+         System.err.println("Usage: ConfigAccessorGenerator <output directory>");
+         System.exit(1);
+      }
+
+      Path out = Paths.get(args[0]);
+      int  count = 0;
+      for (Class<? extends WidgetConfig> cfgCls : new LinkedHashSet<>(ConfigManager.CONFIG_CLASSES))
+      {
+         Field[] fields = WidgetConfigDef.trackedFields(cfgCls);
+         String  self   = ConfigAccessorFactory.prebuiltName(cfgCls).replace('.', '/');
+         byte[]  bytes  = generateClass(self, cfgCls, fields, ConfigAccessorFactory.fingerprint(fields));
+
+         Path file = out.resolve(self + ".class");
+         Files.createDirectories(file.getParent());
+         Files.write(file, bytes);
+         count++;
+      }
+
+      System.out.println("Generated " + count + " widget config accessors in " + out);
+   }
+}

=== modified file 'src/com/goldencode/p2j/ui/ConfigManager.java'
--- old/src/com/goldencode/p2j/ui/ConfigManager.java	2026-08-03 15:52:49 +0000
+++ new/src/com/goldencode/p2j/ui/ConfigManager.java	2026-09-25 09:25:04 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2014-2026, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- ---------------------------------Description----------------------------------
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
 ** 001 CA  20140924 Created initial version.
 ** 002 CA  20141014 Separated data which needs to survive the appserver agent's context reset into 
 **                  a different container.
@@ -55,6 +55,8 @@
 ** 026 TG  20260414 Replaced ReflectASM with MethodHandles API.
 ** 027 CA  20260803 FWD Client can run entirely in the FWD Server JVM now - so any constant field is marked as
 **                  an explicit constant.
+** 028 TG  20260924 Field access via generated ConfigAccessor; bulk collect/apply of the config updates.
+** 029 TG  20260925 The backup config copy is not registered for afterConfigUpdate notifications.
 */ 
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -164,12 +166,12 @@
    /** Map of widget configuration definitions by their config class. */
    protected final Map<Class<? extends WidgetConfig>, WidgetConfigDef> cfgDefs = new IdentityHashMap<>();
 
+   /** This list contains all config classes which implement {@link WidgetConfig}.*/
+   static final List<Class<? extends WidgetConfig>> CONFIG_CLASSES;
+
    /** Flag indicating if config changes are tracked or not. */
    private boolean tracking = true;
 
-   /** This list contains all config classes which implement {@link WidgetConfig}.*/
-   private static final List<Class<? extends WidgetConfig>> CONFIG_CLASSES;
-
 
    static
    {
@@ -233,7 +235,7 @@
       // walk the widget config classes and resolve their configuration
       for (Class<? extends WidgetConfig> cls : CONFIG_CLASSES)
       {
-         WidgetConfigDef cfgDef = new WidgetConfigDef(cls);
+         WidgetConfigDef cfgDef = WidgetConfigDef.forClass(cls);
 
          cfgDefs.put(cls, cfgDef);
       }
@@ -456,9 +458,12 @@
 
       List<WidgetConfigUpdates> res = new ArrayList<>();
 
-      for (WidgetId wid : dirtyConfigs.keySet())
+      for (Map.Entry<WidgetId, Set<String>> entry : dirtyConfigs.entrySet())
       {
-         if (!activeConfigs.containsKey(wid))
+         WidgetId     wid    = entry.getKey();
+         WidgetConfig active = activeConfigs.get(wid);
+
+         if (active == null)
          {
             // skip configs which have no ID or are not part of the active configurations
             continue;
@@ -469,16 +474,14 @@
             // skip widgets which are not part of the official registry
             continue;
          }
-         
+
          WidgetConfigDef wdef   = widgetCfg.get(wid);
-         WidgetConfig    active = activeConfigs.get(wid);
          WidgetConfig    backup = backupConfigs.get(wid);
+         Set<String>     dirty  = entry.getValue();
 
-         List<Integer> changedAttributes = new ArrayList<>();
-         
-         List<Object> changes = new ArrayList<>();
-         List<NativeTypeSerializer> workers = new ArrayList<>();
-         for (String field : dirtyConfigs.get(wid))
+         int[] fids  = new int[dirty.size()];
+         int   count = 0;
+         for (String field : dirty)
          {
             Integer ofid = wdef.fieldIds.get(field);
             if (ofid == null)
@@ -488,60 +491,53 @@
             }
 
             int fid = ofid.intValue();
-            if (wdef.ignoredFields[fid])
-            {
-               continue;
-            }
-
-            try
-            {
-               Object activeVal = wdef.fields[fid].get(active);
-               
-               // TODO: the code bellow is in preparation to send to the server-side only immutable
-               // fields which have changed value or complex fields which are sent always;
-               
-               boolean set = true;
-
-//             There is supposed to be a use case where enabling field tracking may break MODIFIED
-//             attribute for browse columns. See H008 and H007. At the time of this writing, the use case
-//             was not found.
-
-               if (!configTrackingDisabled)
-               {
-                  if (wdef.trackChanged[fid])
-                  {
-                     Object backupVal = wdef.fields[fid].get(backup);
-                     set = !Objects.equals(activeVal, backupVal);
-                  }
-               }
-               
-               if (set)
-               {
-                  changedAttributes.add(fid);
-                  changes.add(activeVal);
-                  workers.add(wdef.workers[fid]);
-               }
-            }
-            catch (Throwable t)
-            {
-               final String msg =
-                  "Could not resolve the active/backup values for widget config %s in class %s.";
-
-               String fname = wdef.fieldNames.get(fid);
-               throw new RuntimeException(String.format(msg, fname, active.cfgClass.getName()), 
-                                                        t);
-            }
+            if (!wdef.ignoredFields[fid])
+            {
+               fids[count++] = fid;
+            }
+         }
+
+         int[]    changedAttributes = new int[count];
+         Object[] changes           = new Object[count];
+         int      changed;
+
+//       There is supposed to be a use case where enabling field tracking may break MODIFIED
+//       attribute for browse columns. See H008 and H007. At the time of this writing, the use case
+//       was not found.
+
+         try
+         {
+            changed = wdef.accessor.collectChanges(active,
+                                                   backup,
+                                                   fids,
+                                                   count,
+                                                   configTrackingDisabled ? null : wdef.trackChanged,
+                                                   changedAttributes,
+                                                   changes);
+         }
+         catch (Throwable t)
+         {
+            final String msg =
+               "Could not resolve the active/backup values for widget %s config fields %s in class %s.";
+
+            throw new RuntimeException(String.format(msg, wid, dirty, active.cfgClass.getName()), t);
          }
 
          // applying all fields will make sure the backup copy will be always in expected state
-         backup.applyConfig(active);
+         ConfigSyncManager.applyUnregistered(backup, active);
          
-         if (!changes.isEmpty())
+         if (changed > 0)
          {
+            NativeTypeSerializer[] workers = new NativeTypeSerializer[changed];
+            for (int i = 0; i < changed; i++)
+            {
+               workers[i] = wdef.workers[changedAttributes[i]];
+            }
+
             res.add(new WidgetConfigUpdates(wid.asInt(),
-                                            Utils.integerCollectionToPrimitive(changedAttributes), 
-                                            changes.toArray(),
-                                            workers.toArray(new NativeTypeSerializer[0])));
+                                            Arrays.copyOf(changedAttributes, changed),
+                                            Arrays.copyOf(changes, changed),
+                                            workers));
          }
       }
       
@@ -712,7 +708,7 @@
     */
    public void addWidgetConfiguration(Class<? extends WidgetConfig> cfgClass)
    {
-      WidgetConfigDef cfgDef = new WidgetConfigDef(cfgClass);
+      WidgetConfigDef cfgDef = WidgetConfigDef.forClass(cfgClass);
       cfgDefs.put(cfgClass, cfgDef);
    }
 
@@ -800,7 +796,7 @@
    
       try
       {
-         res = (T) wdef.fields[fid].get(cfg);
+         res = (T) wdef.accessor.get(cfg, fid);
       }
       catch (Throwable t)
       {
@@ -884,7 +880,7 @@
    {
       try
       {
-         wdef.fields[fid].set(cfg, value);
+         wdef.accessor.set(cfg, fid, value);
       }
       catch (Throwable t)
       {
@@ -896,6 +892,52 @@
    }
    
    /**
+    * Set the specified widget configuration fields to the given values.
+    *
+    * @param    <T>
+    *           Config type.
+    *
+    * @param    cfg
+    *           The widget configuration to be updated.
+    * @param    wdef
+    *           The definition of the widget configuration.
+    * @param    fids
+    *           The IDs of the changed fields.
+    * @param    values
+    *           The new field values.
+    */
+   <T extends WidgetConfig> void setWidgetConfigs(T               cfg,
+                                                  WidgetConfigDef wdef,
+                                                  int[]           fids,
+                                                  Object[]        values)
+   {
+      try
+      {
+         wdef.accessor.applyUpdates(cfg, fids, values, fids.length);
+      }
+      catch (Throwable t)
+      {
+         String fname = null;
+         for (int i = 0; i < fids.length && fname == null; i++)
+         {
+            try
+            {
+               wdef.accessor.set(cfg, fids[i], values[i]);
+            }
+            catch (Throwable ft)
+            {
+               fname = fids[i] >= 0 && fids[i] < wdef.fieldCount ? wdef.fieldNames.get(fids[i])
+                                                                 : String.valueOf(fids[i]);
+            }
+         }
+
+         final String msg = "Could not set the config attr %s for widget %s with class %s.";
+         
+         throw new RuntimeException(String.format(msg, fname, cfg.id, wdef.cfgCls.getName()), t);
+      }
+   }
+   
+   /**
     * Remove this widget IDs from all the maps.
     * 
     * @param    wid

=== modified file 'src/com/goldencode/p2j/ui/ConfigSyncManager.java'
--- old/src/com/goldencode/p2j/ui/ConfigSyncManager.java	2026-09-04 16:47:47 +0000
+++ new/src/com/goldencode/p2j/ui/ConfigSyncManager.java	2026-09-25 09:25:03 +0000
@@ -17,6 +17,7 @@
 ** 008 RNC 20260402 Fixed javadoc warnings.
 ** 009 CA  20260904 In server-side mode, it is required to save and restore the state, so the client can 
 **                  execute in its own instance.
+** 010 TG  20260925 Added applyUnregistered, to copy a config which has no owner (like a backup config).
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -294,6 +295,32 @@
    }
    
    /**
+    * Copy the source config into the destination, without registering the destination for state
+    * synchronization.  This is meant for copies which have no owner (like the backup configs), so
+    * {@link #markScopeEnd} would ignore them anyway.
+    * 
+    * @param    dest
+    *           The destination config.
+    * @param    source
+    *           The source config.
+    */
+   public static void applyUnregistered(WidgetConfig dest, WidgetConfig source)
+   {
+      State state = localState.get();
+      boolean duplicating = state.duplicateInProgress;
+      state.duplicateInProgress = true;
+      
+      try
+      {
+         dest.applyConfig(source);
+      }
+      finally
+      {
+         state.duplicateInProgress = duplicating;
+      }
+   }
+   
+   /**
     * Registers a modified config for state synchronization
     * with its owner. When the passed in owner is <code>null</code>
     * it is later resolved with the help of the config id.

=== modified file 'src/com/goldencode/p2j/ui/GenericFrame.java'
--- old/src/com/goldencode/p2j/ui/GenericFrame.java	2026-07-17 11:52:04 +0000
+++ new/src/com/goldencode/p2j/ui/GenericFrame.java	2026-09-25 09:05:55 +0000
@@ -1034,6 +1034,7 @@
 **         20260525          Skip the removeFromDeferredDefs call in destroy() for frames whose
 **                           ScreenDefinition was never enqueued via deferPush.
 ** 405 TG  20260717          Added isClientRegistered.
+** 406 TG  20260925          Aggregators are processed in DISPLAY order, not in the order of their widget IDs.
 */
 
 /*
@@ -10296,7 +10297,8 @@
          {
             if (wids == null)
             {
-               wids = new HashSet<>();
+               // keep the DISPLAY order; the hash of an element is its widget ID
+               wids = new LinkedHashSet<>();
             }
             wids.add(fe[i]);
          }

=== modified file 'src/com/goldencode/p2j/ui/LogicalTerminal.java'
--- old/src/com/goldencode/p2j/ui/LogicalTerminal.java	2026-09-04 16:47:47 +0000
+++ new/src/com/goldencode/p2j/ui/LogicalTerminal.java	2026-09-25 08:54:35 +0000
@@ -1154,6 +1154,7 @@
 ** 487 CA  20260803          FWD Client can run entirely in the FWD Server JVM now, even in non-MSA mode.
 **     CA  20260904          In server-side mode, it is required to save and restore the ConfigSync state, so 
 **                           the client can execute in its own instance.
+** 488 TG  20260925          clientCall checks the async thread only when there are deferred pushes.
 */
 
 /*
@@ -16283,11 +16284,11 @@
       // The flush is skipped on async threads (e.g. PSTimer): they share this session's context
       // but must not drive synchronous screen-def RPCs concurrently with the main conversation thread.
       if (!inClientFlush &&
-          !SessionManager.get().isAsyncThread(Thread.currentThread()) &&
           (definitions.size() > 0 ||
            !level2definitions.isEmpty() ||
            descriptions.size() > 0 ||
-           !level2descriptions.isEmpty()))
+           !level2descriptions.isEmpty()) &&
+          !SessionManager.get().isAsyncThread(Thread.currentThread()))
       {
          inClientFlush = true;
          try

=== modified file 'src/com/goldencode/p2j/ui/ServerConfigManager.java'
--- old/src/com/goldencode/p2j/ui/ServerConfigManager.java	2026-08-03 15:52:49 +0000
+++ new/src/com/goldencode/p2j/ui/ServerConfigManager.java	2026-09-25 06:45:40 +0000
@@ -27,6 +27,7 @@
 **                  the server context.
 ** 012 CA  20260803 FWD Client can run entirely in the FWD Server JVM now - so any constant field is marked as
 **                  an explicit constant.
+** 013 TG  20260924 Apply the config updates in bulk via the generated ConfigAccessor.
 */ 
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -224,14 +225,7 @@
       final WidgetConfig backup = backupConfigs.get(wid);
 
       // apply the changed fields to active
-      for (int fidx = 0; fidx < cfgUpdate.changedAttributes.length; fidx++)
-      {
-         Object val = cfgUpdate.updates[fidx];
-         
-         int fid = cfgUpdate.changedAttributes[fidx];
-         
-         setWidgetConfig(active, val, wdef, fid);
-      }
+      setWidgetConfigs(active, wdef, cfgUpdate.changedAttributes, cfgUpdate.updates);
       
       // do post-processing - backup always reflects the previous state
       if (widget != null)
@@ -240,13 +234,6 @@
       }
 
       // apply the changed fields to backup
-      for (int fidx = 0; fidx < cfgUpdate.changedAttributes.length; fidx++)
-      {
-         Object val = cfgUpdate.updates[fidx];
-         
-         int fid = cfgUpdate.changedAttributes[fidx];
-         
-         setWidgetConfig(backup, val, wdef, fid);
-      }
+      setWidgetConfigs(backup, wdef, cfgUpdate.changedAttributes, cfgUpdate.updates);
    }
 }

=== modified file 'src/com/goldencode/p2j/ui/WidgetConfig.java'
--- old/src/com/goldencode/p2j/ui/WidgetConfig.java	2026-03-27 08:16:54 +0000
+++ new/src/com/goldencode/p2j/ui/WidgetConfig.java	2026-09-25 09:30:39 +0000
@@ -39,6 +39,7 @@
 **                  to be passed to GenericWidget’s getAttr() and setAttr() as non-capturing lambdas
 **                  for improved performance.
 ** 023 RNC 20260327 Added the 'getMenuMouse()' method.
+** 024 TG  20260925 Added trackingManager(), which caches the client config manager of this config.
 */ 
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -226,7 +227,7 @@
     */
    public transient ZOrderClass zorder = ZOrderClass.WIDGET;
    
-   /** Config manager */
+   /** The client-side config manager which tracks the changes of this config, see trackingManager(). */
    private transient ConfigManager configManager;
    
    /** Bitwise mask to mark ENABLED atribute usage.  */
@@ -469,6 +470,25 @@
       cm.setDynamicConfig(this, "help", String.class, help);
    }
 
+   /**
+    * Get the client-side config manager which tracks the changes of this config, resolved on first use.
+    * A config instance is used only in the context in which it was created.
+    * <p>
+    * The assignment of the cached field is excluded from the {@link SyncConfigChangesAspect}.
+    *
+    * @return   The context's {@link ClientConfigManager}, or <code>null</code> if there is none.
+    */
+   public ClientConfigManager trackingManager()
+   {
+      ConfigManager cm = configManager;
+      if (cm == null)
+      {
+         cm = ClientConfigManager.getInstance();
+         configManager = cm;
+      }
+
+      return (ClientConfigManager) cm;
+   }
 
    /**
     * Getter for the {@link WidgetConfig#frameId} variable.

=== modified file 'src/com/goldencode/p2j/ui/WidgetConfigDef.java'
--- old/src/com/goldencode/p2j/ui/WidgetConfigDef.java	2026-04-14 12:14:55 +0000
+++ new/src/com/goldencode/p2j/ui/WidgetConfigDef.java	2026-09-25 06:45:17 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2014-2026, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- ---------------------------------Description----------------------------------
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
 ** 001 CA  20140924 Created initial version.
 ** 002 VIG 20150121 Added menu components configs to newInstance method implementation.
 ** 003 HC  20150913 Removed ScrollPaneConfig class. Scrolling support overhaul. 
@@ -28,6 +28,7 @@
 **     CA  20200922 Replaced accessIdx map with an array, where the index is the internal field ID.
 **     TJD 20220504 Java 11 compatibility related minor changes
 ** 016 TG  20260414 Replaced ReflectASM with MethodHandles API invocation.
+** 017 TG  20260924 Replaced the VarHandles with a generated ConfigAccessor (prebuilt or at runtime);
 */
 
 /*
@@ -85,7 +86,6 @@
 
 package com.goldencode.p2j.ui;
 
-import java.lang.invoke.*;
 import java.lang.reflect.*;
 import java.util.*;
 
@@ -127,12 +127,13 @@
    final Map<String, Integer> fieldIds = new HashMap<>(); 
    
    /**
-    * The Java reflection fields mapped by our internal FWD field ID used in {@link #fieldIds}.
-    * We use VarHandle instead of Fields because we want to use JNIs directly.
-    * Java Reflection API has an adaptive mechanism which creates a proxy and uses it until reaches
-    * the inflation threshold.
+    * The accessor of the fields, using the field IDs from {@link #fieldIds}; {@code null} for a
+    * definition created only to resolve the tracked fields.
     */
-   public final VarHandle[] fields;
+   final ConfigAccessor accessor;
+
+   /** The tracked fields, indexed by field ID; {@code null} once the accessor is created. */
+   final Field[] trackedFields;
 
    /**
     * A set of field IDs which can be ignored when the configuration is copied from one side
@@ -148,6 +149,17 @@
    
    /** Flag indicating if this field is tracked for changed value (when true) or always.*/
    final boolean[] trackChanged;
+
+   /** The definitions resolved for each widget configuration class. */
+   private static final ClassValue<WidgetConfigDef> DEFS = new ClassValue<WidgetConfigDef>()
+   {
+      @Override
+      @SuppressWarnings("unchecked")
+      protected WidgetConfigDef computeValue(Class<?> type)
+      {
+         return new WidgetConfigDef((Class<? extends WidgetConfig>) type, true);
+      }
+   };
    
    /**
     * Constructor used on the side which resolves widget config definition from the concrete
@@ -161,8 +173,11 @@
     * 
     * @param    cfgCls
     *           The widget configuration class which needs to be resolved.
+    * @param    createAccessor
+    *           {@code true} to create the field accessor, {@code false} to only resolve the tracked
+    *           fields.
     */
-   public WidgetConfigDef(Class<? extends WidgetConfig> cfgCls)
+   private WidgetConfigDef(Class<? extends WidgetConfig> cfgCls, boolean createAccessor)
    {
       this.cfgCls = cfgCls;
       
@@ -178,9 +193,12 @@
       Arrays.fill(ignoredFields, false);
       trackChanged = new boolean[fieldCount];
       Arrays.fill(trackChanged, true);
-      fields = new VarHandle[fieldCount];
-
-      postprocess(cfgCls);
+      Field[] fields = new Field[fieldCount];
+
+      postprocess(cfgCls, fields);
+
+      accessor      = createAccessor ? ConfigAccessorFactory.create(cfgCls, fields) : null;
+      trackedFields = createAccessor ? null : fields;
    }
    
    /**
@@ -342,6 +360,34 @@
          throw new IllegalStateException("Config class not set!");
       }
    }
+
+   /**
+    * Get the definition of the given widget configuration class.  The definition is resolved
+    * once for each class and shared, as it is not changed after it is created.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    *
+    * @return   See above.
+    */
+   static WidgetConfigDef forClass(Class<? extends WidgetConfig> cfgCls)
+   {
+      return DEFS.get(cfgCls);
+   }
+
+   /**
+    * Resolve the tracked fields of the given widget configuration class, in field ID order,
+    * without creating the field accessor.
+    *
+    * @param    cfgCls
+    *           The widget configuration class.
+    *
+    * @return   The tracked fields, indexed by field ID.
+    */
+   static Field[] trackedFields(Class<? extends WidgetConfig> cfgCls)
+   {
+      return new WidgetConfigDef(cfgCls, false).trackedFields;
+   }
    
    /**
     * Performs postprocessing of the fields defining in the class or one of the parent classes
@@ -356,14 +402,16 @@
     * 
     * @param    cls
     *           The widget class from where the fields will be read.
+    * @param    fields
+    *           Receives the fields declared by the class, indexed by field ID.
     */
    @SuppressWarnings("unchecked")
-   private void postprocess(Class<? extends WidgetConfig> cls)
+   private void postprocess(Class<? extends WidgetConfig> cls, Field[] fields)
    {
       Class<?> parent = cls.getSuperclass();
       if (parent != null && WidgetConfig.class.isAssignableFrom(parent))
       {
-         postprocess((Class<? extends WidgetConfig>) parent);
+         postprocess((Class<? extends WidgetConfig>) parent, fields);
       }
       
       // get the declared fields for this class
@@ -402,17 +450,7 @@
          }
          
          workers[fid] = NativeTypeSerializer.getWorker(type);
-         f.setAccessible(true);
-         try
-         {
-            fields[fid] = java.lang.invoke.MethodHandles.privateLookupIn(
-               f.getDeclaringClass(), java.lang.invoke.MethodHandles.lookup()
-            ).unreflectVarHandle(f);
-         }
-         catch (IllegalAccessException e)
-         {
-            throw new RuntimeException("Could not unreflect varhandle for field: " + fname, e);
-         }
+         fields[fid] = f;
          
          fieldIds.put(fname, fid);
       }

=== modified file 'src/com/goldencode/p2j/ui/chui/ThinClient.java'
--- old/src/com/goldencode/p2j/ui/chui/ThinClient.java	2026-09-09 12:37:30 +0000
+++ new/src/com/goldencode/p2j/ui/chui/ThinClient.java	2026-09-25 11:07:05 +0000
@@ -3229,6 +3229,10 @@
 **                            forcing quirk for the mouse wheel originated events when inactive window
 **                            scrolling is enabled.
 ** 1111 CA  20260803          FWD Client can run entirely in the FWD Server JVM now, even in non-MSA mode.
+** 1112 TG  20260924          Batched server widget attributes are applied via setConfigFields.
+** 1113 TG  20260925          In redirected mode, UNDERLINE first switches the redirected terminal to its frame.
+**      TG  20260925          getInstance() does not lock after initializePrep() has finished.
+**      TG  20260925          pushWidgetAttr applies the pushed values also to the backup config, at the end.
 */
 
 /*
@@ -3902,6 +3906,9 @@
 
    /** The config manager instance */
    private ClientConfigManager cfgManager;
+   
+   /** Flag indicating that {@link #initializePrep} has finished initializing this instance. */
+   private volatile boolean initialized = false;
 
    /** Error writer used in MSA context */
    private ErrorWriter msaErrorWriter = new ErrorWriterBatch(this);
@@ -3999,9 +4006,19 @@
     *           been called. Otherwise, the singleton instance of this class
     *           will be returned.
     */
-   public static synchronized ThinClient getInstance()
+   public static ThinClient getInstance()
    {
-      return local.get();
+      ThinClient tc = local.get();
+      if (tc != null && tc.initialized)
+      {
+         return tc;
+      }
+      
+      // wait for initializePrep to finish
+      synchronized (ThinClient.class)
+      {
+         return local.get();
+      }
    }
 
    /**
@@ -4217,7 +4234,11 @@
             OutputManager.initErrorWriter();
          }
 
-         local.get().loginPageAddress = cfg.getString(ConfigItem.LOGIN_PAGE, null);
+         ThinClient tc = local.get();
+         tc.loginPageAddress = cfg.getString(ConfigItem.LOGIN_PAGE, null);
+         
+         // from now on, getInstance does not need to wait for this method
+         tc.initialized = true;
       }
    }
    
@@ -7289,6 +7310,14 @@
 
       final Frame frame = getFrame(frameId);
 
+      // in redirected mode, the terminal must be owned by this frame before drawing (as in view_):
+      // switching flushes and hides the previous owner, otherwise the window repaint below renders
+      // that frame's current (possibly just cleared) row into the terminal it still owns
+      if (isRedirected() && frame != null)
+      {
+         switchFrames(currentStream, frameId);
+      }
+
       eventDrawingBracket(frame, () ->
       {
          if (!frame.isDown())
@@ -11575,8 +11604,16 @@
     *           The widget iteration for down frames.
     * @param    code
     *           The code to run, which applies the changes.
+    * @param    backupCode
+    *           The code which applies the pushed values to the widget's backup config, after all the
+    *           processing of the changes is done.
     */
-   private void pushWidgetAttr(ConfigManager mgr, boolean frameVisible, int id, int iterDown, Runnable code)
+   private void pushWidgetAttr(ConfigManager mgr, 
+                               boolean       frameVisible, 
+                               int           id, 
+                               int           iterDown, 
+                               Runnable      code,
+                               Runnable      backupCode)
    {
       Widget<?> widget = getWidget(id, iterDown);
       if (widget == null)
@@ -11631,8 +11668,10 @@
             widget.postprocessConfig();
          }
 
-         // don't apply the changes to backup config copy now, the original state is still needed in order for
-         // field changes tracking in ConfigManager.getConfigUpdates to detect changes
+         // the original state of the backup config was needed until now; the pushed values are known by
+         // the server-side, so the backup must reflect them (otherwise a later client-side change back
+         // to the old value would not be detected by ConfigManager.getConfigUpdates)
+         backupCode.run();
       });
    }
    
@@ -11659,6 +11698,14 @@
          WidgetId wid = widget.getId();
          
          mgr.setConfigField(wid, widget.config(), fname, value);
+      },
+      () -> 
+      {
+         Widget widget = getWidget(id);
+         if (widget != null)
+         {
+            mgr.setBackupField(widget.getId(), fname, value);
+         }
       });
    }
 
@@ -11703,9 +11750,14 @@
          WidgetId wid = widget.getId();
          WidgetConfig wcfg = widget.config();
 
-         for (int i = 0; i < fids.length; i++)
+         mgr.setConfigFields(wid, wcfg, fids, values);
+      },
+      () ->
+      {
+         Widget widget = getWidget(id, iter);
+         if (widget != null)
          {
-            mgr.setConfigField(wid, wcfg, fids[i], values[i]);
+            mgr.setBackupFields(widget.getId(), fids, values);
          }
       });
    }

=== modified file 'src/com/goldencode/p2j/ui/client/FillIn.java'
--- old/src/com/goldencode/p2j/ui/client/FillIn.java	2026-08-18 12:14:52 +0000
+++ new/src/com/goldencode/p2j/ui/client/FillIn.java	2026-09-25 10:11:42 +0000
@@ -553,6 +553,7 @@
 ** 239 RNC 20260818          Added option to construct a presentation without reporting format errors.
 **                           A delete key applied to an overflowed numeric value should clear the GUI widget
 **                           and post a VALUE-CHANGED event.
+** 240 TG  20260925          Added getHeaderDeltaWidth(), used by FrameLayoutCache.
 */
 
 /*
@@ -3132,6 +3133,16 @@
    {
       return drawLine;
    }
+
+   /**
+    * Get the width delta of a header widget, see {@link #setHeaderParm}.
+    * 
+    * @return  The width delta.
+    */
+   double getHeaderDeltaWidth()
+   {
+      return deltaWidth;
+   }
    
    /**
     * Get widget height.

=== modified file 'src/com/goldencode/p2j/ui/client/Frame.java'
--- old/src/com/goldencode/p2j/ui/client/Frame.java	2026-07-06 10:27:18 +0000
+++ new/src/com/goldencode/p2j/ui/client/Frame.java	2026-09-25 11:08:36 +0000
@@ -772,6 +772,8 @@
 ** 344 RNC 20260220          Avoid storing duplicate lambdas as remove listeners.
 ** 345 RNC 20260225          Added the 'getScrollPane()' method.
 ** 346 SB  20260706          Added 'isFramePane'.
+** 347 TG  20260925          relocateComponent assigns the frame offsets only when they change.
+**                           In redirected ChUI output, unchanged layout passes are skipped (FrameLayoutCache)
 */
 
 /*
@@ -897,6 +899,9 @@
 
    /** Saved frame pane dimension. */
    private Dimension savedPaneDim = null;
+   
+   /** The result of the last layout passes, in redirected ChUI output; see {@link FrameLayoutCache}. */
+   private FrameLayoutCache<O> layoutCache = null;
 
    /** Track dynamic down. */
    private boolean dynamicDown = false;
@@ -1984,7 +1989,30 @@
          d = restoreFrameDetails();
       }
 
-      Dimension dMin = getLayout().minimumSize(createTempContainer(null));
+      TempContainer<O> measured = createTempContainer(null);
+      
+      // in redirected ChUI output, the layout passes are skipped if they would reproduce the current
+      // state, see FrameLayoutCache
+      FrameLayoutCache<O> cached = layoutCache;
+      FrameLayoutCache<O> cache = null;
+      boolean hit = false;
+      layoutCache = null;
+      
+      if (FrameLayoutCache.ENABLED && 
+          isRedirected()           && 
+          screen().isChui()        && 
+          getLayout() instanceof ZeroColumnLayout)
+      {
+         Widget<O>[] widgets = measured.widgets();
+         hit = cached != null && cached.matches(this, widgets);
+         if (!hit || FrameLayoutCache.CHECK)
+         {
+            cache = new FrameLayoutCache<>(this, widgets);
+         }
+      }
+      boolean skip = hit && !FrameLayoutCache.CHECK;
+      
+      Dimension dMin = skip ? cached.minimumSize() : getLayout().minimumSize(measured);
 
       int titleLen = config.title == null ? 0 : config.title.length();
       boolean readjust = false;
@@ -2040,7 +2068,30 @@
          setInnerAndOuterSize(d, titleLen);
       }
 
-      getLayout().doLayout(createTempContainer(dMin));
+      if (skip)
+      {
+         layoutCache = cached;
+      }
+      else
+      {
+         if (cache != null)
+         {
+            cache.beforePlacement(this, dMin);
+         }
+         
+         getLayout().doLayout(createTempContainer(dMin));
+         
+         if (cache != null && cache.afterPlacement(this))
+         {
+            layoutCache = cache;
+         }
+         
+         if (hit)
+         {
+            // check mode: the layout passes should have reproduced the cached state
+            cached.verify(this, layoutCache);
+         }
+      }
 
       if (readjust)
          adjustDown();
@@ -8035,8 +8086,18 @@
       BaseConfig bcSrc = UiUtils.getConfig(source, BaseConfig.class);
       if (bcDest != null && bcSrc != null)
       {
-         bcDest.frameRowOffset = step + bcSrc.frameRowOffset;
-         bcDest.frameYOffset = dest.screen().coordinates().rowToPixels(bcDest.frameRowOffset);
+         // assign only on change, an assignment marks the config as dirty
+         double rowOffset = step + bcSrc.frameRowOffset;
+         if (bcDest.frameRowOffset != rowOffset)
+         {
+            bcDest.frameRowOffset = rowOffset;
+         }
+         
+         int yOffset = dest.screen().coordinates().rowToPixels(bcDest.frameRowOffset);
+         if (bcDest.frameYOffset != yOffset)
+         {
+            bcDest.frameYOffset = yOffset;
+         }
       }
       
       if (bcDest != null)

=== added file 'src/com/goldencode/p2j/ui/client/FrameLayoutCache.java'
--- old/src/com/goldencode/p2j/ui/client/FrameLayoutCache.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/ui/client/FrameLayoutCache.java	2026-09-25 10:20:38 +0000
@@ -0,0 +1,504 @@
+/*
+** Module   : FrameLayoutCache.java
+** Abstract : The result of the last layout pass of a redirected ChUI frame, to skip unchanged layouts.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------Description----------------------------------
+** 001 TG  20260925 Created initial version.
+*/
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+** 
+** Additional terms under GNU Affero GPL version 3 section 7:
+** 
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+** 
+**   0. Attribution Requirement.
+** 
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+** 
+**   1. No License To Use Trademarks.
+** 
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+** 
+**   2. No Misrepresentation of Affiliation.
+** 
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+** 
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+** 
+**   3. No Misrepresentation of Source or Origin.
+** 
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+package com.goldencode.p2j.ui.client;
+
+import java.util.*;
+import java.util.logging.*;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.widget.*;
+import com.goldencode.p2j.util.logging.*;
+
+/**
+ * The result of the last {@link ZeroColumnLayout} pass of a redirected ChUI frame.  In redirected
+ * output, a frame is laid out each time it is shown again (see <code>ThinClient.placeFrameInt</code>),
+ * which in a report is nearly each time it is displayed.  When nothing the layout depends on has
+ * changed since the last layout, and nothing the layout produced has been changed since, the layout
+ * passes (the minimum size measurement and the placement) would produce the current state again, and
+ * are skipped.  The rest of <code>Frame.doLayoutWorker</code> (frame sizing, DOWN body) always runs.
+ * <p>
+ * The layout passes are skipped when:
+ * <ul>
+ *   <li>the same widgets (identity and order) are laid out;</li>
+ *   <li>the frame config and the content pane size, before the layout passes, are the same as before
+ *       the last layout passes;</li>
+ *   <li>the config of each widget and of its label is the same as after the last layout passes;</li>
+ *   <li>the geometry and the layout state of each widget and of its label are the same as after the
+ *       last layout passes;</li>
+ *   <li>the label texts the layout would use are the same.</li>
+ * </ul>
+ * Nothing is recorded if the layout passes change the frame config (they would not be idempotent).
+ * <p>
+ * The cache can be disabled with the <code>fwd.layout.cache.disabled</code> system property.  With
+ * the <code>fwd.layout.cache.check</code> system property, the layout passes are always executed and
+ * when the cache would have skipped them, any difference in their result is logged as a warning.
+ *
+ * @param    <O>
+ *           The output manager type.
+ */
+final class FrameLayoutCache<O extends OutputManager<?>>
+{
+   /** Logger. */
+   private static final CentralLogger LOG = CentralLogger.get(FrameLayoutCache.class);
+
+   /** Flag indicating if the cache is used. */
+   static final boolean ENABLED = !Boolean.getBoolean("fwd.layout.cache.disabled");
+
+   /** Flag indicating if the layout passes are always executed, to check the cache. */
+   static final boolean CHECK = Boolean.getBoolean("fwd.layout.cache.check");
+
+   /** The config fields which are not compared: the ID is not copied by {@link WidgetConfig#applyConfig}. */
+   private static final Set<String> IGNORED = Collections.singleton("id");
+
+   /** The laid out widgets. */
+   private final Widget<O>[] widgets;
+
+   /** A copy of the frame config, before the layout passes. */
+   private final WidgetConfig frameConfig;
+
+   /** The size of the frame's content pane, before the layout passes. */
+   private final Dimension paneSize;
+
+   /** The label texts for each widget, see {@link #labelTexts}. */
+   private final String[] labelTexts;
+
+   /** Copies of the widget and label configs, after the layout passes; see {@link #configsOf}. */
+   private WidgetConfig[] configs;
+
+   /** The geometry and layout state hash, after the layout passes. */
+   private long geometry;
+
+   /** The same as {@link #geometry}, but without the identity of the labels. */
+   private long content;
+
+   /** The geometry description, after the layout passes (only in check mode). */
+   private String geometryText;
+
+   /** A copy of the frame config, just before the placement pass. */
+   private WidgetConfig placementConfig;
+
+   /** The minimum size, computed by the measurement pass. */
+   private Dimension minSize;
+
+   /**
+    * Capture the state before the layout passes.
+    *
+    * @param    frame
+    *           The frame.
+    * @param    widgets
+    *           The widgets to lay out.
+    */
+   FrameLayoutCache(Frame<O> frame, Widget<O>[] widgets)
+   {
+      this.widgets     = widgets.clone();
+      this.frameConfig = cm().snapshotConfig(frame.config());
+      this.paneSize    = frame.getContentPane().dimension().copy();
+      this.labelTexts  = labelTexts(frame, widgets);
+   }
+
+   /**
+    * Check if the layout passes can be skipped, because they would produce the current state.
+    *
+    * @param    frame
+    *           The frame.
+    * @param    widgets
+    *           The widgets to lay out.
+    *
+    * @return   <code>true</code> if the layout passes can be skipped.
+    */
+   boolean matches(Frame<O> frame, Widget<O>[] widgets)
+   {
+      if (configs == null || !Arrays.equals(this.widgets, widgets))
+      {
+         return false;
+      }
+
+      ClientConfigManager cm = cm();
+
+      if (!paneSize.equals(frame.getContentPane().dimension()) ||
+          cm.firstDifference(frameConfig, frame.config(), IGNORED) != null)
+      {
+         return false;
+      }
+
+      if (!Arrays.equals(labelTexts, labelTexts(frame, widgets)))
+      {
+         return false;
+      }
+
+      WidgetConfig[] current = configsOf(widgets, false);
+      for (int i = 0; i < current.length; i++)
+      {
+         if (current[i] == null || configs[i] == null)
+         {
+            if (current[i] != configs[i])
+            {
+               return false;
+            }
+         }
+         else if (cm.firstDifference(configs[i], current[i], IGNORED) != null)
+         {
+            return false;
+         }
+      }
+
+      return geometry(widgets, null, true) == geometry;
+   }
+
+   /**
+    * Get the minimum size computed by the last measurement pass.
+    *
+    * @return   A copy of the minimum size.
+    */
+   Dimension minimumSize()
+   {
+      return minSize.copy();
+   }
+
+   /**
+    * Capture the frame config just before the placement pass.
+    *
+    * @param    frame
+    *           The frame.
+    * @param    minSize
+    *           The minimum size computed by the measurement pass.
+    */
+   void beforePlacement(Frame<O> frame, Dimension minSize)
+   {
+      this.minSize = minSize.copy();
+      this.placementConfig = cm().snapshotConfig(frame.config());
+   }
+
+   /**
+    * Capture the state after the layout passes.
+    *
+    * @param    frame
+    *           The frame.
+    *
+    * @return   <code>true</code> if the state was recorded; <code>false</code> if the placement pass
+    *           changed the frame config, in which case this instance must not be used.
+    */
+   boolean afterPlacement(Frame<O> frame)
+   {
+      if (cm().firstDifference(placementConfig, frame.config(), IGNORED) != null)
+      {
+         return false;
+      }
+
+      configs = configsOf(widgets, true);
+      StringBuilder text = CHECK ? new StringBuilder() : null;
+      geometry = geometry(widgets, null, true);
+      content = geometry(widgets, text, false);
+      geometryText = CHECK ? text.toString() : null;
+
+      return true;
+   }
+
+   /**
+    * In check mode, compare the result of the layout passes which the cache would have skipped with
+    * this (cached) state, and log any difference.
+    *
+    * @param    frame
+    *           The frame.
+    * @param    actual
+    *           The state recorded by the layout passes executed for the check, may be
+    *           <code>null</code> if they changed the frame config.
+    */
+   void verify(Frame<O> frame, FrameLayoutCache<O> actual)
+   {
+      String diff = null;
+      if (actual == null)
+      {
+         diff = "the placement pass changed the frame config";
+      }
+      else if (!minSize.equals(actual.minSize))
+      {
+         diff = "minimum size " + minSize + " vs " + actual.minSize;
+      }
+      else if (content != actual.content)
+      {
+         StringBuilder sb = new StringBuilder("geometry");
+         String[] a = geometryText.split("\n");
+         String[] b = actual.geometryText.split("\n");
+         for (int i = 0; i < Math.max(a.length, b.length); i++)
+         {
+            String x = i < a.length ? a[i] : "";
+            String y = i < b.length ? b[i] : "";
+            if (!x.equals(y))
+            {
+               sb.append("\n  cached: ").append(x).append("\n  actual: ").append(y);
+            }
+         }
+         diff = sb.toString();
+      }
+      else
+      {
+         ClientConfigManager cm = cm();
+         for (int i = 0; i < configs.length && diff == null; i++)
+         {
+            if (configs[i] != null && actual.configs[i] != null)
+            {
+               String f = cm.firstDifference(configs[i], actual.configs[i], IGNORED);
+               if (f != null)
+               {
+                  diff = "config field " + f + " of widget #" + i;
+               }
+            }
+         }
+      }
+
+      if (diff != null)
+      {
+         LOG.log(Level.WARNING, "Frame layout cache mismatch for frame " + frame.getId() + ": " + diff);
+      }
+   }
+
+   /**
+    * Get the configs of the widgets and of their labels, in a fixed order: each widget's config
+    * followed by its label's config (or <code>null</code> if it has no label).
+    *
+    * @param    widgets
+    *           The laid out widgets.
+    * @param    copy
+    *           <code>true</code> to return copies.
+    *
+    * @return   See above.
+    */
+   private static <O extends OutputManager<?>> WidgetConfig[] configsOf(Widget<O>[] widgets, boolean copy)
+   {
+      ClientConfigManager cm = copy ? cm() : null;
+      WidgetConfig[] res = new WidgetConfig[widgets.length * 2];
+
+      for (int i = 0; i < widgets.length; i++)
+      {
+         WidgetConfig cfg = widgets[i].config();
+         res[2 * i] = cfg == null ? null : copy ? cm.snapshotConfig(cfg) : cfg;
+
+         if (widgets[i] instanceof LabeledWidget)
+         {
+            Label<O> label = ((LabeledWidget<O>) widgets[i]).getLabelInstance();
+            WidgetConfig lcfg = label == null ? null : label.config();
+            res[2 * i + 1] = lcfg == null ? null : copy ? cm.snapshotConfig(lcfg) : lcfg;
+         }
+      }
+
+      return res;
+   }
+
+   /**
+    * Get the label texts the layout would use for each labeled widget, with and without headers.
+    *
+    * @param    frame
+    *           The frame.
+    * @param    widgets
+    *           The laid out widgets.
+    *
+    * @return   See above.
+    */
+   private static <O extends OutputManager<?>> String[] labelTexts(Frame<O> frame, Widget<O>[] widgets)
+   {
+      boolean sideLabels = frame.isSideLabels();
+      String[] res = new String[widgets.length * 2];
+
+      for (int i = 0; i < widgets.length; i++)
+      {
+         if (widgets[i] instanceof LabeledWidget)
+         {
+            LabeledWidget<?> lw = (LabeledWidget<?>) widgets[i];
+            res[2 * i]     = ZeroColumnLayout.getLabel(lw, sideLabels, false);
+            res[2 * i + 1] = ZeroColumnLayout.getLabel(lw, sideLabels, true);
+         }
+      }
+
+      return res;
+   }
+
+   /**
+    * Compute the hash of the geometry and layout state of the widgets and of their labels.
+    *
+    * @param    widgets
+    *           The laid out widgets.
+    * @param    text
+    *           If not <code>null</code>, receives a description of the hashed state.
+    * @param    identity
+    *           <code>true</code> to include the identity of the labels.
+    *
+    * @return   The hash.
+    */
+   private static <O extends OutputManager<?>> long geometry(Widget<O>[] widgets, 
+                                                            StringBuilder text,
+                                                            boolean       identity)
+   {
+      long h = 17;
+
+      for (Widget<O> w : widgets)
+      {
+         h = widget(h, w, text);
+
+         if (w instanceof LabeledWidget)
+         {
+            LabeledWidget<O> lw = (LabeledWidget<O>) w;
+            h = mix(h, lw.wasVisible() ? 1 : 0, text, " wasVisible");
+
+            Label<O> label = lw.getLabelInstance();
+            h = mix(h, label == null ? 0 : identity ? System.identityHashCode(label) : 1, text, " label");
+            if (label != null)
+            {
+               h = widget(h, label, text);
+            }
+         }
+
+         if (w instanceof FillIn)
+         {
+            FillIn<O, ?> fin = (FillIn<O, ?>) w;
+            h = mix(h, fin.isDrawLine() ? 1 : 0, text, " drawLine");
+            h = mix(h, Double.doubleToLongBits(fin.getHeaderDeltaWidth()), text, " deltaWidth");
+         }
+      }
+
+      return h;
+   }
+
+   /**
+    * Add the geometry and state of a widget to a hash.
+    *
+    * @param    h
+    *           The hash so far.
+    * @param    w
+    *           The widget.
+    * @param    text
+    *           If not <code>null</code>, receives a description of the hashed state.
+    *
+    * @return   The new hash.
+    */
+   private static long widget(long h, Widget<?> w, StringBuilder text)
+   {
+      if (text != null)
+      {
+         text.append('\n').append(w.getClass().getSimpleName()).append(' ').append(w.getId()).append(':');
+      }
+
+      Point p = w.location();
+      NativePoint np = w.physicalLocation();
+      Dimension d = w.dimension();
+
+      h = mix(h, System.identityHashCode(w.parent()), text, " parent");
+      h = mix(h, Double.doubleToLongBits(p.x), text, " x");
+      h = mix(h, Double.doubleToLongBits(p.y), text, " y");
+      h = mix(h, np.x, text, " px");
+      h = mix(h, np.y, text, " py");
+      h = mix(h, Double.doubleToLongBits(d.width), text, " w");
+      h = mix(h, Double.doubleToLongBits(d.height), text, " h");
+      h = mix(h, w.isVisible() ? 1 : 0, text, " visible");
+      h = mix(h, w.hidden() ? 1 : 0, text, " hidden");
+
+      if (w instanceof ClientDrivenLabel)
+      {
+         ClientDrivenLabel<?> l = (ClientDrivenLabel<?>) w;
+         h = mix(h, l.name().hashCode(), text, " name");
+         h = mix(h, l.isUnderlined() ? 1 : 0, text, " underlined");
+         h = mix(h, l.isRightAligned() ? 1 : 0, text, " rightAligned");
+         h = mix(h, Double.doubleToLongBits(l.offset), text, " offset");
+         h = mix(h, l.emptyMode ? 1 : 0, text, " emptyMode");
+         // a pending disable of the empty mode has an effect only if the empty mode is on
+         h = mix(h, l.emptyMode && l.disableEmptyMode ? 1 : 0, text, " disableEmptyMode");
+         h = mix(h, System.identityHashCode(l.getSideWidget()), text, " sideWidget");
+      }
+
+      return h;
+   }
+
+   /**
+    * Add a value to a hash.
+    *
+    * @param    h
+    *           The hash so far.
+    * @param    v
+    *           The value.
+    * @param    text
+    *           If not <code>null</code>, receives the value.
+    * @param    name
+    *           The name of the value, for the description.
+    *
+    * @return   The new hash.
+    */
+   private static long mix(long h, long v, StringBuilder text, String name)
+   {
+      if (text != null)
+      {
+         text.append(name).append('=').append(v);
+      }
+
+      return (h ^ v) * 0x9E3779B97F4A7C15L + 0x632BE59BD9B4E019L;
+   }
+
+   /**
+    * Get the client config manager.
+    *
+    * @return   See above.
+    */
+   private static ClientConfigManager cm()
+   {
+      return ClientConfigManager.getInstance();
+   }
+}

=== modified file 'src/com/goldencode/p2j/ui/client/LabeledDataContainer.java'
--- old/src/com/goldencode/p2j/ui/client/LabeledDataContainer.java	2025-02-26 12:04:48 +0000
+++ new/src/com/goldencode/p2j/ui/client/LabeledDataContainer.java	2026-09-25 09:26:21 +0000
@@ -51,6 +51,7 @@
 ** 035 EVL 20241122 Fixed the label visibility regression from previous change.  The setHidden() must also
 **                  have the side label case as an exception.
 ** 036 RNC 20241218 Change side label visibility only if this container changed its visibility.
+** 037 TG  20260925 setNoLabels assigns the labels flag only when it changes.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -314,7 +315,11 @@
    @Override
    public void setNoLabels(boolean labels)
    {
-      config().labels = !labels;
+      // assign only on change, an assignment marks the config as dirty
+      if (config().labels == labels)
+      {
+         config().labels = !labels;
+      }
    }
 
    /**

=== modified file 'src/com/goldencode/p2j/ui/client/OutputManager.java'
--- old/src/com/goldencode/p2j/ui/client/OutputManager.java	2026-08-03 15:52:49 +0000
+++ new/src/com/goldencode/p2j/ui/client/OutputManager.java	2026-09-25 08:54:35 +0000
@@ -103,6 +103,7 @@
 ** 062 AS  20260309 Reduced logging level to fine for ignored events due to invalidation.
 ** 063 RNC 20260807 Fixed NPE in setInvalidate() when draining events which have no source widget.
 ** 064 CA  20260803 FWD Client can run entirely in the FWD Server JVM now, even in non-MSA mode.
+** 065 TG  20260925 instance() and getDriver() do not lock, unless newInstance() is running.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -208,6 +209,9 @@
    /** Context local instance of the default terminal manager. */
    protected static final ContextLocal<OutputManager<?>> defaultTerminal = new ContextLocal<>();
    
+   /** Number of {@link #newInstance} calls in progress; {@link #instance} waits for them to finish. */
+   private static volatile int creating = 0;
+   
    /** logger */
    private static final CentralLogger LOG = CentralLogger.get(OutputManager.class);
 
@@ -305,6 +309,37 @@
    throws InterruptedException,
           InvocationTargetException
    {
+      creating++;
+      try
+      {
+         return newInstanceWorker(cfg, driver, allServerSide);
+      }
+      finally
+      {
+         creating--;
+      }
+   }
+   
+   /**
+    * Force construction of a new instance, see {@link #newInstance}.
+    *
+    * @param    cfg
+    *           The bootstrap configuration file.
+    * @param    driver
+    *           Implements low level input/output primitives.
+    * @param    allServerSide
+    *           Flag indicating the FWD client is running in the same JVM as the FWD server.
+    * 
+    * @return   The newly created instance.
+    * 
+    * @throws   InterruptedException 
+    * @throws   InvocationTargetException 
+    */
+   private static OutputManager<? extends OutputPrimitives> 
+   newInstanceWorker(BootstrapConfig cfg, ScreenDriver<?> driver, boolean allServerSide)
+   throws InterruptedException,
+          InvocationTargetException
+   {
       if (driver == null) 
       {
          String  id    = cfg.getString(ConfigItem.DRIVER_TYPE, CHUI_DRIVER_CONSOLE);
@@ -384,7 +419,26 @@
     * 
     * @return   The instance or <code>null</code> if none exists.
     */
-   public static synchronized OutputManager<? extends OutputPrimitives> instance()
+   public static OutputManager<? extends OutputPrimitives> instance()
+   {
+      if (creating > 0)
+      {
+         // wait for newInstance to finish
+         synchronized (OutputManager.class)
+         {
+            return instanceWorker();
+         }
+      }
+      
+      return instanceWorker();
+   }
+   
+   /**
+    * Get the context-specific instance, if it exists.
+    * 
+    * @return   The instance or <code>null</code> if none exists.
+    */
+   private static OutputManager<? extends OutputPrimitives> instanceWorker()
    {
       OutputManager value = defaultTerminal.get();
       
@@ -397,7 +451,7 @@
     * @return   The current driver or <code>null</code> if the output manager
     *           is not initialized.
     */
-   public static synchronized ScreenDriver<?> getDriver()
+   public static ScreenDriver<?> getDriver()
    {
       OutputManager<?> mgr = instance();
       

=== modified file 'src/com/goldencode/p2j/ui/client/WidgetRegistry.java'
--- old/src/com/goldencode/p2j/ui/client/WidgetRegistry.java	2026-08-05 12:03:00 +0000
+++ new/src/com/goldencode/p2j/ui/client/WidgetRegistry.java	2026-09-25 09:25:04 +0000
@@ -89,6 +89,7 @@
 ** 046 HC  20250728 ConfigManager class and its descendants refactored to force static reference
 **                  of concrete class. This is needed to allow client-side code to be executed in
 **                  the server context.
+** 047 TG  20260925 getComponent(int, int) searches the windows only once when the widget is not found.
 */
 
 /*
@@ -660,11 +661,7 @@
          w = getComponent(new WidgetId(id));
       }
 
-      if (w == null)
-      {
-         w = WindowManager.findWindow(id);
-      }
-
+      // no need to search the windows when w is null, getComponent(WidgetId) already did it
       return w;
    }
 

=== modified file 'src/com/goldencode/p2j/ui/client/ZeroColumnLayout.java'
--- old/src/com/goldencode/p2j/ui/client/ZeroColumnLayout.java	2025-07-28 08:11:21 +0000
+++ new/src/com/goldencode/p2j/ui/client/ZeroColumnLayout.java	2026-09-25 09:37:58 +0000
@@ -334,6 +334,7 @@
 ** 148 HC  20250728          ConfigManager class and its descendants refactored to force static
 **                           reference of concrete class. This is needed to allow client-side
 **                           code to be executed in the server context.
+** 149 TG  20260925          Header height is computed without labels; frame offsets are set only on change.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -520,6 +521,29 @@
                                     Label<?>         label,
                                     boolean          topLabel)
    {
+      return labelHeight(widget, labelText, label != null && label.isUnderlined(), topLabel);
+   }
+   
+   /**
+    * Compute the label height in character units, see 
+    * {@link #labelHeight(LabeledWidget, String, Label, boolean)}.
+    *
+    * @param    widget
+    *           The widget to which the label belongs.
+    * @param    labelText
+    *           The label text.
+    * @param    underlined
+    *           Flag identifying that the label is underlined.
+    * @param    topLabel
+    *           Flag identifying that the label is shown above the widget.
+    *
+    * @return   See above.
+    */
+   public static double labelHeight(LabeledWidget<?> widget,
+                                    String           labelText,
+                                    boolean          underlined,
+                                    boolean          topLabel)
+   {
       if (labelText == null)
       {
          // if the labels are on top, we reserve the current line
@@ -543,8 +567,7 @@
       }
       
       
-      return height + (label != null && label.isUnderlined() ? 1 :
-             ThinClient.getInstance().isChui() ? 0 : 0.5);
+      return height + (underlined ? 1 : ThinClient.getInstance().isChui() ? 0 : 0.5);
    }
    
    /**
@@ -2020,35 +2043,21 @@
             // safe, comparison of scaled doubles
             if (labelText != null && (labels || fin.isForceLabel()) && !fin.isNoLabels())
             {
-               ClientDrivenLabel<O> label = null;
-               Dimension sz = c[i].dimension();
-               
-               WidgetId nextId = c[i].getId().createId(WidgetId.nextID());
-               
+               // only the height is needed, no label instance is created: side labels are never
+               // underlined, top labels are underlined unless the frame has NO-UNDERLINE
+               boolean underlined = false;
                if (sideLabels)
                {
                   labelText += ": ";
-                  label =  factory.createLabel(nextId, labelText, c[i].config().frameId);
-                  label.setSideWidget(c[i]);
-                  sz.width = Coordinate.scale(sz.width +
-                                              labelWidth((LabeledWidget<?>) c[i], labelText));
                }
                else
                {
-                  // safe, comparison of scaled doubles
-                  if (labelWidth(fin, labelText) > sz.width)
-                     sz.width = labelWidth(fin, labelText);
-                  label = factory.createLabel(nextId, labelText, c[i].config().frameId);
-                  label.setUnderlined(isFrameUnderline(c[i].config().frameId));
-                  label.setRightAligned(fin.isRightAligned());
-                  label.setWidth(sz.width);
+                  underlined = isFrameUnderline(c[i].config().frameId);
                }
                
                // safe, comparison of scaled doubles
-               double lblHeight = labelHeight(fin, labelText, label, !sideLabels);
+               double lblHeight = labelHeight(fin, labelText, underlined, !sideLabels);
                maxHeight = maxHeight < lblHeight ? lblHeight : maxHeight;
-               
-               label.destroy();
             }
          }
          
@@ -2398,8 +2407,12 @@
          String labelText = getLabel(fin);
          Label<O> removed = fin.getLabelInstance();
 
+         // in the measurement pass (no placement), a label instance is needed only for the height of a
+         // GUI side label; the height of a ChUI side label is computed from its text
+         boolean needLabel = doPlacement || (sideLabels && !isChui);
+         
          ClientDrivenLabel<O> label = null;
-         WidgetId nextId = c[i].getId().createId(WidgetId.nextID());
+         WidgetId nextId = needLabel ? c[i].getId().createId(WidgetId.nextID()) : null;
          
          // side-label space width including colon and space
          double labelSpaceWidth = 0;
@@ -2417,12 +2430,18 @@
             labelWidth = labelWidth((LabeledWidget<?>) c[i], labelText);
             
             labelText += ": ";
-            label = factory.createLabel(nextId, labelText, c[i].config().frameId);
-            label.setRightAligned(ThinClient.getInstance().isChui());
-            label.setSideWidget(c[i]);
+            if (needLabel)
+            {
+               label = factory.createLabel(nextId, labelText, c[i].config().frameId);
+               label.setRightAligned(ThinClient.getInstance().isChui());
+               label.setSideWidget(c[i]);
+            }
             
             labelSpaceWidth = labelWidth((LabeledWidget<?>) c[i], labelText);
-            label.setWidth(labelSpaceWidth);
+            if (label != null)
+            {
+               label.setWidth(labelSpaceWidth);
+            }
             
             if (fin.getColon() != BaseConfig.INV_COORD)
             {
@@ -2443,7 +2462,10 @@
                      prevColumn = nextColumn;
                      colonColumn = nextColumn;
                      labelSpaceWidth = fin.getColon() + 2 - colonColumn; // label is cut
-                     label.setWidth(labelSpaceWidth);
+                     if (label != null)
+                     {
+                        label.setWidth(labelSpaceWidth);
+                     }
                   }
                }
             }
@@ -2451,7 +2473,7 @@
             sz.width = Coordinate.scale(sz.width + labelSpaceWidth);
             
             // the total widget height may be greater due to its label 
-            double lheight = label.height();
+            double lheight = label != null ? label.height() : chuiSideLabelHeight(fin, removed, labelText);
             if (sz.height < lheight)
             {
                sz.height = lheight;
@@ -2466,16 +2488,19 @@
                sz.width = lblWidth;
             }
             
-            label = factory.createLabel(nextId, labelText, c[i].config().frameId);
-            label.setUnderlined(isFrameUnderline(c[i].config().frameId));
-            label.setRightAligned(fin.isRightAligned());
-            label.setWidth(sz.width);
-            label.setOffset(deltaWidth);
-            // fix for UNLESS-HIDDEN option
-            if (fin.hidden() && !fin.wasVisible())
+            if (needLabel)
             {
-               label.setEmptyModeOn();
-               label.setEmptyModeOff();
+               label = factory.createLabel(nextId, labelText, c[i].config().frameId);
+               label.setUnderlined(isFrameUnderline(c[i].config().frameId));
+               label.setRightAligned(fin.isRightAligned());
+               label.setWidth(sz.width);
+               label.setOffset(deltaWidth);
+               // fix for UNLESS-HIDDEN option
+               if (fin.hidden() && !fin.wasVisible())
+               {
+                  label.setEmptyModeOn();
+                  label.setEmptyModeOff();
+               }
             }
             sz.height = Coordinate.scale(sz.height + maxHeight);
          }
@@ -2686,6 +2711,34 @@
       }
       
       /**
+       * Compute the height of a ChUI side label, as it would be computed for a new label instance
+       * created for this widget: the label takes the text of the widget's current label, if any (see
+       * {@link ClientDrivenLabel#setSideWidget}), and has one line for each line of its text.
+       *
+       * @param   fin
+       *          The labeled widget.
+       * @param   current
+       *          The widget's current label instance, may be <code>null</code>.
+       * @param   labelText
+       *          The side label text, including the colon.
+       *
+       * @return  The label height.
+       */
+      private double chuiSideLabelHeight(LabeledWidget<O> fin, Label<O> current, String labelText)
+      {
+         String text = labelText;
+         if (fin instanceof LabeledDataContainer && current != null)
+         {
+            WidgetConfig cfg = current.config();
+            text = cfg instanceof BaseConfig ? ((BaseConfig) cfg).name : "";
+         }
+         
+         int lines = (text == null ? "" : text).split("\n").length;
+         
+         return fin.screen().coordinates().heightFromNative(lines);
+      }
+      
+      /**
        * Handle SPACE widget.
        *
        * @param   i
@@ -3023,12 +3076,28 @@
             // widget column can not be different from the FRAME- attributes, as the 
             // FIELD-GROUP:COLUMN attribute is readonly.  This is left in place in case in the
             // future is found a case where these may be different (maybe nested frames?)
-            bc.frameColumnOffset = 0;
-            bc.frameXOffset = 0;
+            // (assign only on change, an assignment marks the config as dirty)
+            if (bc.frameColumnOffset != 0)
+            {
+               bc.frameColumnOffset = 0;
+            }
+            if (bc.frameXOffset != 0)
+            {
+               bc.frameXOffset = 0;
+            }
             
             // adjust the vertical offset: it includes the height of the frame's top-label header
-            bc.frameRowOffset = sideLabels ? 0 : distanceY;
-            bc.frameYOffset = w.screen().coordinates().rowToPixels(bc.frameRowOffset);
+            double rowOffset = sideLabels ? 0 : distanceY;
+            if (bc.frameRowOffset != rowOffset)
+            {
+               bc.frameRowOffset = rowOffset;
+            }
+            
+            int yOffset = w.screen().coordinates().rowToPixels(bc.frameRowOffset);
+            if (bc.frameYOffset != yOffset)
+            {
+               bc.frameYOffset = yOffset;
+            }
             
             bc.widgetPlaced = true;
          }

=== modified file 'src/com/goldencode/p2j/ui/client/chui/driver/AbstractChuiDriver.java'
--- old/src/com/goldencode/p2j/ui/client/chui/driver/AbstractChuiDriver.java	2024-07-15 14:07:29 +0000
+++ new/src/com/goldencode/p2j/ui/client/chui/driver/AbstractChuiDriver.java	2026-09-25 09:46:38 +0000
@@ -9,6 +9,7 @@
 ** 002 HC  20150702 Improved positioning of top-level windows on systems with multiple displays.
 ** 003 HC  20191023 Improved emulation of getSystemMetrics Win32 function.
 ** 004 GBB 20240709 Hard-coded config names replaced by ConfigItem constants.
+** 005 TG  20260925 ChUI drivers do not track widget state changes, unless they act as GUI.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -99,6 +100,18 @@
    }
 
    /**
+    * ChUI drivers do not use the widget state change notifications (a GUI batch program keeps them,
+    * as its widgets are GUI widgets).
+    * 
+    * @return   <code>true</code> only if this driver {@link #actsAsGui() acts as GUI}.
+    */
+   @Override
+   public boolean tracksWidgetState()
+   {
+      return actsAsGui();
+   }
+
+   /**
     * Read configuration values from the bootstrap configuration and convert
     * these into an options instance.
     * <p>

=== modified file 'src/com/goldencode/p2j/ui/client/driver/ScreenDriver.java'
--- old/src/com/goldencode/p2j/ui/client/driver/ScreenDriver.java	2025-01-28 13:24:27 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/ScreenDriver.java	2026-09-25 08:52:13 +0000
@@ -55,6 +55,7 @@
 **                  and UriPathAnchor.
 ** 035 GBB 20240301 getBrowserWebSocket() removed. Replaced by ScreenWebDriver.getBrowserDetails().
 ** 036 FER 20250127 Added actsAsGui() method.
+** 037 TG  20260925 Added tracksWidgetState().
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -522,6 +523,18 @@
    {
       // no-op by default
    }
+   
+   /**
+    * Check if this driver needs the {@link #widgetStateChanged} notifications (or the widgets need
+    * {@link com.goldencode.p2j.ui.client.widget.Widget#widgetStateChanged}).  When <code>false</code>,
+    * the widgets do not propagate the notification to their children.
+    * 
+    * @return   <code>true</code> by default.
+    */
+   public default boolean tracksWidgetState()
+   {
+      return true;
+   }
 
    /**
     * Updates the screen metrics.

=== modified file 'src/com/goldencode/p2j/ui/client/widget/AbstractContainer.java'
--- old/src/com/goldencode/p2j/ui/client/widget/AbstractContainer.java	2026-04-21 08:02:46 +0000
+++ new/src/com/goldencode/p2j/ui/client/widget/AbstractContainer.java	2026-09-25 08:52:13 +0000
@@ -152,6 +152,7 @@
 ** 078 RNC 20251118 Skip computing the occlusion state for widgets that are not visible.
 ** 079 LS  20260211 Changed logging level in 'detach' to FINE.
 ** 080 RNC 20260402 Fixed javadoc warnings.
+** 081 TG  20260925 widgetStateChanged is not propagated when the driver does not track it.
 */
 
 /*
@@ -218,6 +219,7 @@
 import com.goldencode.p2j.ui.*;
 import com.goldencode.p2j.ui.chui.ThinClient;
 import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.driver.ScreenDriver;
 import com.goldencode.p2j.ui.client.event.*;
 import com.goldencode.p2j.ui.client.layout.*;
 import com.goldencode.p2j.util.*;
@@ -1889,6 +1891,12 @@
    @Override
    public void widgetStateChanged()
    {
+      ScreenDriver<?> driver = screen().getInstanceDriver();
+      if (driver != null && !driver.tracksWidgetState())
+      {
+         return;
+      }
+      
       super.widgetStateChanged();
       widgets.forEach(w -> w.widgetStateChanged());
    }

=== modified file 'src/com/goldencode/p2j/ui/client/widget/AbstractWidget.java'
--- old/src/com/goldencode/p2j/ui/client/widget/AbstractWidget.java	2026-07-02 15:05:30 +0000
+++ new/src/com/goldencode/p2j/ui/client/widget/AbstractWidget.java	2026-09-25 09:26:21 +0000
@@ -264,6 +264,7 @@
 **                  dragging has started.
 ** 118 RNC 20260402 Fixed javadoc warnings.
 ** 119 RNC 20260225 Overridden the 'getMouseWheelTarget()' method.
+** 120 TG  20260925 The config's hidden flag is assigned only when it changes (an assignment marks it dirty).
 */
 
 /*
@@ -2934,9 +2935,11 @@
       }
       
       this.hidden = hidden;
-      if (config() != null)
+      WidgetConfig cfg = config();
+      if (cfg != null && cfg.hidden != hidden)
       {
-         config().hidden = hidden;
+         // assign only on change, an assignment marks the config as dirty
+         cfg.hidden = hidden;
       }
 
       if (mod)

=== modified file 'src/com/goldencode/p2j/util/FileStream.java'
--- old/src/com/goldencode/p2j/util/FileStream.java	2026-09-12 15:12:42 +0000
+++ new/src/com/goldencode/p2j/util/FileStream.java	2026-09-25 06:46:11 +0000
@@ -93,6 +93,7 @@
 ** 037 CA  20260320          Access to streams is now bi-directional: the stream is moved (and re-opened) by
 **                           the controlling side, and stays there until the other side needs it.
 ** 038 OM  20260505          In case of file truncation, the stream is reset in the attempt to fix the issue.
+** 039 TG  20260925          A flush no longer forces the data to the storage device (fsync).
 */
 
 /*
@@ -1163,10 +1164,8 @@
    protected void flushData()
    throws IOException
    {
-      if (file != null)
-      {
-         file.force(true);
-      }
+      // bytes written to the channel are already handed to the OS, which is the destination
+      // of a flush; forcing them to the storage device (fsync) on each flush is not needed
    }
    
    /**

