Just like you can have a std::atomic synchronizes two threads with each other with release-acquire semantic, you can also have Fence-Fence, Atomic-Fence, Fence-Atomic synchronizations. C++ reference has very detailed documentation about when there exists a valid synchronizes-with relationship,

Rust's memory model is a word-to-word copy of C++'s, and it has pretty good documentations e.g. is a good example of Fence-Fence synchronization.

Thread 1                                          Thread 2

fence(Release);      A --------------, Relaxed); X ---------    |
                               |    |
                               |    |
                               -------------> Y  if x.load(Relaxed) == 3 {
                                    |-------> B      fence(Acquire);

The aforementioned ordering can be established as well with release-store and acquire-load of an atomic. Why do we need atomic_thread_fence at all? Especially it requires an atomic variable to work correctly to begin with?

Basically it's like a batch annotation. fence(release) marks all following stores with release semantic. The following quote from C++ reference is saying effectively the same thing.

While an atomic store-release operation prevents all preceding writes from moving past the store-release, an atomic_thread_fence with memory_order_release ordering prevents all preceding writes from moving past all subsequent stores.