Read/Write Offsets and Seeking

Learn how to manipulate the read/write offset of an SDL_RWops object to control stream interactions.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

This lesson focuses on two essential functions for working with SDL_RWops: SDL_RWtell() and SDL_RWseek().

  • SDL_RWtell() provides the current read/write offset
  • SDL_RWseek() lets you modify it

This gives us fine-grained control over stream interactions. We'll examine how these functions work together and apply them to a practical example of managing a game's high score.

Read/Write Offsets

Behind the scenes, SDL_RWops objects maintain a simple integer value that gets updated as we perform read and write operations. This value is sometimes called the read/write position, cursor, or offset.

Its purpose is to keep track of the data we’ve already read, or already written. For example, the following program is reading a file containing 10 characters - HelloWorld.

When we read 5 characters using SDL_RWread(), the internal offset of the RWOps object we’re using is updated, such that the next read begins where we left off:

// content.txt
HelloWorld
#include <SDL.h>
#include <iostream>

int main(int, char**) {
  SDL_RWops* File{SDL_RWFromFile(
    "content.txt", "rb")};

  // 5 bytes + 1 for null terminator
  char Content[6];
  Content[5] = '\0';

  SDL_RWread(File, Content, 1, 5);
  std::cout << "First Read: " << Content;

  SDL_RWread(File, Content, 1, 5);
  std::cout << "\nSecond Read: " << Content;

  SDL_RWclose(File);
  return 0;
}
First Read: Hello
Second Read: World

SDL_RWtell()

We can retrieve the current offset of an SDL_RWops object by passing it to the SDL_RWtell() function. When we first create an SDL_RWops, we expect its offset to be 0, but this will change as we interact with the stream:

#include <SDL.h>
#include <iostream>

int main(int, char**) {
  SDL_RWops* File{SDL_RWFromFile(
    "content.txt", "rb")};

  char Content[6];
  Content[5] = '\0';

  std::cout << "Read/Write Offset: "
    << SDL_RWtell(File);
  SDL_RWread(File, Content, 1, 5);
  std::cout << "\nFirst Read: " << Content;

  std::cout << "\nRead/Write Offset: "
    << SDL_RWtell(File);
  SDL_RWread(File, Content, 1, 5);
  std::cout << "\nSecond Read: " << Content;

  std::cout << "\nRead/Write Offset: "
    << SDL_RWtell(File);

  SDL_RWclose(File);
  return 0;
}
Read/Write Offset: 0
First Read: Hello
Read/Write Offset: 5
Second Read: World
Read/Write Offset: 10

Error Handling

The SDL_RWtell() function can sometimes fail. When this happens, it will return a negative error code. We can check for this outcome and react to it as needed. We can also call SDL_GetError() for more information on what went wrong:

if (SDL_RWtell(File) < 0) {
  std::cout << "Couldn't determine read/write "
    "offset: " << SDL_GetError();
}

SDL_RWseek()

We can manipulate the read/write offset of an SDL_RWops object using the SDL_RWseek() function. This function requires three arguments:

  1. The stream: the pointer to the SDL_RWops object whose read/write offset we want to update
  2. The seek value: an integer representing how far we want the offset to move, relative to the anchor. This can be a negative value if we want to move the offset backwards, or a positive value to move it forward
  3. The anchor: where we want to offset to move from

The second and third arguments work together to determine where the offset should be after our call to SDL_RWseek(). The possible values for the third argument are:

  • RW_SEEK_SET: Our seek value sets the offset relative to the start of the stream. We should provide a positive seek value in this context.
  • RW_SEEK_CUR: Our seek value moves the offset relative to the current position of the offset. Our seek value can be positive or negative in this context.
  • RW_SEEK_END: Our seek value sets the offset relative to the end of the stream. Our seek value should be negative in this context.

Here’s an example:

#include <SDL.h>
#include <iostream>

int main(int, char**) {
  SDL_RWops* File{SDL_RWFromFile(
    "content.txt", "rb")};

  char Content[6];
  Content[5] = '\0';

  std::cout << "Read/Write Offset: "
    << SDL_RWtell(File);

  SDL_RWseek(File, 5, RW_SEEK_SET);

  std::cout << "\nRead/Write Offset: "
    << SDL_RWtell(File);

  SDL_RWread(File, Content, 1, 5);
  std::cout << "\nFirst Read: " << Content;

  SDL_RWclose(File);
  return 0;
}
Read/Write Offset: 0
Read/Write Offset: 5
First Read: World

Return Value

The SDL_RWseek() function returns the new offset of the SDL_RWops object, relative to the beginning of the stream:

#include <SDL.h>
#include <iostream>

int main(int, char**) {
  SDL_RWops* File{SDL_RWFromFile(
    "content.txt", "rb")};

  std::cout << "Read/Write Offset: "
    << SDL_RWtell(File);

  std::cout << "\nRead/Write Offset: "
    << SDL_RWseek(File, -3, RW_SEEK_END);

  SDL_RWclose(File);
  return 0;
}
Read/Write Offset: 0
Read/Write Offset: 7

Error Handling

The SDL_RWseek() function can sometimes fail. When this happens, it will return -1. We can check for this outcome and react to it as needed. We can also call SDL_GetError() for more information on what went wrong:

#include <SDL.h>
#include <iostream>

int main(int, char**) {
  SDL_RWops* File{SDL_RWFromFile(
    "content.txt", "rb")};

  if (SDL_RWseek(File, -5, RW_SEEK_SET) < 0) {
    std::cout << "Error seeking: "
      << SDL_GetError();
  }

  SDL_RWclose(File);
  return 0;
}
Error seeking: windows_file_seek: An attempt was made to move the file pointer before the beginning of the file.

Example: Tracking High Score

In this section, we’ll create a simple example where we keep track of the player’s highest score as a 32-bit integer serialized in a highscore.dat file on their hard drive.

Every time the player gets a new score, we’ll compare it to the score in that file and, if it’s higher, we’ll update the file.

Let’s handle the initial case first. The first time we call UpdateHighScore(), the highscore.dat file will not exist. So, we’ll call CreateAndInitializeFile() to create it, and initialize it with the score the player received:

#include <SDL.h>
#include <iostream>

void CreateAndInitializeFile(
  const char* Path, int32_t InitialScore
) {
  SDL_RWops* File{SDL_RWFromFile(Path, "w+b")};
  if (!File) {
    // Handle errors
    return;
  }

  SDL_RWwrite(File, &InitialScore, sizeof(int32_t), 1);
  SDL_RWclose(File);
}

void UpdateHighScore(
  const char* Path, int32_t NewScore
) {
  SDL_RWops* File{SDL_RWFromFile(Path, "r+b")};
  if (!File) {
    CreateAndInitializeFile(Path, NewScore);
    return;
  }
  
  // TODO

  SDL_RWclose(File);
}

int main(int, char**) {
  UpdateHighScore("highscore.dat", 5000);
  return 0;
}

On subsequent calls to UpdateHighScore(), there will be a highscore.dat file available. We’ll open that file for reading and writing, and retrieve the current high score.

If our new score is higher than what is currently in the file, we’ll rewind our read/write offset to the beginning of the file using SDL_RWseek(), and overwrite that integer with our higher score:

#include <SDL.h>
#include <iostream>

void CreateAndInitializeFile(
  const char* Path, int32_t InitialScore
) {
  SDL_RWops* File{SDL_RWFromFile(Path, "w+b")};
  if (!File) {
    return;
  }

  SDL_RWwrite(File, &InitialScore, sizeof(int32_t), 1);
  SDL_RWclose(File);
}

void UpdateHighScore(
  const char* Path, int32_t NewScore
) {
  SDL_RWops* File{SDL_RWFromFile(Path, "r+b")};
  if (!File) {
    CreateAndInitializeFile(Path, NewScore);
    return;
  }

  int32_t CurrentScore{0};
  SDL_RWread(File, &CurrentScore, sizeof(int32_t), 1);

  std::cout << "Current High Score: "
    << CurrentScore << "\n";

  if (NewScore > CurrentScore) {
    std::cout << NewScore << " is a new high score!"
      " Updating file\n";
    SDL_RWseek(File, 0, RW_SEEK_SET);
    SDL_RWwrite(File, &NewScore, sizeof(int32_t), 1);
  }

  SDL_RWclose(File);
}

int main(int, char**) {
  // New high score
  UpdateHighScore("highscore.dat", 5500);
  return 0;
}
Current High Score: 5000
5500 is a new high Score! Updating file

Advanced Serialization

The examples in this chapter have focused on slow, manual implementations of serialization and deserialization so we can build a deeper understanding of what is going on.

In larger projects, we typically want to make it as easy as possible to add serialization and deserialization capabilities to the classes and structs we define. Additionally, we have to deal with more complex situations that include:

  • Inheritance: How do we handle objects that include inherited members?
  • Reference Types: How do we handle objects that include references or pointers to other objects - for example, a Character with a Weapon* member?
  • Polymorphic Types: If we have a pointer or reference to a polymorphic type, how do we serialize that object including data members from its more derived type?
  • Versioning: When we update our game, how do we ensure saved files based on previous versions are still usable, even if we updated, added, or removed class variables?

Fortunately, these are largely solved problems. We have a wide range of open-source serialization libraries that we can add to our project for free.

Many of these libraries, like Cereal, also include support for serializing standard library types like std::string and std::vector, further reducing the amount of code we need to write. We cover Cereal in detail in our advanced course:

Summary

In this lesson, we explored the vital concept of the read/write offset in SDL_RWops, learning how to use SDL_RWtell() to get the current position and SDL_RWseek() to move to different locations within a stream. Here are the key points:

  • The read/write offset tracks the current position within an SDL_RWops stream.
  • SDL_RWtell() returns the current offset.
  • SDL_RWseek() allows you to move the offset to a specific location.
  • RW_SEEK_SET, RW_SEEK_CUR, and RW_SEEK_END define the anchor for seeking.
  • We can detect and react to errors when using SDL_RWtell() and SDL_RWseek().

Was this lesson useful?

Next Lesson

Maths and Geometry Primer

Learn the basics of maths and geometry, so we can represent concepts like positions and distances between objects in our worlds.
3D art showing a teacher in a classroom
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Read/Write Offsets and Seeking

Learn how to manipulate the read/write offset of an SDL_RWops object to control stream interactions.

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Reading and Writing (RWops)
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 79 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Maths and Geometry Primer

Learn the basics of maths and geometry, so we can represent concepts like positions and distances between objects in our worlds.
3D art showing a teacher in a classroom
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved