Tuesday, August 25, 2026

VGA Palette #1


Hi there. In this new series of articles, I'll be writing about color palettes of VGA. Since this is a relatively deep topic, I planned to cover it over several posts. In this post, I'll first focus on VGA 640 x 480 (aka screen 12) and text mode to explain what the color palette is, how to access and change it. I'll mention mode 13h in a separate post later and discuss some visual effects, based on VGA palette.


VGA introduced two important graphics modes. First one is mode 12h with 640 x 480 resolution and 16 colors, and the other one is well-known mode 13h, with 320 x 200 resolution and 256 colors. Other modes were retained for backwards compatibility with older graphics hardware. The VGA card's digital-analog converter (DAC) can display colors from 18-bit RGB color gamut (218 = 256 K = 262 144 colors) on the screen [1], but under mode 13h, only 256 of these at the same time, and under mode 12h only 16. This subset of colors from this color gamut is called a palette.

Color information consists of a palette index and an 18-bit RGB color value (6+6+6). For example, if the index is 1 and the RGB code is 0,0,63, the first color is blue; if the index is 2 and the RGB code is 24,24,24, the second color is gray, and so on. The index value starts at zero, and the zeroth color is the background color, which is therefore usually black (RGB 0,0,0). If the indices from 0 to 255 are filled with linearly increasing t values in RGB t,t,t form, a grayscale palette is obtained. Or if they are filled in the form RGB 0,r,0, a purely green toned palette is obtained.

Colors are processed through the DAC of graphics card. Therefore, palette operations are carried out using three* DAC registers of VGA card [3]:

DAC Address Read Mode Register (Write at 3C7h)
76543210
DAC Read Address

Actually, 3C7h has two functions. If this register is read, the two least significant bits indicate, whether the DAC is in read or write mode. However, if I've already executed an out instruction, I've written, or if I've executed an in instruction, I've read. This is why, this function of the register isn't used any often. To read the palette, the index value is written to this register. Then, the DAC data register on port 3C9h is read three times each in byte-length:


DAC Data Register (Read/Write at 3C9h)
76543210


DAC Data

When writing to the palette, the color index is written to port 3C8h, and three byte-length values are sent to the DAC data register.


DAC Address Write Mode Register (Read/Write at 3C8h)
76543210
DAC Write Address

*There are actually four DAC registers. The DAC Mask register, which I did not mention above, is accessed via port 3C6h and always contains 0FFh value. Writing any other value to this register disables access to the DAC [2].

The aforementioned byte-length read and write operations refer to in al,dx and out dx,al instructions. However, only the lowest 6 bits of the read and written values are significant. Remember that the color gamut is 6+6+6=18 bits.


Let's focus on the text mode and mode 12h, first. In these two modes, a maximum of 16 colors can be displayed on the screen. In this respect, they are similar. One detail, which goes often unnoticed is that the text mode palette can also be modified. In my first VGA post [5], I explained, how to change text and background colors of a text directly. Now, let's take a look at the text mode palette using a simple BASIC code:

FOR I% = 0 TO 63
    OUT &H3C7, I%
    PRINT "("; I%; "="; INP(&H3C9); INP(&H3C9); INP(&H3C9); ")";
NEXT I%

DEF SEG = &HB800

FOR J% = 1 TO 15
    FOR I% = 1 TO 159 STEP 2
        POKE (I% + J% * 160), J%
    NEXT I%
NEXT J%

In first part, I send the color index value from 3C7h, then read the color codes from 3C9h, and printed them to the screen. In second part, I accessed the text mode video memory and colored the first line with the first palette color, the second line with the second palette color, and so on:

I mentioned that 16 colors can be used in text mode, but I printed 64 color codes to the screen, and interestingly, it appears that non-zero codes have been assigned to the indices between [16, 63]. The codes for indices between [64, 255] are zero, therefore not printed, but colors can be assigned to them as well if needed. So, what's the meaning of this? Normally, a character on the screen consists of 2 bytes: one byte is its ASCII code, and the next one holds its color information. The lower 4 bits of the color byte represent the character's color, while the upper 4 bits represent the character's background color. Since just 4 bits are allocated for colors, assigning a color code to the indices 16 and above might seem pointless at first glance, but there is a trick: The VGA Attribute Register (3C0h) [4] can be used to change a color's palette index. Without getting into too much detail, here is a simple code snippet:

A% = INP(&H3DA)
OUT &H3C0, 5
OUT &H3C0, 60
OUT &H3C0, &H20

where I assigned the 60th color to the fifth one, by writing the value 60, into the fifth attribute register.

Getting back to the screenshot, the eighth palette entry (0, 0, 21) should be navy blue or dark blueish, and the ninth palette entry (0, 0, 63) should be pure blue. However, assuming that we're counting from zero, the eighth row is actually dark grey instead of navy blue, and the ninth row, which supposed to be vivid blue, is just a pale blue (neon blue). If these color codes printed on the screen were represented the actual colors of the rows, the screen would actually look like the right side of the image below. The left side shows, what actually visible is. For an easy comparison, I've put two images side by side:


The conclusion is, that the text mode is actually using attribute registers for the colors [8, 15]. To render the left side of the above image, I manually assigned the first fifteen colors to the first fifteen palette indices manually.

Everything about text mode palette also applies to mode 12h palette. Even though 16 colors can be shown at any time, the default palette contains 64 colors. Hint: If you switch from mode 13h to mode 12h or to text mode, mode 13h palette will also stay in DAC for other modes. In other words, when the computer (or DosBox) starts up, all colors in the range [64, 255] are set to (0, 0, 0). If you just enter mode 13h and switch back to text mode (or mode 12h), these indices won't be containing zeros anymore. 

Similar to text mode, the colors on the screen are selected from the palette, but the attribute register provides a second conversion layer. Here is another BASIC code snippet to demonstrate all these:

SCREEN 12
CONST K = 20

FOR I% = 0 TO 63
    OUT &H3C7, I%
    PRINT "("; I%; "="; INP(&H3C9); INP(&H3C9); INP(&H3C9); ")";
NEXT I%

SLEEP

FOR I% = 0 TO 15
    ' PRINTING RECTANGLES
    LINE (0, I% * K)-(640, (I% + 1) * K), I%, BF
NEXT I%

FOR I% = 0 TO 15
    ' SETTING PALETTE
    OUT &H3C8, I%
    OUT &H3C9, I% * 4
    OUT &H3C9, I% * 0
    OUT &H3C9, 63 - I% * 4
NEXT I%

SLEEP

' Change fifth color
A% = INP(&H3DA)
OUT &H3C0, 5
OUT &H3C0, 60
OUT &H3C0, &H20
 

In the first part, color codes of indices [0, 63] are printed to the screen. In the next part, 16 rectangles, whose size set by the const K, are drawn on the screen using first 16 colors and then 16 colors ranging from blue to red are assigned to the palette. As demonstrated, colors are written to the palette by writing their palette index to the port 3C8h and sending their color codes via port 3C9h afterwards.

Since blue is assigned to the zeroth color here, the background becomes blue. In the final part, the 60th palette entry is assigned to the fifth one, clearly highlighting the color difference.



[1]: https://en.wikipedia.org/wiki/Video_Graphics_Array
[2]: https://wiki.osdev.org/VGA_Hardware#Port_0x3C6
[3]: http://www.osdever.net/FreeVGA/vga/colorreg.htm
[4]: http://www.osdever.net/FreeVGA/vga/attrreg.htm
[5]: https://trapgate.blogspot.com/2025/11/programming-vga-smooth-scrolling-in.html

Sunday, March 22, 2026

Setting Up a 4-Node GlusterFS Cluster on CentOS 9: Is it Still Worth it?


Hi there. In this blog post of mine, I'll be setting up a storage cluster on Linux machines using a hyperconverged architecture, but first let me clarify what these mambo jambo terms mean. By hyperconverged, I mean, I don't have any disk enclosure or any hardware dedicated to store data. Instead, I have multiple identical machines and each of them have bunch of unused disks. I'll unify this disk space in a redundant and highly available configuration and serve it to the other machines, i.e. clients. It's important to have identical machines, because files will be split into chunks and these chunks will be distributed across all nodes for redundancy and performance. Therefore, if the smallest disk in the cluster fills up, new file chunks cannot be written to all disks. Additionally, the CPU and memory must be also equal, so that when one machine finishes writing, it doesn't have to wait for the other nodes.

I'll be using GlusterFS as distributed file system, primarily because its initial setup is relatively simple, whereas advanced configuration can get quite complicated. I won't be going into those details in this article. As an alternative, I could have used Ceph or Quobyte, but Ceph, in my opinion, is much more complicated than GlusterFS. I worked with Quobyte on a project, and unlike GlusterFS and Ceph, its setup and management is incredibly easy. However, GlusterFS is (was) directly supported by RHEL. But I'll get to that.

Normally, servers with terabyte-sized storage are used for such kind of project. To demonstrate a proof of concept, I'll use four machines with 20 GB disks and share a total of 60 GB of disk space with 75% efficiency, calculated as 20 GB * 75% * 4 = 60 GB. The remaining %25 of the space will be used for high availability and error correction. This capacity is directly proportional to the capacity of the underlying disks. On the other hand, the number of machine plays a more important role here. If I were to setup this cluster with three machines, the recommended configuration is a RAID1 like setup with 33% efficiency. This means, you can get a 20 GB shared disk from three machines, each with a 20 GB disk. Of course, the cluster could also be set up using RAID0 like logic, but in this case, even if just a single machine in the cluster gets rebooted, it will yield data corruption and loss. A four-machine cluster offers a quite high efficiency in terms of usable space with the smallest number of nodes and without compromising on high availability. In this configuration, the disks work under a RAID5 like logic, and the system doesn't get affected even if one machine crashes or gets rebooted. On the other hand, I mentioned that the configuration could get complicated. For example, four machines could also be configured with a RAID10 like logic, where half of the file chunks are stored on one node, and the other half on another, with the remaining two machines mirroring the data of the first two. In this case, the system can withstand the failure of two machines, as long as these two machines are not in same mirror group, but the storage efficiency drops to 50%.

I will use the samba service to share the disk over the network. If there are solely Linux clients on the network, NFS is also an option. Another reason to choose samba is that it can provide high availability with ctdb service. Ctdb is a part of samba and ctdb also supports NFS. On the other hand, if there are Windows clients on the network, too, samba is the only viable option. And if disk or directory authorization is going to be managed via Active Directory (AD), it is easier to handle this with samba.

As distro, I will be using CentOS9, but I also tested the GlusterFS commands and samba configuration on Ubuntu. In that regard, the configuration steps will be distro-agnostic as much as possible, except dnf. I will clone the machines from Cloud Image.

First of all, since I'll be installing on multiple machines, I opened four different panes in tmux and after connecting to each node on each pane, I ran set syn command of tmux. In this way, the commands will be run on all machines simultaneously.

An update on four panes in tmux

After establishing ssh session to each machine, I installed the necessary packages. The first line is for the tools for troubleshooting. The second line installs the repository containing GlusterFS packages, and the third line installs the glusterfs-server package. In the following lines, I install samba related packages, update the system and finally reboot the system if necessary using needs-restart .

dnf -y install tcpdump telnet wget epel-release
dnf -y install centos-release-gluster9
dnf -y install glusterfs-server

dnf -y install samba cifs-utils samba samba-common-tools samba-winbind ctdb --enablerepo=resilientstorage

dnf update
needs-restarting -r || reboot

GlusterFS requires, that the machines are able to reach each other using their hostnames. This should be normally done using DNS records. Since I'm working in a tiny test environment, I manually add the machines' IP addresses to their hosts file. I name the machines server01 thru server04 as shown below. I then assign the hostnames to the machines using the second command. Of course, this command must be entered individually, and if you want, bash to display the new hostname, you must log out and log back in (for Ubuntu use hostnamectl set-hostname server01).

cat >> /etc/hosts << EOF
172.18.186.101   server01
172.18.186.102   server02
172.18.186.103   server03
172.18.186.104   server04
EOF

nmcli gen hostname server01
chronyc sources

GlusterFS is a very time-sensitive service, so there should be theoretically no time difference between machines. Therefore, I check whether chrony is running using the last command above. By the way, Ubuntu has its own NTP client instead of chronyd but you can install chronyd explicitly and the system's own NTP client will be uninstalled automatically.

output of chronyc sources

Now, setting up the disks. First, I check the disks using lsblk . My disk is /dev/vdb. To make it more flexible, I create a disk partition, and set up a logical volume using LVM inside this partition and then use it.

fdisk /dev/vdb
# I won't go into the partitioning details here. You can find how to do it in previous posts.

pvcreate /dev/vdb1
vgcreate vg_gluster /dev/vdb1
lvcreate -l 100%FREE -n lv_brick vg_gluster
mkfs.xfs -f -i size=512 /dev/vg_gluster/lv_brick
mkdir -p /data/glusterfs/brick1
echo "/dev/vg_gluster/lv_brick  /data/glusterfs/brick1  xfs  defaults  1  2" >> /etc/fstab
systemctl daemon-reload
mount -a

If everything has gone smoothly so far, the final command should not produce any output, and the newly mounted partition should appear in df -hP output. At this point, the machines are ready for GlusterFS installation.

I create a GlusterFS volume called shared_storage using the commands below. The systemctl command will be run on all machines. The remaining gluster commands will be run on the first machine, only.

systemctl enable --now glusterd

gluster peer probe server02
gluster peer probe server03
gluster peer probe server04
gluster volume create shared_storage disperse 4 redundancy 1 \
server01:/data/glusterfs/brick1/brick \
server02:/data/glusterfs/brick1/brick \
server03:/data/glusterfs/brick1/brick \
server04:/data/glusterfs/brick1/brick

gluster volume start shared_storage

gluster volume status shared_storage

gluster volume status shared_storage

In the image above, when I entered the command on all machines at the same moment, the first two machines are waiting for the command to finish on the other machines. This is the normal case. The command likely ran on server03 and server04 with a few milliseconds difference. Normally, it should have produced output on just a single machine.

By the way, I do not make any configuration on the host firewall, as firewalld isn't running on my machines.

At this point, I have to use a little workaround for samba. Normally, samba had GlusterFS integration, and it used to work quite well [1]. However, RedHat decided to remove GlusterFS support as of the end of 2024 and shift the resources to Ceph [2][3]. For this reason, this integration has been removed from the distros [5]. In other words, samba-vhs-glusterfs package is no longer available for RedHat and its compatibles. This issue can be worked around as follows (to be run on all machines). Fedora still supports this [4], but I do not know for how long.

mkdir -p /mnt/gluster_shared
echo "localhost:shared_storage  /mnt/gluster_shared  glusterfs  defaults,_netdev  0  0" >> /etc/fstab
systemctl daemon-reload
mount -a

In this way, all machines mount GlusterFS shared disk on themselves. Since I cannot get samba to communicate directly with GlusterFS, I mount and serve this resource via samba as if it is a normal directory. Its drawback is slightly less performance, due to the file system operations having to go through the kernel twice instead of once [1].

Now I can configure ctdb and share the disk over the network, but before I do that, I need to set selinux to Permissive mode. A few sources mention that a ctdb cluster can also be set up without disabling selinux by adjusting only couple booleans, but it didn't work for me.

sed -i -e "s/SELINUX=enforcing/SELINUX=permissive/" /etc/selinux/config
setenforce 0

Only the IPs of the nodes should be in /etc/ctdb/nodes file. In /etc/ctdb/public_addresses file, you must enter the floating IP of the cluster and the network interface to which this IP will be assigned.

cat > /etc/ctdb/nodes << EOF
172.18.186.101
172.18.186.102
172.18.186.103
172.18.186.104
EOF

cat > /etc/ctdb/public_addresses << EOF
172.18.186.200/24    eth0
EOF

The contents of smb.conf file should be like this:

[global]
        netbios name = GLUSTER_CLUSTER
        workgroup = SAMBA
        #security = user
        clustering = yes

        passdb backend = tdbsam
        idmap config * : backend = tdb
        idmap config * : range = 1000000-1999999

        #printing = cups
        #printcap name = cups
        #load printers = yes
        #cups options = raw

[shared_storage]
    comment = GlusterFS
    path = /mnt/gluster_shared
    valid users = sambauser
    read only = no
    guest ok = yes
    create mask = 0664
    directory mask = 0775

And I configure ctdb then as follows:

systemctl stop smb nmb
systemctl disable smb nmb
ctdb event script enable legacy 00.ctdb
ctdb event script enable legacy 10.interface
ctdb event script enable legacy 50.samba
systemctl enable --now ctdb

The enabled scripts are for ctdb to manage the floating IP and clustered samba. The status of the cluster can be checked using ctdb status command. If everything is configured correctly, all nodes should be "OK" in the output, a few seconds after running the last command. By the way, it is also possible to assign multiple floating IPs to the cluster, I only assigned one. The status of the IP(s) can be checked using ctdb ip all command.

ctdb status

Now final step is to create a user on all samba nodes:

groupadd -g 2000 sambagroup
useradd -u 2000 -g sambagroup -s /sbin/nologin sambauser
smbpasswd -a sambauser

The system password of sambauser isn't needed as samba keeps its own database.

Thus, the storage cluster is up and running. Now I need a client to mount and test it. Setting this up is much easier. I run following commands on a fifth machine on the same subnet:

dnf -y install centos-release-gluster9
dnf -y install glusterfs-fuse samba-client cifs-utils
mkdir -p /mnt/my_shared_storage

cat >> /etc/hosts << EOF
172.18.186.101   server01
172.18.186.102   server02
172.18.186.103   server03
172.18.186.104   server04
EOF

mount -t glusterfs 172.18.186.200:/shared_storage /mnt/my_shared_storage/
df -hP

With the first command, I install the glusterfs repository. Then I install the package, needed to mount glusterfs and also samba client, with the second command. On the client, I also create the hosts file containing the list of all servers. Even though the client mounts the storage via a floating IP, it has to be able to resolve IP addresses of cluster nodes for intracluster communication. This step is of course not necessary in an environment with DNS server. In the final step, I mount shared_storage as glusterfs, and when I check with df a 60 GB disk was mounted. But I have not mounted it as samba, yet.

Before mounting as samba, I can first view shared resources using the first command below, and then connect to this share using the second command:

smbclient -L //172.18.186.200 -U sambauser
smbclient //172.18.186.200/shared_storage -U sambauser

Before mounting this share, I unmount the GlusterFS, mounted in previous step, and then mount the resource to the same mount point as a samba share:

umount /mnt/my_shared_storage
mount -t cifs //172.18.186.200/shared_storage /mnt/my_shared_storage -o username=sambauser

And add the following line to /etc/fstab to make it permanent:

//10.0.100.231/shared_storage  /mnt/my_shared_storage  cifs  credentials=/etc/samba/user.cred,iocharset=utf8,_netdev 0 0

/etc/samba/user.cred has the login credentials.

cat >> /etc/samba/user.cred << EOF
username=sambauser
password=secret
domain=SAMBA
EOF

chmod go= /etc/samba/user.cred
chown root:root /etc/samba/user.cred
systemctl daemon-reload
mount -a
df -hP

I conclude this post here for now, as it has got long enough. I plan to explain how to integrate this with AD or LDAP in a future post.


Sources:

[1]: https://lalatendu.org/2014/04/20/glusterfs-vfs-plugin-for-samba/
[2]: https://en.wikipedia.org/wiki/Gluster#cite_ref-10
[3]: https://www.reddit.com/r/kubernetes/comments/zojdl7/whats_the_story_behind_the_abandonment_with/
[4]: https://pkgs.org/search/?q=samba-vfs-glusterfs
[5]: https://www.samba.org/samba/docs/4.7/man-html/vfs_glusterfs.8.html