Unlocking PBP Mode on Dell S3423DWC Monitors


To skip this post and check out the VCP codes I found, click here.
Motivation
I love my ultrawide monitor. One of the main features that sold it to me was the picture-by-picture (PBP) mode, which allows two different input sources to be displayed simultaneously. I usually end up switching modes dozens of times per day, and it’s a hassle to navigate the monitor menu every single time. When I had Windows, I could launch different modes with the press of a button by using an external number pad, AutoHotKey, and the official Dell Display Manager app. This would still be my recommended method if I hadn’t switched to Fedora.
Unfortunately, Dell Display Manager doesn’t have a Linux executable, so I needed to figure out how the app was able to control PBP.
Background
After some time searching (and asking ChatGPT), I discovered the Virtual Control Panel (VCP) protocol. It’s implemented as a series of registers that can be read from and written to. Most manufacturers support a standard set of commands (brightness, contrast, etc.) but more niche functionality is usually hidden behind undocumented registers. And that’s where the picture by picture modes were.
Small Steps
ddcutil
is a Linux command-line tool that can query and change monitor settings via VCP. The most important commands are:
ddcutil detect
Detects all connected monitors and displays the I2C bus, which can be used to uniquely identify a display in multi-monitor setups.
ddcutil capabilities --verbose
Displays the monitor model and any known VCP registers and information about their usage. I was most interested in the manufacturer-specific registers:
... # Excerpt of command output
Feature: DF (VCP Version)
Feature: E0 (Manufacturer specific feature)
Feature: E1 (Manufacturer specific feature)
Feature: E2 (Manufacturer specific feature)
Values (unparsed): 00 02 22 20 21 0E 12 14
Values ( parsed): 00 02 22 20 21 0E 12 14 (interpretation unavailable)
Feature: E5 (Manufacturer specific feature)
...
ddcutil getvcp <REGISTER>
and
Gets more information about the register, including its current value.
ddcutil setvcp <REGISTER> <VALUE>
Self-explanatory.
Guess and Check
I was able to figure out a small selection of codes that controlled the PBP and PIP modes on the monitor through trial and error:
Register | Value | Effect |
E9 | 0x01 | PIP - Toggle PIP size (same as moving between 0x21 and 0x22) |
0x02 | Moves PIP location around screen. Cycles top right, bottom right, bottom left, top left. | |
0×21 | PIP - Small | |
0×22 | PIP - Big (1/4th of screen) | |
0×23 | PBP - 50/50 split | |
0×24 | Same as above | |
0x29 | PBP - 25/75 split | |
0x2A | PBP - 75/25 split | |
0x2D | PBP - 33/66 split | |
0x2E | PBP - 66/33 split | |
E8 | 0x0010: USB-C | |
0x0011: HDMI1 | ||
0x0012: HDMI2 | Changes secondary video source | |
E9 | 0x00 | Turns off PBP or PIP mode. |
F0 | 0x0F: FPS | |
0x10: RTS | ||
0x11: RPG | Changes color presets |
The Missing Piece
For the longest time, I couldn't find the right code to swap USB input between the different input sources. I knew it was possible since the Dell Display Manager app on Windows could do it.
Luckily, I found a KVM utility someone created for their Dell monitor. Within the source code, I found their swap USB register and it worked! I finally had everything I needed.
Register | Value | Effect |
E7 | 0xff00 | Swaps USB input when in PBP mode |
One-Button Switching
I used keyd to implement AutoHotkey-like functionality; it's simple enough to install on Fedora.
This blog post was a huge help in quickly getting up to speed on how keyd worked. To adapt it to your system, just run sudo keyd monitor
and note down which device ID shows up when you hit a key on your desired device.
Below, I've provided my keyd configuration file (place in /etc/keyd/numpad.conf
):
[ids]
1c4f:0002:3c76615e
[main]
# NOTE: The main input is always on the left.
# I want to ensure USBC is always left.
# Split 50/50
kp2 = command(~/bin/ensure-monitor-input.sh USBC; ddcutil setvcp E9 0x24)
# Split 66/33; mostly the work laptop
kp1 = command(~/bin/ensure-monitor-input.sh USBC; ddcutil setvcp E9 0x2E)
# Split 33/66; mostly the home desktop
kp3 = command(~/bin/ensure-monitor-input.sh USBC; ddcutil setvcp E9 0x2D)
# Turn off the split
kp0 = command(ddcutil setvcp E9 00)
# Switch the USB inputs
kpenter = command(ddcutil setvcp 0xe7 0xff00)
And the source to ensure-monitor-input.sh, which avoids unnecessarily changing the main input:
#!/bin/bash
# Args:
# $1 = target value for input source (VCP 0x60), e.g., 0x0010.
# OR, can accept USBC, HDMI1, or HDMI2
TARGET_INPUT=$1
if [[ $TARGET_INPUT == "USBC" ]]; then
TARGET_INPUT="x1b"
elif [[ $TARGET_INPUT == "HDMI1" ]]; then
TARGET_INPUT="x11"
elif [[ $TARGET_INPUT == "HDMI2" ]]; then
TARGET_INPUT="x12"
fi
# awk grabs the last 3 chars
CURRENT_INPUT=$(ddcutil getvcp 60 --terse | awk '{print substr($0, length($0)-2)}')
# Only change input if not already set
if [[ "$CURRENT_INPUT" != "$TARGET_INPUT" ]]; then
ddcutil setvcp 60 "$TARGET_INPUT"
fi
Conclusion
It was fun learning about VCP and trying out different manufacturer codes. I'm grateful (but a little sad) I didn't have to truly open up the monitor protocol with USB sniffing or exporting out all the registers and running diffs.
Maybe next time!
Subscribe to my newsletter
Read articles from Joshua Manuel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
