diff --git a/manual/luatex-modifications.tex b/manual/luatex-modifications.tex
index 8d67d7dfafe910be87afc786a9ef32a27e1ae1fd..875aefc166f2a80154492bface3f4e0f57a6f700 100644
--- a/manual/luatex-modifications.tex
+++ b/manual/luatex-modifications.tex
@@ -1164,6 +1164,10 @@ Because \type {\noindent} doesn't inject anything but a \type {\indent} injects
 an box, paragraphs with only an indent and directions are handled as paragraphs
 with content.
 
+By default paragraphs before a display equation containing dir nodes are never ignored.
+Changing that could break existing documents, but when you set \lpr {mathemptydisplaymode}
+to~\type {1} empty paragraphs before a display equation will be ignored.
+
 \stopsubsection
 
 \startsubsection[title={Controlling glue with \lpr {breakafterdirmode}}]
diff --git a/manual/luatex.pdf b/manual/luatex.pdf
index f2e14ab1d81e37ca3fc39a23e5b983f7fe472793..ec898c4e725d40ff2e156991412534c0ce3b4fb6 100644
Binary files a/manual/luatex.pdf and b/manual/luatex.pdf differ
diff --git a/manual/luatex.tex b/manual/luatex.tex
index 0bcb9e76196b08f83c2be7e585c17f8b9f17c5d6..d2453f8a9fbd1ccdcadf8b249b714b8a7e731f11 100644
--- a/manual/luatex.tex
+++ b/manual/luatex.tex
@@ -73,7 +73,7 @@
 \startdocument
   [manual=Lua\TeX,
    status=stable,
-   version=1.20]
+   version=1.21]
 
 \startnotmode[*export]
     \component luatex-titlepage
diff --git a/source/texk/web2c/luatexdir/ChangeLog b/source/texk/web2c/luatexdir/ChangeLog
index 022c5fa1855d6f727f0f39300655b37c71753969..75d657fc6b4d723b846a1d4ac5529132cf7d0078 100644
--- a/source/texk/web2c/luatexdir/ChangeLog
+++ b/source/texk/web2c/luatexdir/ChangeLog
@@ -1,3 +1,8 @@
+2025-02-05 Luigi Scarso <luigi.scarso@gmail.com>
+	* add mathemptydisplaymode primitive, 
+	* fix nodes leak in end_graf (thanks to udifoglle@gmail.com)
+
+
 2025-02-02 Luigi Scarso <luigi.scarso@gmail.com>
 	* Fixed subtype of beforedisplaypenalty (thanks to udifoglle@gmail.com)
 
diff --git a/source/texk/web2c/luatexdir/NEWS b/source/texk/web2c/luatexdir/NEWS
index c250af07b458c5b99952fe7e6ecd647fbd81a6b3..7df6e240271ec7374ff8cbc9fd5b07d2feee3e89 100644
--- a/source/texk/web2c/luatexdir/NEWS
+++ b/source/texk/web2c/luatexdir/NEWS
@@ -1,3 +1,28 @@
+==============================================================
+LuaTeX 1.21.0 2025-02-13
+==============================================================
+
+- LuaJIT 2.1.1736781742
+- New luaffi lib, supporting aarch64
+- New primitive \mathemptydisplaymode. By default paragraphs
+  before a display equation containing dir nodes are never ignored.
+  Changing that could break existing documents, but when you set mathemptydisplaymode
+  to 1 empty paragraphs before a display equation will be ignored.
+- New primitve \ignoreprimitiveerror.
+  If \ignoreprimitiveerror=1, the error
+  "Infinite glue shrinkage found in box being split" that occurs
+  when vsplit-ting a box containing infinite negative glue is turned into a warning.
+- In output_name_tree, sort_dest_names now compares
+  literal strings or hexdecimal strings.
+- Increment tally in tprint also when doterm is true 
+- For PNG, we no longer set the attr on the mask. If we get a report about dropping this 
+  we could add a 'maskattr' feature, but one should consider
+  to include a correct pdf of the image instead, it's more robust.
+- Handle token combiners in immediate assignments (H.Hagen)
+
+For more details, see ChangeLog.
+
+
 ==============================================================
 LuaTeX 1.18.0 2024-02-13
 ==============================================================
diff --git a/source/texk/web2c/luatexdir/luatex_svnversion.h b/source/texk/web2c/luatexdir/luatex_svnversion.h
index b28a95c89edb5b42b4853dad4a7016882c10349f..0be6c1305bd9b794ff48abd117d581c1d9264ecc 100644
--- a/source/texk/web2c/luatexdir/luatex_svnversion.h
+++ b/source/texk/web2c/luatexdir/luatex_svnversion.h
@@ -1,4 +1,4 @@
 #ifndef luatex_svn_revision_h
 #define luatex_svn_revision_h
-#define luatex_svn_revision 7656
+#define luatex_svn_revision 7657
 #endif
diff --git a/source/texk/web2c/luatexdir/tex/commands.c b/source/texk/web2c/luatexdir/tex/commands.c
index 371626daf53d3f0c59b9b2749e40fc37f26021c6..e794bf0f32283fd6bf1e4f8a2ffe4261074bd6c9 100644
--- a/source/texk/web2c/luatexdir/tex/commands.c
+++ b/source/texk/web2c/luatexdir/tex/commands.c
@@ -180,6 +180,7 @@ void initialize_commands(void)
     primitive_luatex("mathdefaultsmode", assign_int_cmd, int_base + math_defaults_mode_code, int_base);
     primitive_luatex("discretionaryligaturemode", assign_int_cmd, int_base + discretionary_ligature_mode_code, int_base);
     primitive_etex("partokencontext", assign_int_cmd, int_base + partoken_context_code, int_base);
+    primitive_luatex("mathemptydisplaymode", assign_int_cmd, int_base + math_empty_display_mode_code, int_base);
 
     /*tex
 
diff --git a/source/texk/web2c/luatexdir/tex/equivalents.h b/source/texk/web2c/luatexdir/tex/equivalents.h
index bf329b3a1be1c3cb273ef38cb76b7aeaaf1515ff..988d7ae395077428a7af789dba10d45d811b0639 100644
--- a/source/texk/web2c/luatexdir/tex/equivalents.h
+++ b/source/texk/web2c/luatexdir/tex/equivalents.h
@@ -318,7 +318,9 @@ the |number_regs| \.{\\dimen} registers.
 
 #  define ignore_primitive_error_code 125 				/*ignore some primitive/engine errors*/
 
-#  define math_option_code 126
+#  define math_empty_display_mode_code 126
+
+#  define math_option_code 127
 
 
 #  define mathoption_int_base_code (math_option_code+1)                 /* one reserve */
@@ -830,6 +832,8 @@ extern halfword last_cs_name;
 
 #define show_stream_par                    int_par(show_stream_code)
 
+#define math_empty_display_mode_par        int_par(math_empty_display_mode_code)
+
 /* */
 
 #define math_use_current_family_code 7
diff --git a/source/texk/web2c/luatexdir/tex/maincontrol.c b/source/texk/web2c/luatexdir/tex/maincontrol.c
index a66ad5439574abb690c14088875166d3f0ff053f..8af87ec9843f8058df84252286da2cbb8e583e5b 100644
--- a/source/texk/web2c/luatexdir/tex/maincontrol.c
+++ b/source/texk/web2c/luatexdir/tex/maincontrol.c
@@ -1964,7 +1964,7 @@ void end_graf(int line_break_context)
         if (head == tail) {
             pop_nest();
         } else if (only_dirs(vlink(head))) {
-            flush_node(vlink(head));
+            flush_node_list(vlink(head));
             pop_nest();
         } else {
             line_break(false, line_break_context);
diff --git a/source/texk/web2c/luatexdir/tex/texmath.c b/source/texk/web2c/luatexdir/tex/texmath.c
index a8de1808144e30d11459833a83069669318674da..3c069a5d6bfa67ca31d3af6684fcdc3800290be8 100644
--- a/source/texk/web2c/luatexdir/tex/texmath.c
+++ b/source/texk/web2c/luatexdir/tex/texmath.c
@@ -1084,6 +1084,18 @@ static boolean math_and_text_reversed_p(void)
 
 */
 
+static int only_dirs(halfword n)
+{
+    while (n) {
+        if (type(n) == local_par_node || type(n) == dir_node) {
+            n = vlink(n);
+        } else {
+            return 0;
+        }
+    }
+    return 1;
+}
+
 void enter_display_math(void)
 {
     /*tex new or partial |pre_display_size| */
@@ -1101,19 +1113,21 @@ void enter_display_math(void)
         \.{\$\${ }\$\$}
 
     */
-    if (head == tail ||
-        (vlink(head) == tail &&
-         type(tail) == local_par_node && vlink(tail) == null)) {
-        if (vlink(head) == tail) {
-            /*tex
-
-                |resume_after_display| inserts a |local_par_node|, but if there
-                is another display immediately following, we have to get rid of
-                that node.
-
-            */
-            flush_node(tail);
-        }
+    if (head == tail) {
+        pop_nest();
+        w = -max_dimen;
+    } else if ((vlink(head) == tail &&
+               type(tail) == local_par_node && vlink(tail) == null) ||
+               (math_empty_display_mode_par == 1 && only_dirs(vlink(head)))) {
+        /*tex
+
+            We ignore |null| paragraphs, that is those that only have a local par node,
+            for example because |resume_after_display| inserts a |local_par_node|.
+            If |\mathdisplayemptymode=1| paragraphs caontaining only local par node
+            and possibly a few dir nodes are ignored as well.
+
+        */
+        flush_node_list(vlink(head));
         pop_nest();
         w = -max_dimen;
     } else {
@@ -2561,54 +2575,32 @@ static void finish_displayed_math(boolean l, pointer eqno_box, pointer p)
     if (eqno_w != 0) {
         r = new_kern(line_w - eq_w - eqno_w - d);
         if (l) {
-            if (swap_dir) {
-                if (math_direction_par==dir_TLT) {
-                    /*tex TRT + TLT + \eqno: (swap_dir=true,  math_direction_par=TLT, l=true) */
-                    s = new_kern(width(r) + eqno_w);
-                } else {
-                    /*tex TLT + TRT + \eqno: (swap_dir=true,  math_direction_par=TRT, l=true) */
-                    s = new_kern(d);
-                }
-                try_couple_nodes(eqno_box,r);
-                try_couple_nodes(r,eq_box);
-                try_couple_nodes(eq_box,s);
+            if (math_direction_par==dir_TLT) {
+                /*tex TRT + TLT + \eqno: (swap_dir=true, math_direction_par=TLT, l=true) */
+                /*tex TLT + TLT + \leqno: (swap_dir=false, math_direction_par=TLT, l=true) */
+                s = new_kern(width(r) + eqno_w);
             } else {
-                if (math_direction_par==dir_TLT) {
-                    /*tex TLT + TLT + \leqno: (swap_dir=false, math_direction_par=TLT, l=true) */
-                    s = new_kern(width(r) + eqno_w);
-                } else {
-                    /*tex TRT + TRT + \leqno: (swap_dir=false, math_direction_par=TRT, l=true) */
-                    s = new_kern(d);
-                }
-                try_couple_nodes(eqno_box,r);
-                try_couple_nodes(r,eq_box);
-                try_couple_nodes(eq_box,s);
+                /*tex TLT + TRT + \eqno: (swap_dir=true, math_direction_par=TRT, l=true) */
+                /*tex TRT + TRT + \leqno: (swap_dir=false, math_direction_par=TRT, l=true) */
+                s = new_kern(d);
             }
+            try_couple_nodes(eqno_box,r);
+            try_couple_nodes(r,eq_box);
+            try_couple_nodes(eq_box,s);
             eq_box = eqno_box;
         } else {
-            if (swap_dir) {
-                if (math_direction_par==dir_TLT) {
-                    /*tex TRT + TLT + \leqno: (swap_dir=true,  math_direction_par=TLT, l=false) */
-                    s = new_kern(d);
-                } else {
-                    /*tex TLT + TRT + \leqno: (swap_dir=true,  math_direction_par=TRT, l=false) */
-                    s = new_kern(width(r) + eqno_w);
-                }
-                try_couple_nodes(s,eq_box);
-                try_couple_nodes(eq_box,r);
-                try_couple_nodes(r,eqno_box);
+            if (math_direction_par==dir_TLT) {
+                /*tex TRT + TLT + \leqno: (swap_dir=true,  math_direction_par=TLT, l=false) */
+                /*tex TLT + TLT + \eqno: (swap_dir=false, math_direction_par=TLT, l=false) */
+                s = new_kern(d);
             } else {
-                if (math_direction_par==dir_TLT) {
-                    /*tex TLT + TLT + \eqno: (swap_dir=false, math_direction_par=TLT, l=false) */
-                    s = new_kern(d);
-                } else {
-                    /*tex TRT + TRT + \eqno: (swap_dir=false, math_direction_par=TRT, l=false) */
-                    s = new_kern(width(r) + eqno_w);
-                }
-                try_couple_nodes(s,eq_box);
-                try_couple_nodes(eq_box,r);
-                try_couple_nodes(r,eqno_box);
+                /*tex TLT + TRT + \leqno: (swap_dir=true,  math_direction_par=TRT, l=false) */
+                /*tex TRT + TRT + \eqno: (swap_dir=false, math_direction_par=TRT, l=false) */
+                s = new_kern(width(r) + eqno_w);
             }
+            try_couple_nodes(s,eq_box);
+            try_couple_nodes(eq_box,r);
+            try_couple_nodes(r,eqno_box);
             eq_box = s;
         }
         eq_box = hpack(eq_box, 0, additional, -1);