TMD Viewer 2026.1 - Windows

TMD Viewer connects to a desktop provided by TMD Server. Install and start the
server on the remote computer and create a session owned by your account first.

1. Start the viewer
-------------------
Extract the complete viewer package and open tmdviewer.exe. From its folder:

  tmdviewer.exe

Keep the package's libraries, plugins, gfx and data beside the application.

2. Connect to a desktop
-----------------------
Enter the server host/IP in the host field, including the session ID, for example:
  server.example.com:8191#console

For a Linux virtual desktop named work:
  server.example.com:8191#work

Or provide the same address on the command line:
  tmdviewer.exe --host "server.example.com:8191#console"

WSS on TCP port 8191 is the default. The server's firewall must allow that port.
For the native TCP/OpenSSL transport on TCP 4815 instead:
  tmdviewer.exe --use-tcp --host "server.example.com:4815#console"

3. Authenticate and use the desktop
-----------------------------------
Verify and trust the expected server certificate when asked. Log in with the
session owner's operating-system username and password. The session ID selects
the desktop; it is not a username. Use an explicit ID when several sessions
exist. The server will reject an unknown, inactive or inaccessible session.

Once connected, select the desired monitor in the viewer's display controls.
Keyboard and pointer input act on the remote desktop. Copy in a remote
application or paste from the local clipboard as usual while clipboard control
is assigned to this viewer. Closing the viewer disconnects it; a Linux virtual
desktop continues to run until it is stopped or closed on the server.

4. Rendering and decoding options
---------------------------------
Rendering tries Vulkan, then OpenGL, then Cairo software rendering.
To prefer OpenGL:
  tmdviewer.exe --opengl

To use software rendering:
  tmdviewer.exe --software-rendering

To request software decoding for codecs that support it:
  tmdviewer.exe --software-decoding

Rendering and decoding are separate settings. Software H.264 decoding is not
provided; forcing software decoding does not enable it. Available codecs depend
on both endpoints and the installed GPU/driver.

5. Troubleshooting and browser alternative
------------------------------------------
If connection fails, check the host name, selected transport/port, server
service, session state and owner. A Linux console requires that owner to be
logged into its graphical desktop; a Linux virtual session must be active.

To connect without the native application, open:
  https://server.example.com:8191/#console

The server must include the browser bundle. Allow the browser's required
permissions. Browser settings are saved per browser profile and website origin;
use Ctrl+V to explicitly send local clipboard contents to the remote desktop.

Documentation and support
-------------------------
https://www.texturemind.com/tmd/documentation/
support@texturemind.com

Created by Gianpaolo Ingegneri
2026-2030 (c) All rights reserved
https://www.texturemind.com/

Third-party notices
-------------------
See THIRD-PARTY-LICENSES.txt for third-party license texts.
This software is based in part on the work of the Independent JPEG Group.

FFmpeg source and build information
----------------------------------
This software uses libraries from the FFmpeg project (https://ffmpeg.org/)
under the GNU Lesser General Public License, version 2.1 or later (LGPL-2.1+).
The license text is included in THIRD-PARTY-LICENSES.txt. FFmpeg source:
https://github.com/FFmpeg/FFmpeg.git
Licensing information: https://ffmpeg.org/legal.html
Recorded Windows source revision:

SHA-1: 6400860b9d4bec5ace91ec3a5e43ad76f9660579

* avformat/flvdec: initialize ret in flv_read_packet() to AVERROR_BUG

Recorded Windows build commands (Visual Studio 2022 environment, MSYS2 shell):

- 64-bit build

  CC=cl PKG_CONFIG_PATH="../../libaom/install2022x64/lib/pkgconfig:../../libvpx/install2022x64/lib/pkgconfig:../../libdav1d/installdir64/lib/pkgconfig" \
  ../configure --toolchain=msvc --prefix=../installdir_x64 --arch=x86_64 --enable-asm --enable-shared --disable-everything --enable-swscale --enable-avcodec \
  --enable-decoder=mpeg1video --enable-encoder=mpeg1video --enable-decoder=av1 --enable-decoder=h264 --enable-decoder=vp9 --enable-libaom --enable-encoder=libaom_av1 \
  --enable-libvpx --enable-decoder=libvpx_vp9 --enable-encoder=libvpx_vp9 --enable-libdav1d --enable-decoder=libdav1d --enable-dxva2 --enable-hwaccel=av1_dxva2 \
  --enable-hwaccel=h264_dxva2 --enable-hwaccel=vp9_dxva2 --enable-d3d11va --enable-hwaccel=av1_d3d11va --enable-hwaccel=av1_d3d11va2 --enable-hwaccel=h264_d3d11va \
  --enable-hwaccel=h264_d3d11va2 --enable-hwaccel=vp9_d3d11va --enable-hwaccel=vp9_d3d11va2 --enable-d3d12va --enable-hwaccel=av1_d3d12va --enable-hwaccel=h264_d3d12va \
  --enable-hwaccel=vp9_d3d12va --disable-avformat --disable-avfilter --disable-avdevice --disable-swresample --disable-postproc --extra-cflags="-MT"

- 32-bit build

  CC=cl PKG_CONFIG_PATH="../../libaom/install2022x86/lib/pkgconfig:../../libvpx/install2022x86/lib/pkgconfig:../../libdav1d/installdir32/lib/pkgconfig" \
  ../configure --toolchain=msvc --prefix=../installdir_x86 --arch=x86 --enable-asm --enable-shared --disable-everything --enable-swscale --enable-avcodec \
  --enable-decoder=mpeg1video --enable-encoder=mpeg1video --enable-decoder=av1 --enable-decoder=h264 --enable-decoder=vp9 --enable-libaom --enable-encoder=libaom_av1 \
  --enable-libvpx --enable-decoder=libvpx_vp9 --enable-encoder=libvpx_vp9 --enable-libdav1d --enable-decoder=libdav1d --enable-dxva2 --enable-hwaccel=av1_dxva2 \
  --enable-hwaccel=h264_dxva2 --enable-hwaccel=vp9_dxva2 --enable-d3d11va --enable-hwaccel=av1_d3d11va --enable-hwaccel=av1_d3d11va2 --enable-hwaccel=h264_d3d11va \
  --enable-hwaccel=h264_d3d11va2 --enable-hwaccel=vp9_d3d11va --enable-hwaccel=vp9_d3d11va2 --enable-d3d12va --enable-hwaccel=av1_d3d12va --enable-hwaccel=h264_d3d12va \
  --enable-hwaccel=vp9_d3d12va --disable-avformat --disable-avfilter --disable-avdevice --disable-swresample --disable-postproc --extra-cflags="-MT"

After each configure command, build and install from its separate build folder:
  make -j4
  make install

The H.264 patch below also applies to the recorded Windows source revision
6400860b9d4bec5ace91ec3a5e43ad76f9660579. Apply it before configuring either
architecture, then use these recorded configure arguments. Run the hardware-only
API regression against the resulting DLLs before distributing a new build.

FFmpeg H.264 modification
------------------------
TMD uses a modified FFmpeg build. The patch is:
  utils/ffmpeg/patches/ffmpeg-7.1.5-h264-hardware-only.diff
Target: FFmpeg n7.1.5, commit 3a0867c2bfda4a4d4309ca1a8cbdc6175e67f587.
The complete diff is reproduced at the end of this README.

The shared h264 decoder registration and bitstream parsing are retained for
hardware decoding. The patch offers only hardware pixel formats, removes CPU
slice reconstruction dispatch, disables CPU error concealment, and rejects
software output, including delayed/error-recovery frames. Software H.264 cannot
be selected through the patched FFmpeg decoder API. Some software helpers may
remain in the binary. VAAPI and D3D11VA hardware implementations are retained.
TMD also rejects software H.264 decoder selection and fallback.

This decoder patch does not change hardware encoders. h264_vaapi is a hardware
encoder. libx264 and libopenh264 software H.264 encoders are not enabled in the
builds documented here. Software MPEG-1 and VP9 remain available.

For a manual build, run from a clean FFmpeg n7.1.5 source checkout, replacing
/path/to/tmf-trunk with the actual source tree location:

  git apply --check /path/to/tmf-trunk/utils/ffmpeg/patches/ffmpeg-7.1.5-h264-hardware-only.diff
  git apply /path/to/tmf-trunk/utils/ffmpeg/patches/ffmpeg-7.1.5-h264-hardware-only.diff

Apply once before configuring any architecture. --enable-decoder=h264 alone in
an upstream, unpatched build DOES enable software H.264; it is not a substitute
for this patch. Rebase and verify the patch when changing the FFmpeg revision.
Use fresh build/install directories to avoid retaining unpatched libraries.

The source tree includes utils/ffmpeg/test-h264-hardware-only.c and its embedded
test stream. The Linux builder runs this public-API regression automatically;
manual Windows verification is documented in docs/build-FFmpeg-Windows.txt.
The checks reject software output without needing a GPU. Successful hardware
decoding must also be tested on the intended GPU/driver.


Appendix: complete FFmpeg H.264 hardware-only patch
-------------------------------------------------

diff --git a/libavcodec/h264_slice.c b/libavcodec/h264_slice.c
index aa51a588fabb8be9a65f4997dc9bbc051cd350d3..63a7b23cbe2059a581e7857f159c22115a4080fa 100644
--- a/libavcodec/h264_slice.c
+++ b/libavcodec/h264_slice.c
@@ -915,8 +915,23 @@ static enum AVPixelFormat get_pixel_format(H264Context *h, int force_callback)
 
     *fmt = AV_PIX_FMT_NONE;
 
+    /* TMF: the registered H.264 decoder is hardware-only, including for
+     * callers using the default get_format callback or explicitly requesting
+     * a software format. Never offer software fallback to ff_get_format(). */
+    fmt = pix_fmts;
+    for (int i = 0; pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
+        const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmts[i]);
+        if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
+            *fmt++ = pix_fmts[i];
+    }
+    *fmt = AV_PIX_FMT_NONE;
+    if (fmt == pix_fmts) {
+        av_log(h->avctx, AV_LOG_ERROR, "TMF H.264 requires hardware decoding; no supported hardware format.\n");
+        return AV_PIX_FMT_NONE;
+    }
+
     for (int i = 0; pix_fmts[i] != AV_PIX_FMT_NONE; i++)
-        if (pix_fmts[i] == h->avctx->pix_fmt && !force_callback)
+        if (pix_fmts[i] == h->avctx->pix_fmt && !force_callback && h->avctx->hwaccel)
             return pix_fmts[i];
     return ff_get_format(h->avctx, pix_fmts);
 }
@@ -2775,76 +2790,13 @@ finish:
  */
 int ff_h264_execute_decode_slices(H264Context *h)
 {
-    AVCodecContext *const avctx = h->avctx;
-    H264SliceContext *sl;
-    int context_count = h->nb_slice_ctx_queued;
-    int ret = 0;
-    int i, j;
-
-    h->slice_ctx[0].next_slice_idx = INT_MAX;
-
-    if (h->avctx->hwaccel || context_count < 1)
-        return 0;
-
-    av_assert0(context_count && h->slice_ctx[context_count - 1].mb_y < h->mb_height);
-
-    if (context_count == 1) {
-
-        h->slice_ctx[0].next_slice_idx = h->mb_width * h->mb_height;
-        h->postpone_filter = 0;
-
-        ret = decode_slice(avctx, &h->slice_ctx[0]);
-        h->mb_y = h->slice_ctx[0].mb_y;
-        if (ret < 0)
-            goto finish;
-    } else {
-        av_assert0(context_count > 0);
-        for (i = 0; i < context_count; i++) {
-            int next_slice_idx = h->mb_width * h->mb_height;
-            int slice_idx;
-
-            sl                 = &h->slice_ctx[i];
-
-            /* make sure none of those slices overlap */
-            slice_idx = sl->mb_y * h->mb_width + sl->mb_x;
-            for (j = 0; j < context_count; j++) {
-                H264SliceContext *sl2 = &h->slice_ctx[j];
-                int        slice_idx2 = sl2->mb_y * h->mb_width + sl2->mb_x;
-
-                if (i == j || slice_idx2 < slice_idx)
-                    continue;
-                next_slice_idx = FFMIN(next_slice_idx, slice_idx2);
-            }
-            sl->next_slice_idx = next_slice_idx;
-        }
-
-        avctx->execute(avctx, decode_slice, h->slice_ctx,
-                       NULL, context_count, sizeof(h->slice_ctx[0]));
-
-        /* pull back stuff from slices to master context */
-        sl                   = &h->slice_ctx[context_count - 1];
-        h->mb_y              = sl->mb_y;
-
-        if (h->postpone_filter) {
-            h->postpone_filter = 0;
-
-            for (i = 0; i < context_count; i++) {
-                int y_end, x_end;
-
-                sl = &h->slice_ctx[i];
-                y_end = FFMIN(sl->mb_y + 1, h->mb_height);
-                x_end = (sl->mb_y >= h->mb_height) ? h->mb_width : sl->mb_x;
-
-                for (j = sl->resync_mb_y; j < y_end; j += 1 + FIELD_OR_MBAFF_PICTURE(h)) {
-                    sl->mb_y = j;
-                    loop_filter(h, sl, j > sl->resync_mb_y ? 0 : sl->resync_mb_x,
-                                j == y_end - 1 ? x_end : h->mb_width);
-                }
-            }
-        }
+    /* TMF: hardware slices are submitted through FF_HW_CALL(decode_slice).
+     * Refuse the CPU slice decoder even if format selection is bypassed or
+     * a hardware decoder is lost while processing a packet. */
+    if (h->nb_slice_ctx_queued > 0 && !h->avctx->hwaccel) {
+        av_log(h->avctx, AV_LOG_ERROR, "TMF H.264 software slice decoding is disabled.\n");
+        h->nb_slice_ctx_queued = 0;
+        return AVERROR(ENOSYS);
     }
-
-finish:
-    h->nb_slice_ctx_queued = 0;
-    return ret;
+    return 0;
 }
diff --git a/libavcodec/h264dec.c b/libavcodec/h264dec.c
index 0154fe17b665842284c5d782ca5c895be098fbec..cb944352e6267ca59ed9e0b26ae34903cbc6bef2 100644
--- a/libavcodec/h264dec.c
+++ b/libavcodec/h264dec.c
@@ -69,35 +69,8 @@ static void h264_er_decode_mb(void *opaque, int ref, int mv_dir, int mv_type,
                               int mb_x, int mb_y, int mb_intra, int mb_skipped)
 {
     const H264Context *h = opaque;
-    H264SliceContext *sl = &h->slice_ctx[0];
-
-    sl->mb_x = mb_x;
-    sl->mb_y = mb_y;
-    sl->mb_xy = mb_x + mb_y * h->mb_stride;
-    memset(sl->non_zero_count_cache, 0, sizeof(sl->non_zero_count_cache));
-    av_assert1(ref >= 0);
-    /* FIXME: It is possible albeit uncommon that slice references
-     * differ between slices. We take the easy approach and ignore
-     * it for now. If this turns out to have any relevance in
-     * practice then correct remapping should be added. */
-    if (ref >= sl->ref_count[0])
-        ref = 0;
-    if (!sl->ref_list[0][ref].data[0]) {
-        av_log(h->avctx, AV_LOG_DEBUG, "Reference not available for error concealing\n");
-        ref = 0;
-    }
-    if ((sl->ref_list[0][ref].reference&3) != 3) {
-        av_log(h->avctx, AV_LOG_DEBUG, "Reference invalid\n");
-        return;
-    }
-    fill_rectangle(&h->cur_pic.ref_index[0][4 * sl->mb_xy],
-                   2, 2, 2, ref, 1);
-    fill_rectangle(&sl->ref_cache[0][scan8[0]], 4, 4, 8, ref, 1);
-    fill_rectangle(sl->mv_cache[0][scan8[0]], 4, 4, 8,
-                   pack16to32((*mv)[0][0][0], (*mv)[0][0][1]), 4);
-    sl->mb_mbaff =
-    sl->mb_field_decoding_flag = 0;
-    ff_h264_hl_decode_mb(h, &h->slice_ctx[0]);
+    /* TMF: error concealment must never reconstruct macroblocks on the CPU. */
+    av_log(h->avctx, AV_LOG_ERROR, "TMF H.264 software error concealment is disabled.\n");
 }
 
 void ff_h264_draw_horiz_band(const H264Context *h, H264SliceContext *sl,
@@ -427,6 +400,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
 
     ff_h264_flush_change(h);
 
+    /* TMF: do not enter software error recovery, including for damaged input. */
+    h->enable_er = 0;
+
     if (h->enable_er < 0 && (avctx->active_thread_type & FF_THREAD_SLICE))
         h->enable_er = 0;
 
@@ -938,6 +914,11 @@ static int is_avcc_extradata(const uint8_t *buf, int buf_size)
 static int finalize_frame(H264Context *h, AVFrame *dst, H264Picture *out, int *got_frame)
 {
     int ret;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(out->f->format);
+
+    /* TMF: reject software output, including delayed/corrupt field recovery. */
+    if (!h->avctx->hwaccel || !desc || !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
+        return AVERROR(ENOSYS);
 
     if (((h->avctx->flags & AV_CODEC_FLAG_OUTPUT_CORRUPT) ||
          (h->avctx->flags2 & AV_CODEC_FLAG2_SHOW_ALL) ||
@@ -1107,7 +1088,6 @@ static int h264_decode_frame(AVCodecContext *avctx, AVFrame *pict,
 static const AVOption h264_options[] = {
     { "is_avc", "is avc", OFFSET(is_avc), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, VDX },
     { "nal_length_size", "nal_length_size", OFFSET(nal_length_size), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 4, VDX },
-    { "enable_er", "Enable error resilience on damaged frames (unsafe)", OFFSET(enable_er), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VD },
     { "x264_build", "Assume this x264 version if no x264 version found in any SEI", OFFSET(x264_build), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, VD },
     { "skip_gray", "Do not return gray gap frames", OFFSET(skip_gray), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, VD },
     { "noref_gray", "Avoid using gray gap frames as references", OFFSET(noref_gray), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, VD },
