Assuming your messages are serially numbered, a "high water mark" is just "the ID of the last message I read in this forum." You could do the same thing with timestamps rather than message IDs, if you prefer. Depending on how you built your data model, IDs might be faster.
Let's say you have a model where there are separate tables for rooms (forums), users, and messages. You need one more table. In one system I built, the highwater table looks like this
CREATE TABLE hiwater (
uid int(11) DEFAULT '0' NOT NULL,
roomid int(11) DEFAULT '0' NOT NULL,
messageid int(11) DEFAULT '0' NOT NULL,
KEY uid (uid, roomid)
);
So the logic is:
Find the messages where the room (forum) matches the current room, and where the message ID is greater than the current user's highwater mark for this room.
And don't forget to UPDATE (or INSERT, on a first read), the data in the highwater table, based on the last message ID of your retrieved bundle of messages.