TL;DR - don't use tristate logic. use a separate data_in and data_out bus. If that doesn't work it is your logic at fault.
I've never seen an FPGA with tristate logic within the fabric. On the edge of the FPGA a tristate driver is implemented using an I/O buffer that looks like this:

The tools map an "inout" port to 'I', 'O' and 'T' inputs. If anybody asked me, I strongly recommend you never pass an INOUT signal outside of the top-level of a design, but quickly convert it into data_in, data_out, tristate signals. This stops a logic error deep in your design locking a bus that may run across the entire design - which is a real pain to debug. Another trap for young players is that the 'T' signal can be relatively slow to take effect, and can be a problem for high speed designs like SDRAM interfaces.
However... if you really have to use any tristate logic within the internals of your FPGA project the tools attempt to map it to a common bus with a single driver, and logic structure that selects who is driving that bus at any time - maybe something like:
bus <= (driver1_data and driver1_enable) or (driver2_data and driver2_enable) or (driver3_data and driver3_enable) or ....
Unlike actual tristate drivers this is not a very efficient structure and doesn't scale very well as you end up with high fan-out nets running all over the FPGA.
But it does tell you something that may be of value - each module that connects to the bus must have a clearly identifiable "output_enable" signal for each module that drives the bus, and for the design to work properly you must ensure that only one output_enable for a bus can be asserted at any time.
So if you are connecting a memory to a CPU you need something like this:
with output_enable select data <= "ZZZZ" when '0', mem(addr) when others;
...and output_enable should only be asserted only during read cycles, (during which time the CPU's own output_enable has to be turned off).
If you have multiple components drive the same bus at the same time, nothing really bad will happen (no flames or smoke), but the value on the logical bus will be some mashup of the requested values - and this is why you are getting the 'X's during simulation - it is where the data bus is getting driven with a "1" by one device and a "0" by another.